Skip to content

Commit a6b1df9

Browse files
committed
merge: resolve middleware.py docstring conflict from origin/main
Take main's phrasing (#16 added a CI workflow; the conflict is only in the top-of-file docstring line-length fix — both sides shortened the same overlong docstring differently). https://claude.ai/code/session_01KLjCnJoNbCbk8is6pVZcom
2 parents d5e4972 + 39a1f91 commit a6b1df9

10 files changed

Lines changed: 141 additions & 24 deletions

File tree

‎.github/workflows/pr.yml‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
name: PR
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
7+
# Cancel in-progress runs when new commits are pushed to the same PR
8+
# so we never waste minutes on outdated code.
9+
concurrency:
10+
group: pr-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
16+
env:
17+
NODE_VERSION: "24"
18+
# Hash all workspace pyproject.toml files so cache invalidates when any
19+
# member's deps change. uv.lock is gitignored, so this is the best key
20+
# available; commit uv.lock to get true lockfile-based caching.
21+
UV_CACHE_GLOB: |
22+
pyproject.toml
23+
framework/*/pyproject.toml
24+
modules/*/pyproject.toml
25+
host/pyproject.toml
26+
27+
jobs:
28+
python-lint:
29+
name: Python lint & format
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v6
33+
- uses: astral-sh/setup-uv@v8.0.0
34+
with:
35+
enable-cache: true
36+
cache-dependency-glob: ${{ env.UV_CACHE_GLOB }}
37+
- run: make install-py
38+
- run: make ci-python-lint
39+
40+
python-typecheck:
41+
name: Python typecheck
42+
runs-on: ubuntu-latest
43+
steps:
44+
- uses: actions/checkout@v6
45+
- uses: astral-sh/setup-uv@v8.0.0
46+
with:
47+
enable-cache: true
48+
cache-dependency-glob: ${{ env.UV_CACHE_GLOB }}
49+
- run: make install-py
50+
- run: make ci-python-typecheck
51+
52+
python-tests:
53+
name: Python tests
54+
runs-on: ubuntu-latest
55+
steps:
56+
- uses: actions/checkout@v6
57+
- uses: astral-sh/setup-uv@v8.0.0
58+
with:
59+
enable-cache: true
60+
cache-dependency-glob: ${{ env.UV_CACHE_GLOB }}
61+
- run: make install-py
62+
- run: make test
63+
64+
js-lint:
65+
name: JS lint & format
66+
runs-on: ubuntu-latest
67+
steps:
68+
- uses: actions/checkout@v6
69+
- uses: actions/setup-node@v6
70+
with:
71+
node-version: ${{ env.NODE_VERSION }}
72+
cache: "npm"
73+
- run: make install-js
74+
- run: make ci-js-lint
75+
76+
js-typecheck:
77+
name: JS typecheck
78+
runs-on: ubuntu-latest
79+
steps:
80+
- uses: actions/checkout@v6
81+
- uses: actions/setup-node@v6
82+
with:
83+
node-version: ${{ env.NODE_VERSION }}
84+
cache: "npm"
85+
- run: make install-js
86+
- run: make ci-js-typecheck
87+
88+
# Single required status check for branch protection.
89+
# Protect `main` with this one check and every leaf job is required transitively.
90+
pr-checks:
91+
name: PR checks
92+
runs-on: ubuntu-latest
93+
needs:
94+
- python-lint
95+
- python-typecheck
96+
- python-tests
97+
- js-lint
98+
- js-typecheck
99+
if: always()
100+
steps:
101+
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
102+
run: exit 1

‎Makefile‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
.PHONY: install dev dev-api dev-ui build test lint doctor migrate migration downgrade migration-history docker-up docker-down kill new-module
1+
.PHONY: install install-py install-js dev dev-api dev-ui build test lint doctor migrate migration downgrade migration-history docker-up docker-down kill new-module ci-python-lint ci-python-typecheck ci-js-lint ci-js-typecheck
22

33
# Install
44
install:
55
uv sync --all-packages
66
npm install
77

8+
# Install (granular — used by CI so Python jobs don't pull npm and vice versa)
9+
install-py:
10+
uv sync --all-packages
11+
12+
install-js:
13+
npm ci
14+
815
# Development
916
dev: docker-up
1017
@echo "Starting API and UI dev servers..."
@@ -24,11 +31,20 @@ build:
2431
test:
2532
uv run pytest
2633

27-
lint:
34+
lint: ci-python-lint ci-python-typecheck ci-js-lint ci-js-typecheck
35+
36+
# Kept granular so pr.yml can run them in parallel.
37+
ci-python-lint:
2838
uv run ruff format --check .
2939
uv run ruff check .
40+
41+
ci-python-typecheck:
3042
uv run ty check
31-
npx biome check .
43+
44+
ci-js-lint:
45+
npx biome ci .
46+
47+
ci-js-typecheck:
3248
npx tsc --noEmit -p host/client_app/tsconfig.json
3349

3450
# Diagnostics

‎framework/db/simple_module_db/listeners.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ def _entity_label(obj: object) -> str:
3535
def _entity_pk(obj: object) -> object:
3636
"""Return the primary key value(s) if available, else None."""
3737
try:
38-
identity = sa_inspect(obj).identity
38+
inspector = sa_inspect(obj)
39+
if inspector is None:
40+
return None
41+
identity = inspector.identity
3942
if identity and len(identity) == 1:
4043
return identity[0]
4144
return identity

‎framework/db/tests/test_db.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import AsyncGenerator
6+
57
import pytest
68
from simple_module_db.base import create_module_base
79
from simple_module_db.listeners import TenantIsolationError, current_tenant_id, register_listeners
@@ -16,13 +18,13 @@
1618
_TenantBase = create_module_base("mt_test", provider=DatabaseProvider.SQLITE)
1719

1820

19-
class _TenantItem(_TenantBase, MultiTenantMixin): # type: ignore[misc]
21+
class _TenantItem(_TenantBase, MultiTenantMixin): # ty: ignore[unsupported-base]
2022
__tablename__ = "mt_test_item"
2123
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
2224
name: Mapped[str] = mapped_column(String(100))
2325

2426

25-
class _TenantSoftItem(_TenantBase, MultiTenantMixin, SoftDeleteMixin): # type: ignore[misc]
27+
class _TenantSoftItem(_TenantBase, MultiTenantMixin, SoftDeleteMixin): # ty: ignore[unsupported-base]
2628
"""Combines multi-tenant and soft-delete mixins to test filter composition."""
2729

2830
__tablename__ = "mt_test_soft_item"
@@ -31,15 +33,15 @@ class _TenantSoftItem(_TenantBase, MultiTenantMixin, SoftDeleteMixin): # type:
3133

3234

3335
@pytest.fixture
34-
async def tenant_session() -> AsyncSession:
36+
async def tenant_session() -> AsyncGenerator[AsyncSession, None]:
3537
"""Session backed by in-memory SQLite with tenant listeners registered."""
3638
db_state = init_db("sqlite+aiosqlite:///:memory:")
3739
try:
3840
register_listeners(db_state)
3941
async with db_state.engine.begin() as conn:
4042
await conn.run_sync(_TenantBase.metadata.create_all)
4143
async with db_state.session_factory() as session:
42-
yield session # type: ignore[misc]
44+
yield session
4345
finally:
4446
await db_state.engine.dispose()
4547

‎framework/hosting/simple_module_hosting/app_builder.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
210210
)
211211
app.add_exception_handler(HTTPException, _http_exception_handler) # ty: ignore[invalid-argument-type]
212212
app.add_exception_handler(NotFoundError, _not_found_error_handler) # ty: ignore[invalid-argument-type]
213-
app.add_exception_handler(Exception, _unhandled_exception_handler) # ty: ignore[invalid-argument-type]
213+
app.add_exception_handler(Exception, _unhandled_exception_handler)
214214
for mod in modules:
215215
mod.register_exception_handlers(app)
216216

‎framework/hosting/simple_module_hosting/middleware.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Middleware: security headers, tenants, correlation IDs, request logging, and Inertia layout.
1+
"""Middleware: security headers, tenant isolation, correlation IDs, request logging, layout data.
22
33
All middleware classes use the raw ASGI pattern instead of ``BaseHTTPMiddleware``
44
to avoid its known issues with streaming responses, extra task creation,

‎framework/hosting/simple_module_hosting/permissions.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __call__(self, request: Request) -> None:
5656
raise HTTPException(status_code=401, detail="Authentication required")
5757

5858
# Use cached permissions from middleware if available
59-
permissions: set[str] = getattr(request.state, "resolved_permissions", None) # type: ignore[assignment]
59+
permissions: set[str] | None = getattr(request.state, "resolved_permissions", None)
6060
if permissions is None:
6161
permissions = resolve_permissions(user.roles)
6262
request.state.resolved_permissions = permissions

‎framework/hosting/tests/test_logging.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def test_injects_empty_string_by_default(self):
111111
name="x", level=logging.INFO, pathname="", lineno=0, msg="", args=None, exc_info=None
112112
)
113113
filt.filter(record)
114-
assert record.correlation_id == "" # type: ignore[attr-defined]
114+
assert record.correlation_id == "" # ty: ignore[unresolved-attribute]
115115

116116
def test_injects_current_correlation_id(self):
117117
token = correlation_id.set("req-42")
@@ -127,7 +127,7 @@ def test_injects_current_correlation_id(self):
127127
exc_info=None,
128128
)
129129
filt.filter(record)
130-
assert record.correlation_id == "req-42" # type: ignore[attr-defined]
130+
assert record.correlation_id == "req-42" # ty: ignore[unresolved-attribute]
131131
finally:
132132
correlation_id.reset(token)
133133

@@ -219,7 +219,7 @@ async def test_log_records_have_extra_fields(self, client: httpx.AsyncClient, ca
219219
]
220220
assert len(completed) == 1
221221
record = completed[0]
222-
assert record.method == "GET" # type: ignore[attr-defined]
223-
assert record.path == "/dashboard" # type: ignore[attr-defined]
222+
assert record.method == "GET" # ty: ignore[unresolved-attribute]
223+
assert record.path == "/dashboard" # ty: ignore[unresolved-attribute]
224224
assert hasattr(record, "status_code")
225225
assert hasattr(record, "duration_ms")

‎tests/integration/test_inertia_views.py‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,7 @@ async def test_valid_submission_redirects_to_listing(
104104
listing = await authenticated_client.get("/api/products/")
105105
assert [p["name"] for p in listing.json()] == ["Formed"]
106106

107-
async def test_invalid_submission_redirects_back(
108-
self, authenticated_client: httpx.AsyncClient
109-
):
107+
async def test_invalid_submission_redirects_back(self, authenticated_client: httpx.AsyncClient):
110108
resp = await authenticated_client.post(
111109
"/products/",
112110
json={"name": "", "price": "0"},
@@ -146,9 +144,7 @@ async def test_delete_redirects_and_removes_product(
146144
):
147145
product_id = await create_product(name="Doomed")
148146

149-
resp = await authenticated_client.delete(
150-
f"/products/{product_id}", follow_redirects=False
151-
)
147+
resp = await authenticated_client.delete(f"/products/{product_id}", follow_redirects=False)
152148
assert resp.status_code == 303
153149
assert resp.headers["location"] == "/products"
154150

‎tests/integration/test_products_journey.py‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,7 @@ async def test_full_product_lifecycle(self, authenticated_client: httpx.AsyncCli
4646
assert final.status_code == 200
4747
assert all(p["id"] != product_id for p in final.json())
4848

49-
async def test_multiple_products_listed_in_order(
50-
self, authenticated_client: httpx.AsyncClient
51-
):
49+
async def test_multiple_products_listed_in_order(self, authenticated_client: httpx.AsyncClient):
5250
names = ["Alpha", "Beta", "Gamma"]
5351
ids = []
5452
for name in names:

0 commit comments

Comments
 (0)