Skip to content

Commit c11542d

Browse files
ImTotemclaude
andcommitted
refactor: code cleanup — remove dead code, deduplicate, add base classes
Step 1: Delete dead code + REST routers - Remove Sheets repositories (member/repository.py, shorten/repository.py) - Remove REST routers (member/router.py, shorten/router.py, track.py) - Clean up main.py imports and router registrations - Remove service.py alias imports (LinkRepository → PgLinkRepository) Step 2: Unify auth token extraction - Add extract_raw() and decode_or_none() to auth/token.py - Replace duplicate logic in dependencies.py and graphql/context.py Step 3: BaseRepository - New repository.py with find_all() and find_by_id() - PgMemberRepository: 19 lines → 9 lines - PgLinkRepository: 57 lines → 45 lines (domain methods only) Step 4: Auto type conversion - New graphql/convert.py with from_model() and from_paged() - Remove manual _to_member, _to_link, _to_detail helpers - member/resolvers.py: 94 → 66 lines - shorten/resolvers.py: 131 → 88 lines ~185 lines removed, 3 new shared utilities. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 57e761a commit c11542d

16 files changed

Lines changed: 89 additions & 338 deletions

src/bcsd_api/auth/token.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from datetime import datetime, timedelta, timezone
22

3+
from fastapi import Request
34
from jose import JWTError, jwt
45

56
from bcsd_api.exception import Unauthorized
@@ -16,3 +17,17 @@ def decode_token(token: str, secret: str, algorithm: str) -> dict:
1617
return jwt.decode(token, secret, algorithms=[algorithm])
1718
except JWTError:
1819
raise Unauthorized("invalid or expired token")
20+
21+
22+
def extract_raw(request: Request, cookie_name: str) -> str | None:
23+
header = request.headers.get("Authorization", "")
24+
if header.startswith("Bearer "):
25+
return header[7:]
26+
return request.cookies.get(cookie_name)
27+
28+
29+
def decode_or_none(raw: str, secret: str, algorithm: str) -> dict | None:
30+
try:
31+
return jwt.decode(raw, secret, algorithms=[algorithm])
32+
except JWTError:
33+
return None

src/bcsd_api/dependencies.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,11 @@ def get_link_repo(conn: Connection = Depends(get_conn)) -> PgLinkRepository:
6767
return PgLinkRepository(conn)
6868

6969

70-
def _extract_token(request: Request, settings: Settings) -> str:
71-
header = request.headers.get("Authorization", "")
72-
if header.startswith("Bearer "):
73-
return header[7:]
74-
cookie = request.cookies.get(settings.cookie_name)
75-
if cookie:
76-
return cookie
77-
raise Unauthorized("missing authorization")
78-
79-
8070
def current_user(
8171
request: Request,
8272
settings: Settings = Depends(get_settings),
8373
) -> dict:
84-
raw = _extract_token(request, settings)
74+
raw = jwt_token.extract_raw(request, settings.cookie_name)
75+
if not raw:
76+
raise Unauthorized("missing authorization")
8577
return jwt_token.decode_token(raw, settings.jwt_secret, settings.jwt_algorithm)

src/bcsd_api/graphql/context.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,10 @@ def __init__(self, conn, member_repo, link_repo, user):
2424

2525

2626
def _try_auth(request: Request, settings: Settings) -> dict | None:
27-
header = request.headers.get("Authorization", "")
28-
if header.startswith("Bearer "):
29-
raw = header[7:]
30-
else:
31-
raw = request.cookies.get(settings.cookie_name, "")
27+
raw = jwt_token.extract_raw(request, settings.cookie_name)
3228
if not raw:
3329
return None
34-
try:
35-
return jwt_token.decode_token(
36-
raw, settings.jwt_secret, settings.jwt_algorithm,
37-
)
38-
except Exception:
39-
return None
30+
return jwt_token.decode_or_none(raw, settings.jwt_secret, settings.jwt_algorithm)
4031

4132

4233
def require_user(ctx: GqlContext) -> dict:

src/bcsd_api/graphql/convert.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from pydantic import BaseModel
2+
3+
4+
def from_model(source: BaseModel, target_cls):
5+
return target_cls(**source.model_dump())
6+
7+
8+
def from_paged(paged, item_cls, paged_cls):
9+
items = [from_model(m, item_cls) for m in paged.items]
10+
return paged_cls(
11+
items=items, total=paged.total,
12+
page=paged.page, size=paged.size,
13+
)

src/bcsd_api/main.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,7 @@
99
from .auth.router import router as auth_router
1010
from .dependencies import get_authz, get_settings
1111
from .exception import register_handlers
12-
from .member.router import router as member_router
1312
from .redirect import router as redirect_router
14-
from .shorten.router import router as shorten_router
15-
from .track import router as track_router
1613

1714
logger = logging.getLogger(__name__)
1815

@@ -71,9 +68,6 @@ def create_app() -> FastAPI:
7168
)
7269
register_handlers(app)
7370
app.include_router(auth_router)
74-
app.include_router(member_router)
75-
app.include_router(track_router)
76-
app.include_router(shorten_router)
7771
app.include_router(redirect_router)
7872
_mount_graphql(app)
7973
return app
Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
1-
from sqlalchemy import Connection, select
1+
from sqlalchemy import Connection
22

3+
from bcsd_api.repository import BaseRepository
34
from bcsd_api.tables import members
45

56

6-
class PgMemberRepository:
7+
class PgMemberRepository(BaseRepository):
78
def __init__(self, conn: Connection):
8-
self._conn = conn
9-
10-
def find_all(self) -> list[dict]:
11-
rows = self._conn.execute(select(members))
12-
return [row._asdict() for row in rows]
13-
14-
def find_by_id(self, member_id: str) -> dict | None:
15-
stmt = select(members).where(members.c.id == member_id)
16-
row = self._conn.execute(stmt).first()
17-
if not row:
18-
return None
19-
return row._asdict()
9+
super().__init__(conn, members)

src/bcsd_api/member/repository.py

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/bcsd_api/member/resolvers.py

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
from bcsd_api.filter.members import MemberFilter
55
from bcsd_api.graphql.context import GqlContext, require_user
6+
from bcsd_api.graphql.convert import from_model, from_paged
67

78
from . import service
8-
from .schema import MemberDetail, MemberResponse
99
from .types import (
1010
FiltersType,
1111
MeType,
@@ -26,25 +26,6 @@ def _to_filter(inp: MemberFilterInput) -> MemberFilter:
2626
)
2727

2828

29-
def _to_member(m: MemberResponse) -> MemberType:
30-
return MemberType(
31-
id=m.id, name=m.name, email=m.email,
32-
status=m.status, track=m.track,
33-
team=m.team, payment_status=m.payment_status,
34-
)
35-
36-
37-
def _to_detail(m: MemberDetail) -> MemberDetailType:
38-
return MemberDetailType(
39-
id=m.id, name=m.name, email=m.email,
40-
status=m.status, track=m.track,
41-
team=m.team, payment_status=m.payment_status,
42-
department=m.department, student_id=m.student_id,
43-
school_email=m.school_email, phone=m.phone,
44-
join_date=m.join_date, last_updated=m.last_updated,
45-
)
46-
47-
4829
def resolve_members(
4930
info: Info[GqlContext, None],
5031
filter: MemberFilterInput | None = None,
@@ -53,26 +34,18 @@ def resolve_members(
5334
require_user(ctx)
5435
filt = _to_filter(filter) if filter else MemberFilter.model_validate({})
5536
paged = service.list_members(ctx.member_repo, filt)
56-
items = [_to_member(m) for m in paged.items]
57-
return PagedMembers(
58-
items=items, total=paged.total,
59-
page=paged.page, size=paged.size,
60-
)
37+
return from_paged(paged, MemberType, PagedMembers)
6138

6239

6340
def resolve_member(info: Info[GqlContext, None], id: strawberry.ID) -> MemberDetailType:
6441
require_user(info.context)
6542
m = service.get_member(info.context.member_repo, id)
66-
return _to_detail(m)
43+
return from_model(m, MemberDetailType)
6744

6845

6946
def resolve_filters(info: Info[GqlContext, None]) -> FiltersType:
7047
f = service.get_filters(info.context.conn)
71-
return FiltersType(
72-
tracks=f.tracks,
73-
statuses=f.statuses,
74-
payment_statuses=f.payment_statuses,
75-
)
48+
return from_model(f, FiltersType)
7649

7750

7851
def resolve_tracks(info: Info[GqlContext, None]) -> list[str]:
@@ -90,5 +63,5 @@ def resolve_me(info: Info[GqlContext, None]) -> MeType:
9063
return MeType(
9164
id=user["sub"],
9265
email=user["email"],
93-
member=_to_detail(detail),
66+
member=from_model(detail, MemberDetailType),
9467
)

src/bcsd_api/member/router.py

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/bcsd_api/repository.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from sqlalchemy import Connection, select
2+
3+
4+
class BaseRepository:
5+
def __init__(self, conn: Connection, table):
6+
self._conn = conn
7+
self._table = table
8+
9+
def find_all(self) -> list[dict]:
10+
rows = self._conn.execute(select(self._table))
11+
return [row._asdict() for row in rows]
12+
13+
def find_by_id(self, id: str) -> dict | None:
14+
stmt = select(self._table).where(self._table.c.id == id)
15+
row = self._conn.execute(stmt).first()
16+
if not row:
17+
return None
18+
return row._asdict()

0 commit comments

Comments
 (0)