Skip to content

Commit 489e4ff

Browse files
vitoficoclaude
andcommitted
🚨 chore(server-ci): fix install, lint, and format
- Workflow installs into a project venv instead of --system (PEP 668) - Run ruff/pytest via uv run so they pick up the venv - ruff format the whole tree (was missing in the initial scaffolds) - conftest.py: imports consolidated to top of file (E402) - auth.py: Annotated[..., Depends(...)] dep instead of default (B008) - auth.py: shorten thread-offload comment (E501) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a0a33bb commit 489e4ff

12 files changed

Lines changed: 177 additions & 61 deletions

File tree

.github/workflows/server-ci.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ jobs:
2424
- name: Set up Python
2525
run: uv python install 3.12
2626
- name: Install
27-
run: uv pip install --system -e ".[dev]"
27+
run: |
28+
uv venv
29+
uv pip install -e ".[dev]"
2830
- name: Lint
29-
run: ruff check . && ruff format --check .
31+
run: uv run ruff check . && uv run ruff format --check .
3032
- name: Test
31-
run: pytest -v
33+
run: uv run pytest -v
3234

3335
image:
3436
needs: test

server/migrations/env.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ def _ensure_url() -> None:
3535

3636
def run_migrations_offline() -> None:
3737
_ensure_url()
38-
context.configure(url=config.get_main_option("sqlalchemy.url"), target_metadata=target_metadata, literal_binds=True)
38+
context.configure(
39+
url=config.get_main_option("sqlalchemy.url"),
40+
target_metadata=target_metadata,
41+
literal_binds=True,
42+
)
3943
with context.begin_transaction():
4044
context.run_migrations()
4145

server/migrations/versions/0001_initial.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
Revises:
55
Create Date: 2026-05-05 00:00:00.000000
66
"""
7-
from alembic import op
8-
import sqlalchemy as sa
97

8+
import sqlalchemy as sa
9+
from alembic import op
1010

1111
revision = "0001"
1212
down_revision = None
@@ -21,22 +21,39 @@ def upgrade() -> None:
2121
sa.Column("user_id", sa.String(), nullable=False),
2222
sa.Column("metadata_id", sa.String(), nullable=True),
2323
sa.Column("content_hash", sa.String(), nullable=False),
24-
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
24+
sa.Column(
25+
"created_at",
26+
sa.DateTime(timezone=True),
27+
server_default=sa.text("now()"),
28+
nullable=False,
29+
),
2530
sa.UniqueConstraint("user_id", "metadata_id", name="uq_documents_user_metadata"),
2631
sa.UniqueConstraint("user_id", "content_hash", name="uq_documents_user_content_hash"),
2732
)
2833
op.create_index("ix_documents_user", "documents", ["user_id"])
2934

3035
op.create_table(
3136
"progress",
32-
sa.Column("document_pk", sa.BigInteger(), sa.ForeignKey("documents.pk", ondelete="CASCADE"), primary_key=True),
37+
sa.Column(
38+
"document_pk",
39+
sa.BigInteger(),
40+
sa.ForeignKey("documents.pk", ondelete="CASCADE"),
41+
primary_key=True,
42+
),
3343
sa.Column("locator", sa.String(), nullable=False),
3444
sa.Column("percent", sa.Float(), nullable=False),
3545
sa.Column("client_updated_at", sa.DateTime(timezone=True), nullable=False),
36-
sa.Column("received_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
46+
sa.Column(
47+
"received_at",
48+
sa.DateTime(timezone=True),
49+
server_default=sa.text("now()"),
50+
nullable=False,
51+
),
3752
sa.CheckConstraint("percent >= 0 AND percent <= 1", name="ck_progress_percent_range"),
3853
)
39-
op.create_index("ix_progress_document_client_updated_at", "progress", ["document_pk", "client_updated_at"])
54+
op.create_index(
55+
"ix_progress_document_client_updated_at", "progress", ["document_pk", "client_updated_at"]
56+
)
4057

4158

4259
def downgrade() -> None:

server/opds_sync/api/health.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,7 @@ async def readyz() -> dict:
1717
async with session_scope() as s:
1818
await s.execute(text("select 1"))
1919
except Exception as e: # noqa: BLE001 — readiness must not leak details
20-
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="db unreachable") from e
20+
raise HTTPException(
21+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="db unreachable"
22+
) from e
2123
return {"status": "ready"}

server/opds_sync/api/progress.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime, timezone
1+
from datetime import UTC, datetime
22
from typing import Annotated, Literal
33

44
from fastapi import APIRouter, Depends, Query
@@ -37,7 +37,7 @@ class ProgressPushResult(BaseModel):
3737
@field_serializer("server_client_updated_at")
3838
def _serialize_dt(self, v: datetime) -> str:
3939
if v.tzinfo is None:
40-
v = v.replace(tzinfo=timezone.utc)
40+
v = v.replace(tzinfo=UTC)
4141
return v.isoformat()
4242

4343

server/opds_sync/core/auth.py

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

33
import asyncio
44
import logging
5-
from typing import Protocol
5+
from typing import Annotated, Protocol
66

7-
import httpx
87
import jwt
98
from fastapi import Depends, HTTPException, Request, status
109
from jwt import PyJWKClient
@@ -23,7 +22,7 @@ def __init__(self, jwks_url: str) -> None:
2322
self._client = PyJWKClient(jwks_url, cache_keys=True, lifespan=24 * 3600)
2423

2524
async def get_signing_key(self, kid: str):
26-
# PyJWKClient is sync; in practice the JWKS is small and cached, so a thread offload is fine.
25+
# PyJWKClient is sync; the JWKS is small and cached, so a thread offload is fine.
2726
# If this becomes hot, switch to httpx + manual JWKS parsing.
2827
return await asyncio.to_thread(self._client.get_signing_key, kid)
2928

@@ -59,7 +58,10 @@ async def get_validator(request: Request) -> JwtValidator:
5958
return request.app.state.jwt_validator
6059

6160

62-
async def current_user_id(request: Request, validator: JwtValidator = Depends(get_validator)) -> str:
61+
async def current_user_id(
62+
request: Request,
63+
validator: Annotated[JwtValidator, Depends(get_validator)],
64+
) -> str:
6365
auth = request.headers.get("authorization")
6466
if not auth or not auth.lower().startswith("bearer "):
6567
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing bearer")

server/opds_sync/core/identity.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def normalize_metadata_id(raw: str | None) -> str | None:
1414
if not s:
1515
return None
1616
if s.startswith("urn:"):
17-
s = s[len("urn:"):]
17+
s = s[len("urn:") :]
1818
s = _SCHEME_PREFIX.sub("", s, count=1)
1919
s = _WHITESPACE_AND_HYPHEN.sub("", s)
2020
return s or None

server/opds_sync/db/models.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ class Document(Base):
3030
user_id: Mapped[str] = mapped_column(String, nullable=False)
3131
metadata_id: Mapped[str | None] = mapped_column(String, nullable=True)
3232
content_hash: Mapped[str] = mapped_column(String, nullable=False)
33-
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
33+
created_at: Mapped[datetime] = mapped_column(
34+
DateTime(timezone=True), server_default=func.now(), nullable=False
35+
)
3436

35-
progress: Mapped["Progress | None"] = relationship(back_populates="document", uselist=False, cascade="all, delete-orphan")
37+
progress: Mapped["Progress | None"] = relationship(
38+
back_populates="document", uselist=False, cascade="all, delete-orphan"
39+
)
3640

3741

3842
class Progress(Base):
@@ -42,10 +46,14 @@ class Progress(Base):
4246
Index("ix_progress_document_client_updated_at", "document_pk", "client_updated_at"),
4347
)
4448

45-
document_pk: Mapped[int] = mapped_column(BigInteger, ForeignKey("documents.pk", ondelete="CASCADE"), primary_key=True)
49+
document_pk: Mapped[int] = mapped_column(
50+
BigInteger, ForeignKey("documents.pk", ondelete="CASCADE"), primary_key=True
51+
)
4652
locator: Mapped[str] = mapped_column(String, nullable=False)
4753
percent: Mapped[float] = mapped_column(Float, nullable=False)
4854
client_updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
49-
received_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False)
55+
received_at: Mapped[datetime] = mapped_column(
56+
DateTime(timezone=True), server_default=func.now(), nullable=False
57+
)
5058

5159
document: Mapped[Document] = relationship(back_populates="progress")

server/tests/conftest.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
1-
import pytest
1+
import time
22
from collections.abc import AsyncIterator, Iterator
33

4+
import jwt
5+
import pytest
6+
from alembic import command
7+
from alembic.config import Config as AlembicConfig
8+
from cryptography.hazmat.primitives import serialization
9+
from cryptography.hazmat.primitives.asymmetric import rsa
10+
from sqlalchemy.ext.asyncio import (
11+
AsyncEngine,
12+
AsyncSession,
13+
async_sessionmaker,
14+
create_async_engine,
15+
)
16+
from testcontainers.postgres import PostgresContainer
17+
418

519
def pytest_collection_modifyitems(items):
620
"""Ensure test_schema runs before test_progress to avoid committed-row cross-pollution."""
21+
722
def _key(item):
823
path = item.nodeid
924
if "test_schema" in path:
1025
return 0
1126
if "test_progress" in path:
1227
return 1
1328
return 2
14-
items.sort(key=_key)
1529

16-
from alembic import command
17-
from alembic.config import Config as AlembicConfig
18-
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
19-
from testcontainers.postgres import PostgresContainer
30+
items.sort(key=_key)
2031

2132

2233
@pytest.fixture(scope="session")
@@ -49,13 +60,6 @@ async def session(engine: AsyncEngine) -> AsyncIterator[AsyncSession]:
4960
await s.rollback()
5061

5162

52-
import time
53-
54-
import jwt
55-
from cryptography.hazmat.primitives import serialization
56-
from cryptography.hazmat.primitives.asymmetric import rsa
57-
58-
5963
@pytest.fixture(scope="session")
6064
def signing_key():
6165
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
@@ -81,10 +85,20 @@ def audience() -> str:
8185
@pytest.fixture
8286
def make_token(signing_key, issuer, audience):
8387
priv, _ = signing_key
88+
8489
def _make(sub: str, ttl: int = 60, **extra) -> str:
8590
now = int(time.time())
86-
claims = {"sub": sub, "iss": issuer, "aud": audience, "iat": now, "nbf": now, "exp": now + ttl, **extra}
91+
claims = {
92+
"sub": sub,
93+
"iss": issuer,
94+
"aud": audience,
95+
"iat": now,
96+
"nbf": now,
97+
"exp": now + ttl,
98+
**extra,
99+
}
87100
return jwt.encode(claims, priv, algorithm="RS256", headers={"kid": "test-key"})
101+
88102
return _make
89103

90104

@@ -97,6 +111,7 @@ def app_under_test(postgres_url, alembic_upgrade, monkeypatch, signing_key, issu
97111

98112
# Bust the lru_cache so the env vars take effect.
99113
from opds_sync.config import get_settings
114+
100115
get_settings.cache_clear()
101116

102117
from opds_sync.core.auth import JwksFetcher, JwtValidator
@@ -106,7 +121,9 @@ def app_under_test(postgres_url, alembic_upgrade, monkeypatch, signing_key, issu
106121

107122
class StaticJwks(JwksFetcher):
108123
async def get_signing_key(self, kid): # type: ignore[override]
109-
class _K: key = pub # noqa: E701
124+
class _K:
125+
key = pub # noqa: E701
126+
110127
return _K()
111128

112129
app = create_app()

server/tests/integration/test_health.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
async def test_healthz_returns_200():
55
from opds_sync.main import create_app
6+
67
app = create_app()
78
transport = ASGITransport(app=app)
89
async with AsyncClient(transport=transport, base_url="http://test") as client:
@@ -15,9 +16,11 @@ async def test_readyz_returns_200_when_db_reachable(postgres_url, alembic_upgrad
1516
monkeypatch.setenv("OPDS_SYNC_DATABASE_URL", postgres_url)
1617
# Bust the lru_cache on get_settings so the new env var is honoured
1718
from opds_sync.config import get_settings
19+
1820
get_settings.cache_clear()
1921

2022
from opds_sync.main import create_app
23+
2124
app = create_app()
2225
transport = ASGITransport(app=app)
2326
async with AsyncClient(transport=transport, base_url="http://test") as client:

0 commit comments

Comments
 (0)