Skip to content

chore: adopt ruff + mypy with full type hints in CI - #85

Merged
zeemscript merged 1 commit into
Deen-Bridge:devfrom
Fury03:feat/issue-21-ruff-mypy
Jul 30, 2026
Merged

chore: adopt ruff + mypy with full type hints in CI#85
zeemscript merged 1 commit into
Deen-Bridge:devfrom
Fury03:feat/issue-21-ruff-mypy

Conversation

@Fury03

@Fury03 Fury03 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #21

What changed

The quality gate was flake8 ... --max-line-length=120 --ignore=E501,W503 plus compileall — a length rule that disabled itself, no import sorting, no formatter, and no type checking, while CONTRIBUTING.md promised type hints on parameters and return values. This replaces that with ruff + mypy, configured in one pyproject.toml.

Tooling

  • pyproject.toml — ruff (E, F, I, UP, B, W; line length 120; venv/ and data/ excluded) and mypy (pydantic plugin, disallow_untyped_defs scoped to main.py, ignore_missing_imports scoped to google.* / stellar_sdk.* / redis.* / yaml.* rather than set globally).
  • requirements-dev.txt — pinned ruff==0.14.5, mypy==1.19.0, pytest, pytest-asyncio, so CI is reproducible.
  • CI — the flake8 and compileall steps are gone; the job now runs ruff check ., ruff format --check ., and mypy .. The per-suite pytest steps are unchanged.
  • .pre-commit-config.yaml — optional hook wiring the same tools at the same versions.
  • CONTRIBUTING.md — the "Python Style" section now lists the exact local commands (ruff check ., ruff format ., mypy .) and the requirements-dev.txt install step.

Code

  • ruff format applied repo-wide and every ruff check finding resolved.
  • main.py: parameter and return annotations on every function — the route handlers, the SSE event_generator, the retriever seams, get_safety_settings, get_model, and the rest. mypy passes with untyped defs disallowed for the module.
  • Supporting annotations needed to make mypy . pass tree-wide: @overloads on redact_secret_keys (str in → str out), an Awaitable-aware generator type on SafetyPipeline.run_async (it already accepted sync or async callables), plus small local fixes in tafsir.py, study.py, hadith.py, store.py, review_store.py, safety/input_gate.py, and scripts/build_surah_index.py.
  • raise ... from exc added where B904 flagged an unchained re-raise inside an except block.

Behaviour

No runtime behaviour change — formatting, annotations, and exception chaining only. Endpoints respond identically.

Two things deliberately not fixed here:

  • B019 (functools.cache on a method) in hadith.py — the grading sources are process-lifetime singletons so the cache is bounded, but changing the caching strategy is a behaviour change and belongs in its own PR. Left with an explanatory noqa.
  • The zip() in tafsir.py is now strict=True; the two sequences are built from each other, so lengths always match.

Testing

  • ruff check . — clean
  • ruff format --check . — 57 files already formatted
  • mypy . — success, 57 source files
  • pytest -q tests test — 820 passed, 1 skipped, 1 xfailed. The 3 failures in tests/test_adhkar.py are pre-existing on dev (verified by stashing this branch's changes) and are not run by CI.

Summary by CodeRabbit

  • Refactor
    • Modernized Python type annotations throughout the application without changing core behavior.
    • Improved error propagation and timeout handling for chat, streaming, and tafsir operations.
  • Developer Tooling
    • Added Ruff formatting/linting, Mypy type checking, pinned development tools, and optional pre-commit checks.
    • Updated contribution guidance with local validation commands.
  • Quality
    • Updated CI to validate formatting, linting, and type safety.
    • Refined and maintained automated test coverage and test setup.

Replaces the self-contradictory flake8 gate (max-line-length=120 with
E501 ignored, no import sorting, no formatter, no type checking) with
ruff and mypy, both configured in a single pyproject.toml and pinned in
requirements-dev.txt so CI is reproducible.

- ruff: E, F, I, UP, B, W at line length 120; `ruff format` applied
  across the repo (venv/ and data/ excluded)
- mypy: pydantic plugin, disallow_untyped_defs for main.py, with
  ignore_missing_imports scoped to google.*/stellar_sdk.*/redis.*/yaml.*
  rather than set globally
- main.py: return and parameter annotations on every function, including
  the route handlers and the SSE generator
- annotations elsewhere needed to make `mypy .` pass: overloads on
  redact_secret_keys, an Awaitable-aware generator type on
  SafetyPipeline.run_async, and a handful of local type fixes
- CI: flake8 and compileall steps replaced by ruff check,
  ruff format --check, and mypy
- CONTRIBUTING.md documents the local commands; optional pre-commit
  config wires up the same tools

Behaviour-preserving: formatting, annotations, and exception chaining
(`raise ... from exc`) only. The B019 lru_cache-on-method finding in
hadith.py is left in place with a note, since changing that caching
strategy is a behaviour change that belongs in its own PR.

Closes Deen-Bridge#21
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR replaces Flake8 and compile checks with Ruff formatting/linting and Mypy, adds pre-commit configuration and contributor guidance, and modernizes Python type annotations across application, storage, integration, telemetry, and test modules.

Changes

Developer tooling and CI

Layer / File(s) Summary
Ruff and Mypy quality gates
.github/workflows/ci.yml, pyproject.toml, requirements-dev.txt, .pre-commit-config.yaml
CI and pre-commit now run pinned Ruff and Mypy checks with shared project configuration.
Contributor instructions
CONTRIBUTING.md
Setup and local commands document the new linting, formatting, type-checking, and pre-commit workflow.

Application and domain typing

Layer / File(s) Summary
Modernized application contracts
main.py, citations.py, confidence.py, safety/*, verifier.py
Public models, helpers, endpoint signatures, and internal collections use built-in generics and union syntax; selected exception handling uses explicit chaining.
Domain integrations
adhkar.py, corpus.py, fiqh.py, hadith.py, nisab.py, stellar.py, study.py, tafsir.py
Domain models and helper APIs use Python 3.11-compatible annotations, with localized formatting and timeout/error-handling refinements.
Memory, review, feedback, and telemetry contracts
memory/*, feedback.py, review.py, review_store.py, semantic_cache.py, telemetry.py, store.py
Storage interfaces, review records, cache APIs, session state, and telemetry structures were retyped without broad runtime behavior changes.

Validation and tests

Layer / File(s) Summary
Test and fixture alignment
tests/*, test/test_verifier.py
Test imports, fixtures, parameterizations, payloads, and call formatting were aligned with the updated source and tooling configuration; one feedback test now verifies message snapshots before submission.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.80% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed It clearly summarizes the main change: replacing CI linting with Ruff and mypy and adding type hints.
Linked Issues check ✅ Passed The PR matches #21: Ruff/mypy are added, flake8 is removed, CI/docs/pre-commit/config are updated, and main.py is fully typed.
Out of Scope Changes check ✅ Passed No clear unrelated code changes stand out; the typing, test, and docs updates all support the CI and annotation goals.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
main.py (1)

145-158: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider bounding prompt/context length like the feedback model.

FeedbackRequest.prompt/answer/comment are explicitly length-capped with a documented rationale (preventing multi-megabyte bodies), but ChatRequest.prompt and context have no such bound. An oversized prompt still gets redacted, classified, and forwarded to the Gemini API on every request, so a large body costs real work (and tokens) before any downstream limit kicks in.

✏️ Proposed bound
-    prompt: str
+    prompt: str = Field(..., max_length=8000)
     chat_id: str | None = None
-    context: str | None = None  # Additional context for specific queries
+    context: str | None = Field(None, max_length=8000)  # Additional context for specific queries

As per path instructions, this file is a FastAPI service where CI enforces flake8/Ruff style checks and reviewers should flag "missing Pydantic validation on request bodies."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@main.py` around lines 145 - 158, Update the ChatRequest model’s prompt and
context fields with explicit Pydantic maximum-length validation, matching the
existing FeedbackRequest bounds and rationale. Preserve their optional/required
semantics while ensuring oversized request bodies are rejected before redaction,
classification, or downstream API calls.

Source: Path instructions

store.py (1)

42-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Parameterize the helper’s return type.

While touching this helper, replace the bare list return annotation with list[protos.Content] or the SDK’s concrete content type. The current annotation permits an unchecked Any-typed result under strict generic checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store.py` around lines 42 - 44, Update the dicts_to_contents return
annotation from bare list to list[protos.Content] (or the SDK’s concrete Content
type), matching the objects constructed by the helper and preserving its
existing behavior.
review_store.py (1)

118-120: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Keep Redis persistence clients type-checked instead of using Any.

Both modules now disable mypy checking for their Redis calls. Preserve the concrete client type or introduce a shared narrow protocol for the methods each store uses.

  • review_store.py#L118-L120: replace _redis: Any with the supported async Redis client type or protocol.
  • store.py#L55-L56: replace _redis: Any with the supported async Redis client type or protocol.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@review_store.py` around lines 118 - 120, Replace the Any annotation for
_redis in review_store.py lines 118-120 and store.py lines 55-56 with the
supported async Redis client type, or a shared narrow protocol covering the
Redis methods used by both stores; retain the existing _use_redis behavior and
keep Redis calls type-checked.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.pre-commit-config.yaml:
- Around line 15-24: Update the mypy hook configuration around
additional_dependencies so its isolated environment matches CI: include the
complete pinned dependency set from requirements.txt and requirements-dev.txt,
including pytest and pytest-asyncio, and pin currently unpinned entries such as
pydantic, pydantic-settings, and types-PyYAML; alternatively configure the hook
to run against the repository’s managed environment.

In `@feedback.py`:
- Around line 459-460: Update _fetch_keys to return an empty list immediately
when limit is zero or negative, before calling zrevrange; preserve the existing
Redis range query for positive limits so list_records remains consistent with
SQLite.

In `@review.py`:
- Around line 165-167: Add Pydantic validation to the verdict request model
fields corrected_answer, reviewer, and note: enforce appropriate max_length
limits and reject whitespace-only string values before persistence or
cache/export processing. Keep corrected_answer optional while validating any
provided value, and ensure the FastAPI request body uses this validated model.
- Around line 67-68: Update the token validation condition around the
X-Review-Token check to handle non-ASCII strings before calling
secrets.compare_digest, either by comparing encoded bytes or explicitly
rejecting non-ASCII input. Ensure malformed tokens consistently raise the
existing HTTPException with status 401 rather than producing an internal error.

In `@safety/pipeline.py`:
- Around line 33-37: Update run_async and its _complete_async flow to use
inspect.isawaitable for all awaitable results, including Futures and custom
awaitables. Offload synchronous generator execution with asyncio.to_thread
before handling its result, while preserving direct awaiting for async-capable
results and the existing SafetyResult behavior.

---

Nitpick comments:
In `@main.py`:
- Around line 145-158: Update the ChatRequest model’s prompt and context fields
with explicit Pydantic maximum-length validation, matching the existing
FeedbackRequest bounds and rationale. Preserve their optional/required semantics
while ensuring oversized request bodies are rejected before redaction,
classification, or downstream API calls.

In `@review_store.py`:
- Around line 118-120: Replace the Any annotation for _redis in review_store.py
lines 118-120 and store.py lines 55-56 with the supported async Redis client
type, or a shared narrow protocol covering the Redis methods used by both
stores; retain the existing _use_redis behavior and keep Redis calls
type-checked.

In `@store.py`:
- Around line 42-44: Update the dicts_to_contents return annotation from bare
list to list[protos.Content] (or the SDK’s concrete Content type), matching the
objects constructed by the helper and preserving its existing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 55a812e6-af47-4646-9a82-0bb02bde890a

📥 Commits

Reviewing files that changed from the base of the PR and between fbab6cf and 350ee37.

📒 Files selected for processing (57)
  • .github/workflows/ci.yml
  • .pre-commit-config.yaml
  • CONTRIBUTING.md
  • adhkar.py
  • citations.py
  • confidence.py
  • corpus.py
  • feedback.py
  • fiqh.py
  • hadith.py
  • main.py
  • memory/__init__.py
  • memory/extraction.py
  • memory/models.py
  • memory/store.py
  • nisab.py
  • pyproject.toml
  • requirements-dev.txt
  • review.py
  • review_store.py
  • safety/input_gate.py
  • safety/output_check.py
  • safety/pipeline.py
  • safety/policy.py
  • scripts/build_surah_index.py
  • scripts/eval_citations.py
  • scripts/export_eval_candidates.py
  • semantic_cache.py
  • stellar.py
  • store.py
  • study.py
  • tafsir.py
  • telemetry.py
  • test/test_verifier.py
  • tests/conftest.py
  • tests/redteam/test_live.py
  • tests/redteam/test_pipeline.py
  • tests/test_adhkar.py
  • tests/test_chat_robustness.py
  • tests/test_citations.py
  • tests/test_citations_integration.py
  • tests/test_confidence.py
  • tests/test_feedback.py
  • tests/test_fiqh.py
  • tests/test_hadith.py
  • tests/test_memory_extraction.py
  • tests/test_memory_integration.py
  • tests/test_memory_profile.py
  • tests/test_multilingual.py
  • tests/test_purchases.py
  • tests/test_review_queue.py
  • tests/test_semantic_cache.py
  • tests/test_session_persistence.py
  • tests/test_study.py
  • tests/test_tafsir.py
  • tests/test_zakat.py
  • verifier.py
💤 Files with no reviewable changes (4)
  • tests/test_adhkar.py
  • tests/conftest.py
  • tests/test_memory_extraction.py
  • tests/test_memory_profile.py

Comment thread .pre-commit-config.yaml
Comment on lines +15 to +24
# mypy needs the app's own dependencies to resolve imports the way CI does.
additional_dependencies:
- fastapi==0.115.12
- pydantic>=2.5.3
- pydantic-settings>=2.0.0
- httpx==0.28.1
- numpy==2.2.4
- types-PyYAML
pass_filenames: false
args: ["."]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make the pre-commit mypy environment match CI.

This hook runs mypy . in an isolated environment, but it does not install requirements.txt or requirements-dev.txt; notably, pytest and pytest-asyncio are absent even though the checked tree contains pytest-based tests. It also leaves pydantic, pydantic-settings, and types-PyYAML unpinned, so local results can drift from CI. Add the complete pinned dependency set or run the hook against the repository’s managed environment.

#!/bin/bash
rg -n '^\s*(from|import)\s+(pytest|pytest_asyncio)\b' tests
rg -n 'additional_dependencies|requirements-(dev|txt)' .pre-commit-config.yaml .github/workflows/ci.yml
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.pre-commit-config.yaml around lines 15 - 24, Update the mypy hook
configuration around additional_dependencies so its isolated environment matches
CI: include the complete pinned dependency set from requirements.txt and
requirements-dev.txt, including pytest and pytest-asyncio, and pin currently
unpinned entries such as pydantic, pydantic-settings, and types-PyYAML;
alternatively configure the hook to run against the repository’s managed
environment.

Comment thread feedback.py
Comment on lines +459 to 460
def _fetch_keys(self, index_key: str, limit: int) -> list[str]:
return self._r.zrevrange(index_key, 0, limit - 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return no records when limit is zero or negative.

With limit=0, limit - 1 becomes -1, which Redis interprets as the end of the sorted set. list_records(limit=0) can therefore load every record for rating/category queries instead of returning an empty list, unlike SQLite.

Proposed fix
 def _fetch_keys(self, index_key: str, limit: int) -> list[str]:
+    if limit <= 0:
+        return []
     return self._r.zrevrange(index_key, 0, limit - 1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _fetch_keys(self, index_key: str, limit: int) -> list[str]:
return self._r.zrevrange(index_key, 0, limit - 1)
def _fetch_keys(self, index_key: str, limit: int) -> list[str]:
if limit <= 0:
return []
return self._r.zrevrange(index_key, 0, limit - 1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@feedback.py` around lines 459 - 460, Update _fetch_keys to return an empty
list immediately when limit is zero or negative, before calling zrevrange;
preserve the existing Redis range query for positive limits so list_records
remains consistent with SQLite.

Comment thread review.py
Comment on lines 67 to +68
if not token or not secrets.compare_digest(token, SCHOLAR_REVIEW_TOKEN):
raise HTTPException(
status_code=401, detail="A valid X-Review-Token header is required."
)
raise HTTPException(status_code=401, detail="A valid X-Review-Token header is required.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect review.py with line numbers.
file="$(git ls-files | rg '(^|/)review\.py$' | head -n 1)"
printf 'FILE=%s\n' "$file"
wc -l "$file"
cat -n "$file" | sed -n '1,180p'

printf '\n--- compare_digest probe ---\n'
python3 - <<'PY'
import secrets

cases = [
    ("ascii", "abc", "abc"),
    ("ascii-mismatch", "abc", "abd"),
    ("non-ascii-left", "åbc", "abc"),
    ("non-ascii-right", "abc", "åbc"),
    ("bytes-ascii", b"abc", b"abc"),
    ("bytes-non-ascii", "åbc".encode("utf-8"), "abc".encode("utf-8")),
]

for name, a, b in cases:
    try:
        result = secrets.compare_digest(a, b)
        print(name, "=>", type(result).__name__, result)
    except Exception as e:
        print(name, "=>", type(e).__name__, str(e))
PY

Repository: Deen-Bridge/dnb-ai

Length of output: 1922


🌐 Web query:

Python secrets.compare_digest non-ASCII str TypeError documentation header values latin-1 decoding ASGI

💡 Result:

The Python secrets.compare_digest function is designed for constant-time comparisons to prevent timing attacks [1][2]. It explicitly prohibits comparing strings that contain non-ASCII characters, raising a TypeError if such input is provided [3][4][5]. This restriction exists because constant-time guarantees for Unicode strings are difficult to maintain across different internal string representations [6]. In the context of ASGI applications, header values are provided as raw byte strings [7][8]. While modern HTTP standards suggest headers should be ASCII, historical and practical usage allows a wider range of characters [7][8]. Because these headers are often byte strings, developers attempting to decode them into Unicode (e.g., using UTF-8) before passing them to secrets.compare_digest may trigger a TypeError if the decoded string contains non-ASCII characters [3][4]. To resolve this issue, you should ensure that the values being compared are converted to consistent byte sequences before comparison [4][6]. Encoding the strings to bytes—typically using UTF-8—is the standard approach [9][4]: # Correct approach: encode to bytes before comparing safe_compare = secrets.compare_digest( candidate_str.encode('utf-8'), stored_str.encode('utf-8')) Additionally, note that ASGI headers often follow ISO-8859-1 (latin-1) when decoded, rather than UTF-8 [7][10]. If your application logic requires decoding ASGI header bytes to strings, be aware that many frameworks and utilities (like those for instrumentation) have moved toward using latin-1 or providing fallback mechanisms to avoid crashes when encountering non-ASCII byte sequences [10][11][12].

Citations:


Handle non-ASCII review tokens before calling compare_digest. secrets.compare_digest() rejects non-ASCII str, so a malformed X-Review-Token can turn this auth path into a 500 instead of the intended 401. Compare bytes, or validate/reject non-ASCII input first.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@review.py` around lines 67 - 68, Update the token validation condition around
the X-Review-Token check to handle non-ASCII strings before calling
secrets.compare_digest, either by comparing encoded bytes or explicitly
rejecting non-ASCII input. Ensure malformed tokens consistently raise the
existing HTTPException with status 401 rather than producing an internal error.

Comment thread review.py
Comment on lines +165 to +167
corrected_answer: str | None = Field(None, description="Required when the verdict is 'correct'")
reviewer: str | None = Field(None, description="Reviewer's name or identifier")
note: str | None = Field(None, description="Optional note for the record")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add bounds to verdict request fields.

corrected_answer, reviewer, and note accept unbounded strings before being persisted and sent through cache/export processing. A valid reviewer token can submit oversized values and amplify memory, storage, and downstream work. Add appropriate max_length constraints and reject invalid whitespace-only values.

As per path instructions, FastAPI request bodies must include Pydantic validation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@review.py` around lines 165 - 167, Add Pydantic validation to the verdict
request model fields corrected_answer, reviewer, and note: enforce appropriate
max_length limits and reject whitespace-only string values before persistence or
cache/export processing. Keep corrected_answer optional while validating any
provided value, and ensure the FastAPI request body uses this validated model.

Source: Path instructions

Comment thread safety/pipeline.py
Comment on lines +33 to +37
async def run_async(self, prompt: str, generator: Callable[[str], str | Awaitable[str]]) -> SafetyResult:
"""Run the pipeline while keeping classification off the event loop.

``generator`` may be sync or async; the async path is awaited.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make the async generator contract safe for both accepted forms.

This signature permits any Awaitable[str] and synchronous generators, but _complete_async() only checks inspect.iscoroutine(res) and invokes synchronous generators directly on the event-loop thread. A Future or custom awaitable can be passed to enforce() unawaited, while blocking synchronous network work can stall all requests. Use inspect.isawaitable() and offload the synchronous branch with asyncio.to_thread, or narrow the contract to async-only.

As per path instructions, blocking calls in this FastAPI service’s async flow must be awaited or offloaded.

# Add asyncio to the imports, then offload synchronous generators.
res = await asyncio.to_thread(generator, generation_prompt)
generated = await res if inspect.isawaitable(res) else res
#!/bin/bash
python - <<'PY'
import inspect

class CustomAwaitable:
    def __await__(self):
        async def done():
            return "ok"
        return done().__await__()

assert inspect.isawaitable(CustomAwaitable())
assert not inspect.iscoroutine(CustomAwaitable())
PY
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@safety/pipeline.py` around lines 33 - 37, Update run_async and its
_complete_async flow to use inspect.isawaitable for all awaitable results,
including Futures and custom awaitables. Offload synchronous generator execution
with asyncio.to_thread before handling its result, while preserving direct
awaiting for async-capable results and the existing SafetyResult behavior.

@zeemscript
zeemscript merged commit 3113444 into Deen-Bridge:dev Jul 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants