Skip to content

Commit e4a5f8e

Browse files
chore: ruff check + ruff format pass
Apply ruff's safe fixes and format the entire codebase so CI's 'ruff check' and 'ruff format --check' steps are green. Notable changes: - Replace typing.List with list[] (PEP 585). - Replace (str, Enum) classes with StrEnum (Python 3.11+). - Drop unused typing imports. - Reformat all touched files to ruff's default style. No behavior change. 29/29 tests still pass, smoke test still passes.
1 parent d7f94cb commit e4a5f8e

21 files changed

Lines changed: 143 additions & 184 deletions

scripts/smoke_test.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
ADMIN_PASSWORD="StrongTestAdminPass123!" \
66
venv/bin/python scripts/smoke_test.py
77
"""
8+
89
from __future__ import annotations
910

1011
import asyncio
@@ -96,17 +97,13 @@ async def main() -> int:
9697
if r.status_code != 200:
9798
failures.append(f"GET /api/v1/user/me/ returned {r.status_code}, expected 200")
9899
if "private" not in cache_ctrl_authed.lower():
99-
failures.append(
100-
f"cache-control on authed request was {cache_ctrl_authed!r}, expected private"
101-
)
100+
failures.append(f"cache-control on authed request was {cache_ctrl_authed!r}, expected private")
102101

103102
# --- list users still requires admin (403 for non-admin) ----------
104103
r = await c.get("/api/v1/users", headers=auth)
105104
print(f"GET /api/v1/users (non-admin) -> {r.status_code} {r.text[:120]}")
106105
if r.status_code != 403:
107-
failures.append(
108-
f"GET /api/v1/users as non-admin returned {r.status_code}, expected 403"
109-
)
106+
failures.append(f"GET /api/v1/users as non-admin returned {r.status_code}, expected 403")
110107

111108
# --- /execute-command should be gone (replaced by /command) -------
112109
r = await c.post(
@@ -116,9 +113,7 @@ async def main() -> int:
116113
)
117114
print(f"POST .../execute-command (deprecated) -> {r.status_code} {r.text[:120]}")
118115
if r.status_code != 404:
119-
failures.append(
120-
f"POST /execute-command returned {r.status_code}, expected 404 (endpoint removed)"
121-
)
116+
failures.append(f"POST /execute-command returned {r.status_code}, expected 404 (endpoint removed)")
122117

123118
# --- /docs should be available in LOCAL ---------------------------
124119
r = await c.get("/docs")

src/app/admin/initialize.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
from typing import Optional
2-
31
from crudadmin import CRUDAdmin
42

53
from ..core.config import EnvironmentOption, settings
64
from ..core.db.database import async_get_db
75
from .views import register_admin_views
86

97

10-
def create_admin_interface() -> Optional[CRUDAdmin]:
8+
def create_admin_interface() -> CRUDAdmin | None:
119
"""Create and configure the admin interface."""
1210
if not settings.CRUD_ADMIN_ENABLED:
1311
return None

src/app/admin/views.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
from typing import Annotated
2-
31
from crudadmin import CRUDAdmin
42
from crudadmin.admin_interface.model_view import PasswordTransformer
5-
from pydantic import BaseModel, Field
63

74
from ..core.security import get_password_hash
85
from ..models.tier import Tier

src/app/api/v1/logout.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from typing import Optional
2-
31
from fastapi import APIRouter, Cookie, Depends, Response
42
from jose import JWTError
53
from sqlalchemy.ext.asyncio import AsyncSession
@@ -15,7 +13,7 @@
1513
async def logout(
1614
response: Response,
1715
access_token: str = Depends(oauth2_scheme),
18-
refresh_token: Optional[str] = Cookie(None, alias="refresh_token"),
16+
refresh_token: str | None = Cookie(None, alias="refresh_token"),
1917
db: AsyncSession = Depends(async_get_db),
2018
) -> dict[str, str]:
2119
try:

src/app/api/v1/security_agents.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import uuid
2-
from typing import List
32

43
from fastapi import APIRouter, Depends, HTTPException
54
from sqlalchemy.ext.asyncio import AsyncSession
@@ -14,14 +13,15 @@
1413
router = APIRouter(prefix="/security-agents", tags=["security-agents"])
1514

1615

17-
@router.get("/", response_model=List[SecurityAgentRead], dependencies=[Depends(get_current_user)])
16+
@router.get("/", response_model=list[SecurityAgentRead], dependencies=[Depends(get_current_user)])
1817
async def get_security_agents(db: AsyncSession = Depends(async_get_db)):
1918
agents = await crud_security_agent.get_multi(db)
2019
return agents
2120

2221

23-
@router.post("/{agent_id}/command", response_model=CommandTaskRead, status_code=201,
24-
dependencies=[Depends(get_current_user)])
22+
@router.post(
23+
"/{agent_id}/command", response_model=CommandTaskRead, status_code=201, dependencies=[Depends(get_current_user)]
24+
)
2525
async def register_command(
2626
agent_id: str,
2727
command: CommandTaskCreate,
@@ -47,8 +47,9 @@ async def register_command(
4747
return task
4848

4949

50-
@router.get("/{agent_id}/task-status/{task_id}", response_model=CommandTaskRead,
51-
dependencies=[Depends(get_current_user)])
50+
@router.get(
51+
"/{agent_id}/task-status/{task_id}", response_model=CommandTaskRead, dependencies=[Depends(get_current_user)]
52+
)
5253
async def get_task_status(agent_id: str, task_id: str, db: AsyncSession = Depends(async_get_db)):
5354
task = await crud_command_task.get(db, task_id)
5455
if not task or task.agent_id != agent_id:

src/app/api/v1/users.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ async def write_user(
4141
return created_user
4242

4343

44-
@router.get("/users", response_model=PaginatedListResponse[UserRead],
45-
dependencies=[Depends(get_current_superuser)])
44+
@router.get("/users", response_model=PaginatedListResponse[UserRead], dependencies=[Depends(get_current_superuser)])
4645
async def read_users(
4746
request: Request, db: Annotated[AsyncSession, Depends(async_get_db)], page: int = 1, items_per_page: int = 10
4847
) -> dict:
@@ -62,8 +61,7 @@ async def read_users_me(request: Request, current_user: Annotated[dict, Depends(
6261
return current_user
6362

6463

65-
@router.get("/user/{username}", response_model=UserRead,
66-
dependencies=[Depends(get_current_user)])
64+
@router.get("/user/{username}", response_model=UserRead, dependencies=[Depends(get_current_user)])
6765
async def read_user(
6866
request: Request, username: str, db: Annotated[AsyncSession, Depends(async_get_db)]
6967
) -> dict[str, Any]:

src/app/core/config.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import os
2-
from enum import Enum
2+
from enum import StrEnum
33

4-
from pydantic import SecretStr, field_validator, model_validator, computed_field
4+
from pydantic import SecretStr, field_validator, model_validator
55
from pydantic_settings import BaseSettings, SettingsConfigDict
66

7-
87
# Known-insecure default values that must never be used outside LOCAL env.
98
_INSECURE_SECRET_KEY_DEFAULT = "secret-key"
109
_INSECURE_ADMIN_PASSWORD_DEFAULT = "!Ch4ng3Th1sP4ssW0rd!"
@@ -46,8 +45,7 @@ class FirstUserSettings(BaseSettings):
4645
ADMIN_PASSWORD: str = _INSECURE_ADMIN_PASSWORD_DEFAULT
4746

4847

49-
class TestSettings(BaseSettings):
50-
...
48+
class TestSettings(BaseSettings): ...
5149

5250

5351
class ClientSideCacheSettings(BaseSettings):
@@ -81,7 +79,7 @@ class CRUDAdminSettings(BaseSettings):
8179
CRUD_ADMIN_REDIS_SSL: bool = False
8280

8381

84-
class EnvironmentOption(str, Enum):
82+
class EnvironmentOption(StrEnum):
8583
LOCAL = "local"
8684
STAGING = "staging"
8785
PRODUCTION = "production"
@@ -125,13 +123,13 @@ def _validate_secret_key(cls, v: SecretStr) -> SecretStr:
125123
if v.get_secret_value() == _INSECURE_SECRET_KEY_DEFAULT:
126124
raise ValueError(
127125
"SECRET_KEY is set to the known-insecure placeholder 'secret-key'. "
128-
"Generate a strong key with: python -c \"import secrets; print(secrets.token_urlsafe(32))\" "
126+
'Generate a strong key with: python -c "import secrets; print(secrets.token_urlsafe(32))" '
129127
"and set it as the SECRET_KEY env var."
130128
)
131129
if len(v.get_secret_value()) < 32:
132130
raise ValueError(
133131
f"SECRET_KEY must be at least 32 characters long (got {len(v.get_secret_value())}). "
134-
"Use: python -c \"import secrets; print(secrets.token_urlsafe(32))\""
132+
'Use: python -c "import secrets; print(secrets.token_urlsafe(32))"'
135133
)
136134
return v
137135

@@ -144,9 +142,7 @@ def _validate_admin_password(cls, v: str) -> str:
144142
"Set a strong, unique password via the ADMIN_PASSWORD env var."
145143
)
146144
if len(v) < 12:
147-
raise ValueError(
148-
f"ADMIN_PASSWORD must be at least 12 characters long (got {len(v)})."
149-
)
145+
raise ValueError(f"ADMIN_PASSWORD must be at least 12 characters long (got {len(v)}).")
150146
return v
151147

152148
@model_validator(mode="after")
@@ -160,13 +156,11 @@ def _validate_cors_for_environment(self) -> "Settings":
160156
)
161157
if "*" in self.CORS_METHODS:
162158
raise ValueError(
163-
"CORS_METHODS cannot contain '*' in non-LOCAL environments. "
164-
"List explicit HTTP methods."
159+
"CORS_METHODS cannot contain '*' in non-LOCAL environments. List explicit HTTP methods."
165160
)
166161
if "*" in self.CORS_HEADERS:
167162
raise ValueError(
168-
"CORS_HEADERS cannot contain '*' in non-LOCAL environments. "
169-
"List explicit header names."
163+
"CORS_HEADERS cannot contain '*' in non-LOCAL environments. List explicit header names."
170164
)
171165
return self
172166

src/app/core/security.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import hashlib
2-
from datetime import datetime, timedelta, timezone
3-
from enum import Enum
2+
from datetime import UTC, datetime, timedelta
3+
from enum import StrEnum
44
from typing import Any, Literal
55

66
import anyio
@@ -28,7 +28,7 @@
2828
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/login")
2929

3030

31-
class TokenType(str, Enum):
31+
class TokenType(StrEnum):
3232
ACCESS = "access"
3333
REFRESH = "refresh"
3434

@@ -70,9 +70,7 @@ async def get_password_hash_async(password: str) -> str:
7070
return await anyio.to_thread.run_sync(get_password_hash, password)
7171

7272

73-
async def authenticate_user(
74-
username_or_email: str, password: str, db: AsyncSession
75-
) -> dict[str, Any] | Literal[False]:
73+
async def authenticate_user(username_or_email: str, password: str, db: AsyncSession) -> dict[str, Any] | Literal[False]:
7674
if "@" in username_or_email:
7775
db_user = await crud_users.get(db=db, email=username_or_email, is_deleted=False)
7876
else:
@@ -93,7 +91,7 @@ async def authenticate_user(
9391

9492
async def create_access_token(data: dict[str, Any], expires_delta: timedelta | None = None) -> str:
9593
to_encode = data.copy()
96-
expire = datetime.now(timezone.utc) + (
94+
expire = datetime.now(UTC) + (
9795
expires_delta if expires_delta is not None else timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
9896
)
9997
to_encode.update({"exp": expire, "token_type": TokenType.ACCESS})
@@ -103,7 +101,7 @@ async def create_access_token(data: dict[str, Any], expires_delta: timedelta | N
103101

104102
async def create_refresh_token(data: dict[str, Any], expires_delta: timedelta | None = None) -> str:
105103
to_encode = data.copy()
106-
expire = datetime.now(timezone.utc) + (
104+
expire = datetime.now(UTC) + (
107105
expires_delta if expires_delta is not None else timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
108106
)
109107
to_encode.update({"exp": expire, "token_type": TokenType.REFRESH})
@@ -160,7 +158,5 @@ async def blacklist_token(token: str, db: AsyncSession) -> None:
160158
if exp_timestamp is None:
161159
return
162160

163-
expires_at = datetime.fromtimestamp(int(exp_timestamp), tz=timezone.utc)
164-
await crud_token_blacklist.create(
165-
db, object=TokenBlacklistCreate(token=token, expires_at=expires_at)
166-
)
161+
expires_at = datetime.fromtimestamp(int(exp_timestamp), tz=UTC)
162+
await crud_token_blacklist.create(db, object=TokenBlacklistCreate(token=token, expires_at=expires_at))

src/app/core/setup.py

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
DatabaseSettings,
2020
EnvironmentOption,
2121
EnvironmentSettings,
22-
settings,
2322
)
2423
from .db.database import Base
2524
from .db.database import async_engine as engine
@@ -38,13 +37,7 @@ async def set_threadpool_tokens(number_of_tokens: int = 100) -> None:
3837

3938

4039
def lifespan_factory(
41-
settings: (
42-
DatabaseSettings
43-
| AppSettings
44-
| ClientSideCacheSettings
45-
| CORSSettings
46-
| EnvironmentSettings
47-
),
40+
settings: (DatabaseSettings | AppSettings | ClientSideCacheSettings | CORSSettings | EnvironmentSettings),
4841
create_tables_on_start: bool = True,
4942
) -> Callable[[FastAPI], _AsyncGeneratorContextManager[Any]]:
5043
"""Factory to create a lifespan async context manager for a FastAPI app."""
@@ -77,13 +70,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator:
7770
# -------------- application --------------
7871
def create_application(
7972
router: APIRouter,
80-
settings: (
81-
DatabaseSettings
82-
| AppSettings
83-
| ClientSideCacheSettings
84-
| CORSSettings
85-
| EnvironmentSettings
86-
),
73+
settings: (DatabaseSettings | AppSettings | ClientSideCacheSettings | CORSSettings | EnvironmentSettings),
8774
create_tables_on_start: bool = True,
8875
lifespan: Callable[[FastAPI], _AsyncGeneratorContextManager[Any]] | None = None,
8976
**kwargs: Any,

src/app/crud/crud_command_task.py

Lines changed: 41 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,41 @@
1-
from typing import List
2-
3-
from sqlalchemy.ext.asyncio import AsyncSession
4-
from sqlalchemy.future import select
5-
6-
from ..models.command_task import CommandTask
7-
from ..schemas.command_task import CommandTaskCreate, CommandTaskUpdate
8-
9-
10-
class CRUDCommandTask:
11-
async def create(self, db: AsyncSession, obj_in: CommandTaskCreate, agent_id: str, task_id: str) -> CommandTask:
12-
db_obj = CommandTask(task_id=task_id, agent_id=agent_id, **obj_in.model_dump())
13-
db.add(db_obj)
14-
await db.commit()
15-
await db.refresh(db_obj)
16-
return db_obj
17-
18-
async def get(self, db: AsyncSession, task_id: str) -> CommandTask | None:
19-
result = await db.execute(select(CommandTask).where(CommandTask.task_id == task_id))
20-
return result.scalars().first()
21-
22-
async def get_by_agent(self, db: AsyncSession, agent_id: str) -> List[CommandTask]:
23-
result = await db.execute(select(CommandTask).where(CommandTask.agent_id == agent_id))
24-
return list(result.scalars().all())
25-
26-
async def update(self, db: AsyncSession, db_obj: CommandTask, obj_in: CommandTaskUpdate) -> CommandTask:
27-
update_data = obj_in.model_dump(exclude_unset=True)
28-
for field, value in update_data.items():
29-
setattr(db_obj, field, value)
30-
await db.commit()
31-
await db.refresh(db_obj)
32-
return db_obj
33-
34-
async def remove(self, db: AsyncSession, task_id: str) -> CommandTask | None:
35-
result = await db.execute(select(CommandTask).where(CommandTask.task_id == task_id))
36-
db_obj = result.scalars().first()
37-
if db_obj:
38-
await db.delete(db_obj)
39-
await db.commit()
40-
return db_obj
41-
42-
43-
crud_command_task = CRUDCommandTask()
1+
from sqlalchemy.ext.asyncio import AsyncSession
2+
from sqlalchemy.future import select
3+
4+
from ..models.command_task import CommandTask
5+
from ..schemas.command_task import CommandTaskCreate, CommandTaskUpdate
6+
7+
8+
class CRUDCommandTask:
9+
async def create(self, db: AsyncSession, obj_in: CommandTaskCreate, agent_id: str, task_id: str) -> CommandTask:
10+
db_obj = CommandTask(task_id=task_id, agent_id=agent_id, **obj_in.model_dump())
11+
db.add(db_obj)
12+
await db.commit()
13+
await db.refresh(db_obj)
14+
return db_obj
15+
16+
async def get(self, db: AsyncSession, task_id: str) -> CommandTask | None:
17+
result = await db.execute(select(CommandTask).where(CommandTask.task_id == task_id))
18+
return result.scalars().first()
19+
20+
async def get_by_agent(self, db: AsyncSession, agent_id: str) -> list[CommandTask]:
21+
result = await db.execute(select(CommandTask).where(CommandTask.agent_id == agent_id))
22+
return list(result.scalars().all())
23+
24+
async def update(self, db: AsyncSession, db_obj: CommandTask, obj_in: CommandTaskUpdate) -> CommandTask:
25+
update_data = obj_in.model_dump(exclude_unset=True)
26+
for field, value in update_data.items():
27+
setattr(db_obj, field, value)
28+
await db.commit()
29+
await db.refresh(db_obj)
30+
return db_obj
31+
32+
async def remove(self, db: AsyncSession, task_id: str) -> CommandTask | None:
33+
result = await db.execute(select(CommandTask).where(CommandTask.task_id == task_id))
34+
db_obj = result.scalars().first()
35+
if db_obj:
36+
await db.delete(db_obj)
37+
await db.commit()
38+
return db_obj
39+
40+
41+
crud_command_task = CRUDCommandTask()

0 commit comments

Comments
 (0)