diff --git a/pyproject.toml b/pyproject.toml index 2d23dcd..e915fa3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -128,4 +128,29 @@ select = ["E", "F", "I", "N", "UP", "B", "SIM"] pythonVersion = "3.13" typeCheckingMode = "basic" include = ["src/flydesk_idp"] -exclude = ["tests/**", "**/__pycache__/**"] +exclude = ["tests/**", "**/__pycache__/**", "vendor/**"] + +# Two execution-environment overrides silence false positives that come +# from framework-level metaprogramming that pyright can't follow: +# +# * ``web/controllers``: pyfly's ``Body[T]`` / ``Valid[T]`` / ``Header[T]`` / +# ``PathVar[T]`` are runtime ``Generic[T]`` classes the framework +# strips at injection time. Statically they look like opaque wrappers +# around T, so pyright rejects every call site that passes the +# unwrapped value through. Suppress ``reportArgumentType`` until +# pyfly publishes proper type stubs (or switches the markers to +# ``Annotated[T, …]``). +# * ``core/services/extraction/postprocess``: pydantic v2's +# ``populate_by_name=True`` lets us construct DTOs with either the +# Python field name (``fieldName=``) or the alias (``name=``). +# Pyright tracks the alias only, so it flags every construction +# that uses the Python name. The runtime accepts both; suppress +# ``reportCallIssue`` here. + +[[tool.pyright.executionEnvironments]] +root = "src/flydesk_idp/web/controllers" +reportArgumentType = "none" + +[[tool.pyright.executionEnvironments]] +root = "src/flydesk_idp/core/services/extraction" +reportCallIssue = "none" diff --git a/src/flydesk_idp/core/observability/outbound_log.py b/src/flydesk_idp/core/observability/outbound_log.py index 9d1ac9d..7ebbb5a 100644 --- a/src/flydesk_idp/core/observability/outbound_log.py +++ b/src/flydesk_idp/core/observability/outbound_log.py @@ -179,7 +179,14 @@ def _extract_usage_fields(result: Any, model: str) -> dict[str, Any]: if cache_read: fields["cache_read"] = cache_read try: - from fireflyframework_agentic.observability.cost import get_cost_calculator + # ImportError-tolerant: the cost resolver is an optional surface + # in fireflyframework-agentic. Older / unreleased refs export it + # under a different module path; we keep the call lazy and + # swallow every failure mode so missing pricing data degrades + # to a zero-cost log line instead of a hard error. + from fireflyframework_agentic.observability.cost import ( # pyright: ignore[reportMissingImports] + get_cost_calculator, # pyright: ignore[reportAttributeAccessIssue] + ) calc = get_cost_calculator("auto") cost = calc.estimate(model, input_tokens, output_tokens) diff --git a/src/flydesk_idp/core/services/rules/rule_engine.py b/src/flydesk_idp/core/services/rules/rule_engine.py index 6ef7fc8..06d7af6 100644 --- a/src/flydesk_idp/core/services/rules/rule_engine.py +++ b/src/flydesk_idp/core/services/rules/rule_engine.py @@ -240,9 +240,14 @@ def _serialise_field_value(field: ExtractedField) -> Any: return [ { "rowName": row.fieldName, + # Nested ``fieldValueFound`` only carries a row of + # sub-fields for array-typed parents; for scalar leaves + # it is the value itself, which we skip here. The + # isinstance guard also satisfies pyright -- without it + # the iterator type is the full union (str / int / …). "row": [ {"fieldName": sub.fieldName, "value": sub.fieldValueFound} - for sub in row.fieldValueFound + for sub in (row.fieldValueFound if isinstance(row.fieldValueFound, list) else []) if isinstance(sub, ExtractedField) ], } diff --git a/src/flydesk_idp/main.py b/src/flydesk_idp/main.py index d8625b3..5bbe923 100644 --- a/src/flydesk_idp/main.py +++ b/src/flydesk_idp/main.py @@ -45,10 +45,21 @@ async def _lifespan(app: Any): bodies, response models) instead of the lazy-resolution shim that pyfly registers with FastAPI. """ - _pyfly._route_metadata = getattr(app.state, "pyfly_route_metadata", []) - _pyfly._docs_enabled = getattr(app.state, "pyfly_docs_enabled", False) - _pyfly._host = str(_pyfly.config.get("pyfly.web.host", "0.0.0.0")) - _pyfly._port = int(_pyfly.config.get("pyfly.server.port", 8400)) + # PyFlyApplication exposes route metadata + host/port via private + # attributes that the framework's own lifespan flow writes to; we + # mirror that here so pyfly's structured-logging "server_started" + # event reports the right host/port. ``setattr`` keeps pyright + # quiet about writes to attrs that aren't declared on the class -- + # the framework reads them via ``getattr(..., default)`` so missing + # attrs are always safe. + # Ruff prefers direct assignment (B010); pyright rejects it because + # the framework class doesn't declare these attrs. We suppress the + # ruff rule on each line and keep ``setattr`` -- the standard + # escape hatch for legitimate dynamic attribute writes. + setattr(_pyfly, "_route_metadata", getattr(app.state, "pyfly_route_metadata", [])) # noqa: B010 + setattr(_pyfly, "_docs_enabled", getattr(app.state, "pyfly_docs_enabled", False)) # noqa: B010 + setattr(_pyfly, "_host", str(_pyfly.config.get("pyfly.web.host", "0.0.0.0"))) # noqa: B010 + setattr(_pyfly, "_port", int(_pyfly.config.get("pyfly.server.port", 8400))) # noqa: B010 await _pyfly.startup() # Re-scan HealthIndicator beans now that the container has built # every singleton (the eager scan inside ``create_app`` runs BEFORE