"""Async port of the packing list reservation stored procedure.""" from __future__ import annotations import json import logging import sys from dataclasses import dataclass from functools import wraps from pathlib import Path from typing import Any, Dict, List, Optional try: from loguru import logger except Exception: # pragma: no cover - safety fallback if dependency is missing locally class _FallbackLogger: """Minimal adapter used only when Loguru is not installed yet.""" def __init__(self): self._logger = logging.getLogger(MODULE_LOG_NAME if "MODULE_LOG_NAME" in globals() else __name__) self._logger.setLevel(logging.DEBUG) self._logger.propagate = False def bind(self, **_kwargs): return self def add(self, sink, level="INFO", format=None, encoding="utf-8", **_kwargs): handler: logging.Handler if hasattr(sink, "write"): handler = logging.StreamHandler(sink) else: handler = logging.FileHandler(str(sink), encoding=encoding) handler.setLevel(getattr(logging, str(level).upper(), logging.INFO)) handler.setFormatter( logging.Formatter("%(asctime)s | %(levelname)-8s | %(name)s | %(message)s") ) self._logger.addHandler(handler) return 0 def log(self, level, message): getattr(self._logger, str(level).lower(), self._logger.info)(message) def debug(self, message): self._logger.debug(message) def info(self, message): self._logger.info(message) def exception(self, message): self._logger.exception(message) logger = _FallbackLogger() PACKINGLIST_SP_LOG_MODE = "INFO" # "OFF" | "INFO" | "DEBUG" MODULE_LOG_NAME = Path(__file__).stem MODULE_LOG_PATH = Path(__file__).with_suffix(".log") _MODULE_LOG_ENABLED = PACKINGLIST_SP_LOG_MODE.upper() != "OFF" _MODULE_LOG_LEVEL = "DEBUG" if PACKINGLIST_SP_LOG_MODE.upper() == "DEBUG" else "INFO" _MODULE_LOGGER = logger.bind(warehouse_module=MODULE_LOG_NAME) _MODULE_LOGGING_CONFIGURED = False def _configure_module_logger(): """Configure console and file logging for this module.""" global _MODULE_LOGGING_CONFIGURED if _MODULE_LOGGING_CONFIGURED: return if not _MODULE_LOG_ENABLED: _MODULE_LOGGING_CONFIGURED = True return record_filter = lambda record: record["extra"].get("warehouse_module") == MODULE_LOG_NAME logger.add( sys.stderr, level=_MODULE_LOG_LEVEL, colorize=True, filter=record_filter, format=( "{time:YYYY-MM-DD HH:mm:ss.SSS} | " "{level: <8} | " "" + MODULE_LOG_NAME + " | " "{message}" ), ) logger.add( MODULE_LOG_PATH, level=_MODULE_LOG_LEVEL, colorize=False, encoding="utf-8", filter=record_filter, format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " + MODULE_LOG_NAME + " | {message}", ) _MODULE_LOGGING_CONFIGURED = True def _format_payload(payload: Any) -> str: """Serialize payloads for human-readable logging.""" try: return json.dumps(payload, ensure_ascii=False, indent=2, default=str) except Exception: return repr(payload) def _log_call(level: Optional[str] = None): """Trace entry, exit and failure of selected procedure helpers.""" def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): effective_level = level or _MODULE_LOG_LEVEL _MODULE_LOGGER.log( effective_level, f"CALL {func.__qualname__} args={_format_payload(args[1:] if len(args) > 1 else ())} kwargs={_format_payload(kwargs)}", ) try: result = await func(*args, **kwargs) except Exception: _MODULE_LOGGER.exception(f"FAIL {func.__qualname__}") raise _MODULE_LOGGER.log(effective_level, f"RETURN {func.__qualname__}") return result return wrapper return decorator def _log_sql(query_name: str, sql: str, params: Dict[str, Any]): """Log one SQL statement and its parameters.""" _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} params={_format_payload(params)}") _MODULE_LOGGER.debug(f"SQL {query_name} text:\n{sql.strip()}") def _log_dataset(query_name: str, rows: Any): """Log query results at summary or full-debug level depending on the mode.""" if isinstance(rows, list): _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} returned {len(rows)} rows") if PACKINGLIST_SP_LOG_MODE.upper() == "DEBUG": _MODULE_LOGGER.debug(f"SQL {query_name} dataset:\n{_format_payload(rows)}") else: _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"SQL {query_name} scalar={_format_payload(rows)}") _configure_module_logger() if _MODULE_LOG_ENABLED: _MODULE_LOGGER.info( f"Logging inizializzato su {MODULE_LOG_PATH.name} livello={_MODULE_LOG_LEVEL} mode={PACKINGLIST_SP_LOG_MODE.upper()}" ) @dataclass class SPResult: """Container returned by the async stored-procedure port.""" rc: int = 0 message: Optional[str] = "" id_result: Optional[int] = None @_log_call("DEBUG") async def _query_one_value(db, sql: str, params: Dict[str, Any]) -> Optional[Any]: """Return the first column of the first row from a query result.""" _log_sql("_query_one_value", sql, params) if hasattr(db, "query_json"): res = await db.query_json(sql, params) if isinstance(res, list) and res: row0 = res[0] if isinstance(row0, dict): value = next(iter(row0.values()), None) _log_dataset("_query_one_value", value) return value elif isinstance(res, dict): rows = None for key in ("rows", "data", "result", "records"): if key in res and isinstance(res[key], list): rows = res[key] break if rows: row0 = rows[0] if isinstance(row0, dict): value = next(iter(row0.values()), None) _log_dataset("_query_one_value", value) return value if isinstance(row0, (list, tuple)) and row0: value = row0[0] _log_dataset("_query_one_value", value) return value _log_dataset("_query_one_value", None) return None if hasattr(db, "query_value"): value = await db.query_value(sql, params) _log_dataset("_query_one_value", value) return value if hasattr(db, "scalar"): value = await db.scalar(sql, params) _log_dataset("_query_one_value", value) return value raise RuntimeError("Il client DB non espone query_json/query_value/scalar") @_log_call("DEBUG") async def _query_all(db, sql: str, params: Dict[str, Any]) -> List[Dict[str, Any]]: """Return all rows as dictionaries, normalizing different DB client APIs.""" _log_sql("_query_all", sql, params) if hasattr(db, "query_json"): res = await db.query_json(sql, params) if res is None: _log_dataset("_query_all", []) return [] if isinstance(res, list): rows = res if res and isinstance(res[0], dict) else [] _log_dataset("_query_all", rows) return rows if isinstance(res, dict): for key in ("rows", "data", "result", "records"): if key in res and isinstance(res[key], list): rows = res[key] if rows and isinstance(rows[0], dict): _log_dataset("_query_all", rows) return rows cols = res.get("columns") or res.get("cols") or [] out = [] for row in rows: if isinstance(row, (list, tuple)) and cols: out.append({(cols[i] if i < len(cols) else f"c{i}"): row[i] for i in range(min(len(cols), len(row)))}) _log_dataset("_query_all", out) return out _log_dataset("_query_all", []) return [] if hasattr(db, "fetch_all"): rows = await db.fetch_all(sql, params) _log_dataset("_query_all", rows) return rows raise RuntimeError("Il client DB non espone query_json/fetch_all") @_log_call("DEBUG") async def _execute(db, sql: str, params: Dict[str, Any]) -> int: """Execute a DML statement using the best method exposed by the DB client.""" _log_sql("_execute", sql, params) for name in ("execute", "exec", "execute_non_query"): if hasattr(db, name): rc = await getattr(db, name)(sql, params) if isinstance(rc, int): _log_dataset("_execute", rc) return rc _log_dataset("_execute", 0) return 0 if hasattr(db, "query_json"): await db.query_json(sql, params) _log_dataset("_execute", 0) return 0 raise RuntimeError("Il client DB non espone metodi di esecuzione DML noti") @_log_call() async def sp_xExePackingListPallet_async(db, IDOperatore: int, Documento: str) -> SPResult: """Toggle the reservation state of all cells belonging to a packing list. The implementation mirrors the original SQL stored procedure while using the shared async DB client already managed by the application. """ try: _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Procedura async packing list avviata documento={Documento} id_operatore={IDOperatore}") nominativo = await _query_one_value( db, "SELECT LOGIN FROM Operatori WHERE id = :IDOperatore", {"IDOperatore": IDOperatore}, ) or "" celle = await _query_all( db, """ SELECT DISTINCT Cella FROM dbo.XMag_ViewPackingList WHERE Documento = :Documento """, {"Documento": Documento}, ) id_celle = [row.get("Cella") for row in celle if "Cella" in row] _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Celle coinvolte per documento={Documento}: {len(id_celle)}") # Each cell is toggled individually because the original procedure also # updates metadata such as operator and timestamp per row. for id_cella in id_celle: if id_cella is None: continue stato = await _query_one_value( db, "SELECT IDStato FROM Celle WHERE ID = :IDC", {"IDC": id_cella}, ) _MODULE_LOGGER.debug(f"Toggling cella id={id_cella} stato_corrente={stato}") if stato == 0: await _execute( db, """ UPDATE Celle SET IDStato = 1, ModUtente = :N, ModDataOra = GETDATE() WHERE ID = :IDC """, {"N": nominativo, "IDC": id_cella}, ) else: await _execute( db, """ UPDATE Celle SET IDStato = 0, ModUtente = :N, ModDataOra = GETDATE() WHERE ID = :IDC """, {"N": nominativo, "IDC": id_cella}, ) description = await _query_one_value( db, """ SELECT TOP 1 NAZIONE FROM dbo.XMag_ViewPackingList WHERE Documento = :Documento GROUP BY Documento, NAZIONE ORDER BY NAZIONE """, {"Documento": Documento}, ) await _execute( db, """ INSERT INTO dbo.LogPackingList (Code, Description, IDInsUser, InsDateTime) VALUES (:Code, :Descr, :IDInsUser, GETDATE()); """, {"Code": Documento, "Descr": description, "IDInsUser": IDOperatore}, ) new_id = await _query_one_value(db, "SELECT SCOPE_IDENTITY() AS ID", {}) _MODULE_LOGGER.log(_MODULE_LOG_LEVEL, f"Procedura completata documento={Documento} id_result={new_id}") return SPResult(rc=0, message="", id_result=int(new_id) if new_id is not None else None) except Exception as exc: _MODULE_LOGGER.exception(f"Procedura fallita documento={Documento}: {exc}") return SPResult(rc=-1, message=str(exc), id_result=None)