Create infra scripts

This commit is contained in:
2026-02-02 16:38:16 +02:00
parent dd5c88e3bb
commit 68382d62a1
5 changed files with 425 additions and 4 deletions

View File

@@ -0,0 +1,138 @@
from __future__ import annotations
import os
from pathlib import Path
from typing import Optional
import requests
from dotenv import load_dotenv
from uv_app.core.mssql import connect_to_mssql
BASE_URL = "https://api.manorivile.lt/client/v2"
TIMEOUT = 30
DOTENV_PATH = Path(__file__).resolve().parents[2] / ".env"
load_dotenv(DOTENV_PATH, override=True)
SERVICE_CODE = "KAINA"
SERVICE_NAME = "Kaina"
SERVICE_UOM = "VNT"
def _get_api_key() -> str:
api_key = os.getenv("RIVILE_API_KEY", "").strip()
if not api_key:
raise RuntimeError("Missing RIVILE_API_KEY environment variable.")
return api_key
def _post(api_key: str, payload: dict) -> dict:
headers = {
"ApiKey": api_key,
"Content-Type": "application/json",
"Accept": "application/json",
}
response = requests.post(
BASE_URL,
json=payload,
headers=headers,
timeout=TIMEOUT,
)
if response.status_code != 200:
raise RuntimeError(f"Rivile HTTP {response.status_code}: {response.text}")
data = response.json()
if "errorMessage" in data:
raise RuntimeError(f"Rivile API error: {data}")
return data
def _service_exists(api_key: str, code: str) -> bool:
payload = {
"method": "GET_N17_LIST",
"params": {
"list": "H",
"fil": f"n17_kodas_ps='{code}'",
"limit": 1,
},
}
data = _post(api_key, payload)
return bool(data.get("list") or data.get("N17"))
def _choose_service_ds() -> Optional[str]:
conn = connect_to_mssql()
if conn is None:
raise RuntimeError("Failed to connect to MSSQL.")
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT N17_KODAS_DS, COUNT(*) AS cnt
FROM dbo.N17_PROD
WHERE N17_TIPAS = 2
AND N17_KODAS_DS IS NOT NULL
AND LTRIM(RTRIM(N17_KODAS_DS)) <> ''
GROUP BY N17_KODAS_DS
ORDER BY cnt DESC, N17_KODAS_DS
"""
)
rows = cursor.fetchall()
finally:
conn.close()
if not rows:
return None
return str(rows[0][0]).strip()
def create_service_kaina() -> dict:
api_key = _get_api_key()
service_ds = _choose_service_ds()
if not service_ds:
return {"status": "cancelled", "reason": "No N17_KODAS_DS selected"}
n17 = {
"N17_KODAS_PS": SERVICE_CODE,
"N17_TIPAS": "2",
"N17_PAV": SERVICE_NAME,
"N17_KODAS_US": SERVICE_UOM,
"N17_KODAS_DS": service_ds,
}
if _service_exists(api_key, SERVICE_CODE):
payload = {
"method": "EDIT_N17",
"params": {
"oper": "U",
"user": api_key.split(".", 1)[0],
"fld": "N17_KODAS_PS",
"val": SERVICE_CODE,
},
"data": {"N17": n17},
}
data = _post(api_key, payload)
return {"status": "updated", "code": SERVICE_CODE, "response": data}
payload = {
"method": "EDIT_N17",
"params": {"oper": "I"},
"data": {"N17": n17},
}
data = _post(api_key, payload)
return {"status": "created", "code": SERVICE_CODE, "response": data}
def main() -> None:
result = create_service_kaina()
print(result)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,137 @@
from __future__ import annotations
import os
from pathlib import Path
from typing import Optional
import requests
from dotenv import load_dotenv
from uv_app.core.mssql import connect_to_mssql
BASE_URL = "https://api.manorivile.lt/client/v2"
TIMEOUT = 30
DOTENV_PATH = Path(__file__).resolve().parents[2] / ".env"
load_dotenv(DOTENV_PATH, override=True)
SERVICE_CODE = "STATUSAS"
SERVICE_NAME = "Statusas"
SERVICE_UOM = "VNT"
def _get_api_key() -> str:
api_key = os.getenv("RIVILE_API_KEY", "").strip()
if not api_key:
raise RuntimeError("Missing RIVILE_API_KEY environment variable.")
return api_key
def _post(api_key: str, payload: dict) -> dict:
headers = {
"ApiKey": api_key,
"Content-Type": "application/json",
"Accept": "application/json",
}
response = requests.post(
BASE_URL,
json=payload,
headers=headers,
timeout=TIMEOUT,
)
if response.status_code != 200:
raise RuntimeError(f"Rivile HTTP {response.status_code}: {response.text}")
data = response.json()
if "errorMessage" in data:
raise RuntimeError(f"Rivile API error: {data}")
return data
def _service_exists(api_key: str, code: str) -> bool:
payload = {
"method": "GET_N17_LIST",
"params": {
"list": "H",
"fil": f"n17_kodas_ps='{code}'",
"limit": 1,
},
}
data = _post(api_key, payload)
return bool(data.get("list") or data.get("N17"))
def _choose_service_ds() -> Optional[str]:
conn = connect_to_mssql()
if conn is None:
raise RuntimeError("Failed to connect to MSSQL.")
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT N17_KODAS_DS, COUNT(*) AS cnt
FROM dbo.N17_PROD
WHERE N17_TIPAS = 2
AND N17_KODAS_DS IS NOT NULL
AND LTRIM(RTRIM(N17_KODAS_DS)) <> ''
GROUP BY N17_KODAS_DS
ORDER BY cnt DESC, N17_KODAS_DS
"""
)
rows = cursor.fetchall()
finally:
conn.close()
if not rows:
return None
return str(rows[0][0]).strip()
def create_service_statusas() -> dict:
api_key = _get_api_key()
service_ds = _choose_service_ds()
if not service_ds:
return {"status": "cancelled", "reason": "No N17_KODAS_DS selected"}
n17 = {
"N17_KODAS_PS": SERVICE_CODE,
"N17_TIPAS": "2",
"N17_PAV": SERVICE_NAME,
"N17_KODAS_US": SERVICE_UOM,
"N17_KODAS_DS": service_ds,
}
if _service_exists(api_key, SERVICE_CODE):
payload = {
"method": "EDIT_N17",
"params": {
"oper": "U",
"user": api_key.split(".", 1)[0],
"fld": "N17_KODAS_PS",
"val": SERVICE_CODE,
},
"data": {"N17": n17},
}
data = _post(api_key, payload)
return {"status": "updated", "code": SERVICE_CODE, "response": data}
payload = {
"method": "EDIT_N17",
"params": {"oper": "I"},
"data": {"N17": n17},
}
data = _post(api_key, payload)
return {"status": "created", "code": SERVICE_CODE, "response": data}
def main() -> None:
result = create_service_statusas()
print(result)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,111 @@
from __future__ import annotations
import re
from pathlib import Path
from typing import Dict, Optional
import psycopg2
import psycopg2.extras
from uv_app.core.mssql import connect_to_mssql
from uv_app.core.pgsql import connect_to_pgsql
QUERY_PATH = Path(__file__).with_name("routeriai_query.sql")
CLIENT_CODE = "101460"
def _read_query() -> str:
return QUERY_PATH.read_text(encoding="utf-8")
def _clean_text(value: object) -> Optional[str]:
if value is None:
return None
text = value.decode(errors="ignore") if isinstance(value, bytes) else str(value)
text = text.replace("\u00a0", " ")
text = re.sub(r"[\x00-\x1f\x7f\u2028\u2029\u0085]", " ", text)
return re.sub(r"\s+", " ", text).strip()
def _format_diff_value(value: str) -> str:
return (
value.replace("\r", "\\r")
.replace("\n", "\\n")
.replace("\t", "\\t")
.replace("\u2028", "\\u2028")
.replace("\u2029", "\\u2029")
.replace("\u0085", "\\u0085")
)
def _dump_string(label: str, value: object) -> None:
if value is None:
print(f"{label}: None")
return
text = value.decode(errors="ignore") if isinstance(value, bytes) else str(value)
print(f"{label} raw repr: {text!r}")
print(f"{label} raw escaped: {_format_diff_value(text)}")
cleaned = _clean_text(text)
print(f"{label} cleaned repr: {cleaned!r}")
print(f"{label} cleaned escaped: {_format_diff_value(cleaned or '')}")
codepoints = [f"U+{ord(ch):04X}" for ch in text]
print(f"{label} codepoints: {' '.join(codepoints)}")
def _fetch_pg_client() -> Optional[Dict[str, object]]:
conn = connect_to_pgsql()
if conn is None:
raise RuntimeError("Failed to connect to PostgreSQL.")
try:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
cursor.execute(_read_query())
rows = cursor.fetchall()
finally:
conn.close()
for row in rows:
if str(row.get("client_code") or "").strip() == CLIENT_CODE:
return row
return None
def _fetch_mssql_client() -> Optional[Dict[str, object]]:
conn = connect_to_mssql()
if conn is None:
raise RuntimeError("Failed to connect to MSSQL.")
try:
query = """
SELECT
N08_KODAS_KS,
N08_PAV
FROM dbo.N08_KLIJ
WHERE N08_KODAS_KS = ?
"""
cursor = conn.cursor()
cursor.execute(query, (CLIENT_CODE,))
row = cursor.fetchone()
if not row:
return None
columns = [c[0] for c in cursor.description]
return dict(zip(columns, row))
finally:
conn.close()
def main() -> None:
pg_row = _fetch_pg_client()
if not pg_row:
print("Client not found in PostgreSQL.")
else:
print("PostgreSQL data:")
_dump_string("PG name", pg_row.get("name"))
mssql_row = _fetch_mssql_client()
if not mssql_row:
print("Client not found in MSSQL.")
else:
print("MSSQL data:")
_dump_string("MSSQL N08_PAV", mssql_row.get("N08_PAV"))
if __name__ == "__main__":
main()

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import os import os
import re
from datetime import date, datetime from datetime import date, datetime
from pathlib import Path from pathlib import Path
from typing import Dict, Iterable, Optional, Tuple from typing import Dict, Iterable, Optional, Tuple
@@ -108,6 +109,19 @@ def _normalize_person_type(value: Optional[object]) -> str:
return "1" return "1"
def _clean_text(value: object) -> Optional[str]:
if value is None:
return None
if isinstance(value, bytes):
text = value.decode(errors="ignore")
else:
text = str(value)
text = text.replace("\u00a0", " ")
# Drop control chars (including C1) and unicode line/paragraph separators.
text = re.sub(r"[\x00-\x1f\x7f-\x9f\u2028\u2029]", " ", text)
return re.sub(r"\s+", " ", text).strip()
def _build_n08_payload(row: Dict[str, object]) -> Dict[str, object]: def _build_n08_payload(row: Dict[str, object]) -> Dict[str, object]:
mobile = (row.get("mobile_phone") or "").strip() mobile = (row.get("mobile_phone") or "").strip()
phone = (row.get("phone") or "").strip() phone = (row.get("phone") or "").strip()
@@ -129,9 +143,11 @@ def _build_n08_payload(row: Dict[str, object]) -> Dict[str, object]:
if isinstance(creation_date, (date, datetime)): if isinstance(creation_date, (date, datetime)):
creation_date = creation_date.isoformat() creation_date = creation_date.isoformat()
name = _clean_text(row.get("name"))
return { return {
"N08_KODAS_KS": row.get("client_code"), "N08_KODAS_KS": row.get("client_code"),
"N08_PAV": row.get("name"), "N08_PAV": name,
"N08_ADR": row.get("address"), "N08_ADR": row.get("address"),
"N08_E_MAIL": row.get("email"), "N08_E_MAIL": row.get("email"),
"N08_ADD_DATE": creation_date, "N08_ADD_DATE": creation_date,
@@ -163,6 +179,17 @@ def _normalize_value(value: object) -> str:
return str(value).strip() return str(value).strip()
def _format_diff_value(value: str) -> str:
return (
value.replace("\r", "\\r")
.replace("\n", "\\n")
.replace("\t", "\\t")
.replace("\u2028", "\\u2028")
.replace("\u2029", "\\u2029")
.replace("\u0085", "\\u0085")
)
def _diff_fields( def _diff_fields(
existing: Optional[Dict[str, object]], existing: Optional[Dict[str, object]],
desired: Dict[str, object], desired: Dict[str, object],
@@ -207,7 +234,9 @@ def _upsert_client(
return return
print(f"Updating client: {client_code} ({index}/{total})") print(f"Updating client: {client_code} ({index}/{total})")
for field, (old, new) in changes.items(): for field, (old, new) in changes.items():
print(f" {field}: '{old}' -> '{new}'") old_fmt = _format_diff_value(old)
new_fmt = _format_diff_value(new)
print(f" {field}: '{old_fmt}' -> '{new_fmt}'")
payload = { payload = {
"method": "EDIT_N08", "method": "EDIT_N08",
"params": { "params": {
@@ -221,7 +250,8 @@ def _upsert_client(
else: else:
print(f"Creating client: {client_code} ({index}/{total})") print(f"Creating client: {client_code} ({index}/{total})")
for field, (_, new) in changes.items(): for field, (_, new) in changes.items():
print(f" {field}: '' -> '{new}'") new_fmt = _format_diff_value(new)
print(f" {field}: '' -> '{new_fmt}'")
payload = { payload = {
"method": "EDIT_N08_FULL", "method": "EDIT_N08_FULL",
"params": {"oper": "I", "user": user}, "params": {"oper": "I", "user": user},

View File

@@ -1,6 +1,11 @@
SELECT SELECT
k.kodas AS client_code, k.kodas AS client_code,
trim(concat_ws(' ', k.vardas, k.pavarde)) AS name, regexp_replace(
trim(concat_ws(' ', k.vardas, k.pavarde)),
'\s+',
' ',
'g'
) AS name,
trim(concat_ws(', ', trim(concat_ws(', ',
concat_ws(' ', g.pav, k.namas, NULLIF(NULLIF(k.butas::text, '0'), '')), concat_ws(' ', g.pav, k.namas, NULLIF(NULLIF(k.butas::text, '0'), '')),
m.pav, m.pav,