[refactor] cleanup dead code

This commit is contained in:
Piotr Domański 2026-04-07 16:48:30 +02:00
parent 2deb66f7e5
commit 20437905ba
27 changed files with 7 additions and 791 deletions

View file

@ -1,6 +0,0 @@
FROM alpine
RUN apk add --no-cache curl
RUN crontab -l | { cat; echo '1 * * * * curl -X POST "http://tasks:8000/api/cache_product_usage_data" -H "Authorization: Bearer ${API_KEY}"'; } | crontab -
CMD ["crond", "-f", "-l", "2"]

View file

@ -4,56 +4,46 @@ VERSION=0.`git rev-list --count HEAD`
DOCKER_BUILD=docker build DOCKER_BUILD=docker build
ifeq ($(shell uname -m), arm64) ifeq ($(shell uname -m), arm64)
DOCKER_BUILD=docker buildx build --platform linux/amd64 DOCKER_BUILD=docker buildx build --platform linux/amd64 --load
endif endif
api: fooder Dockerfile requirements.txt api: fooder Dockerfile requirements/docker.txt
$(DOCKER_BUILD) -t registry.domandoman.xyz/fooder/api -f Dockerfile . $(DOCKER_BUILD) -t registry.domandoman.xyz/fooder/api -f Dockerfile .
cron: Dockerfile.cron build: api
$(DOCKER_BUILD) -t registry.domandoman.xyz/fooder/cron -f Dockerfile.cron .
build: api cron
push: push:
docker push registry.domandoman.xyz/fooder/api docker push registry.domandoman.xyz/fooder/api
docker push registry.domandoman.xyz/fooder/cron
.PHONY: black mypy flake lint version create-venv test
black: black:
python -m black fooder python -m black fooder
.PHONY: mypy
mypy: mypy:
python -m mypy fooder python -m mypy fooder
.PHONY: flake
flake: flake:
python -m flake8 fooder python -m flake8 fooder
.PHONY: lint
lint: black mypy flake lint: black mypy flake
.PHONY: version
version: version:
@echo $(VERSION) @echo $(VERSION)
.PHONY: create-venv
create-venv: create-venv:
python3 -m venv .venv --prompt="fooderapi-venv" --system-site-packages python3 -m venv .venv --prompt="fooderapi-venv" --system-site-packages
bash -c "source .venv/bin/activate && pip install -r requirements/local.txt" bash -c "source .venv/bin/activate && pip install -r requirements/local.txt"
.PHONY: test
test: test:
./test.sh ./test.sh
.PHONY: alembic # Alembic
alembic: .PHONY: alembic alembic-upgrade alembic-downgrade
alembic: fooder
docker compose exec -e MSG="$(MSG)" api bash -c 'alembic -c /opt/fooder/fooder/alembic.ini revision --autogenerate -m "$${MSG}"' docker compose exec -e MSG="$(MSG)" api bash -c 'alembic -c /opt/fooder/fooder/alembic.ini revision --autogenerate -m "$${MSG}"'
.PHONY: alembic-upgrade
alembic-upgrade: alembic-upgrade:
docker compose exec api bash -c 'alembic -c /opt/fooder/fooder/alembic.ini upgrade head' docker compose exec api bash -c 'alembic -c /opt/fooder/fooder/alembic.ini upgrade head'
.PHONY: alembic-downgrade
alembic-downgrade: alembic-downgrade:
docker compose exec api bash -c 'alembic -c /opt/fooder/fooder/alembic.ini downgrade -1' docker compose exec api bash -c 'alembic -c /opt/fooder/fooder/alembic.ini downgrade -1'

View file

@ -7,10 +7,7 @@ DB_URI="postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database/${PO
ECHO_SQL=0 ECHO_SQL=0
SECRET_KEY="${SECRET_KEY}" # generate with $ openssl rand -hex 32 SECRET_KEY="${SECRET_KEY}" # generate with $ openssl rand -hex 32
API_KEY="${API_KEY}" # generate with $ openssl rand -hex 32
REFRESH_SECRET_KEY="${REFRESH_SECRET_KEY}" # generate with $ openssl rand -hex 32 REFRESH_SECRET_KEY="${REFRESH_SECRET_KEY}" # generate with $ openssl rand -hex 32
ALGORITHM="HS256" ALGORITHM="HS256"
ACCESS_TOKEN_EXPIRE_MINUTES=30 ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_DAYS=30 REFRESH_TOKEN_EXPIRE_DAYS=30
API_KEY="${API_KEY}" # generate with $ openssl rand -hex 32

View file

@ -1,33 +0,0 @@
from typing import Annotated, Any
from fastapi import Depends
from sqlalchemy.ext.asyncio import async_sessionmaker
from ..auth import authorize_api_key, get_current_user
from ..db import get_db_session, AsyncSession
from ..domain.user import User
UserDependency = Annotated[User, Depends(get_current_user)]
ApiKeyDependency = Annotated[None, Depends(authorize_api_key)]
class BaseController:
def __init__(self, session: AsyncSession) -> None:
self.session = session
async def call(self, *args, **kwargs) -> Any:
raise NotImplementedError
async def __call__(self, *args, **kwargs) -> Any:
return await self.call(*args, **kwargs)
class AuthorizedController(BaseController):
def __init__(self, session: AsyncSession, user: UserDependency) -> None:
super().__init__(session)
self.user = user
class TasksSessionController(BaseController):
def __init__(self, session: AsyncSession, api_key: ApiKeyDependency) -> None:
super().__init__(session)

View file

@ -1,24 +0,0 @@
from datetime import date
from fastapi import HTTPException
from ..domain.diary import Diary as DBDiary
from ..model.diary import Diary
from .base import AuthorizedController
class GetDiary(AuthorizedController):
async def call(self, date: date) -> Diary:
diary = await DBDiary.get_diary(self.session, self.user.id, date)
if diary is not None:
return Diary.from_orm(diary)
else:
try:
await DBDiary.create(session, self.user.id, date)
await session.commit()
return Diary.from_orm(
await DBDiary.get_diary(session, self.user.id, date)
)
except AssertionError as e:
raise HTTPException(status_code=400, detail=e.args[0])

View file

@ -1,51 +0,0 @@
from fastapi import HTTPException
from ..domain.entry import Entry as DBEntry
from ..domain.meal import Meal as DBMeal
from ..model.entry import CreateEntryPayload, Entry, UpdateEntryPayload
from .base import AuthorizedController
class CreateEntry(AuthorizedController):
async def call(self, content: CreateEntryPayload) -> Entry:
async with self.async_session.begin() as session:
meal = await DBMeal.get_by_id(session, self.user.id, content.meal_id)
if meal is None:
raise HTTPException(status_code=404, detail="meal not found")
try:
entry = await DBEntry.create(
session, content.meal_id, content.product_id, content.grams
)
return Entry.from_orm(entry)
except AssertionError as e:
raise HTTPException(status_code=400, detail=e.args[0])
class UpdateEntry(AuthorizedController):
async def call(self, entry_id: int, content: UpdateEntryPayload) -> Entry:
async with self.async_session.begin() as session:
entry = await DBEntry.get_by_id(session, self.user.id, entry_id)
if entry is None:
raise HTTPException(status_code=404, detail="entry not found")
try:
await entry.update(
session, content.meal_id, content.product_id, content.grams
)
return Entry.from_orm(entry)
except AssertionError as e:
raise HTTPException(status_code=400, detail=e.args[0])
class DeleteEntry(AuthorizedController):
async def call(self, entry_id: int) -> None:
async with self.async_session.begin() as session:
entry = await DBEntry.get_by_id(session, self.user.id, entry_id)
if entry is None:
raise HTTPException(status_code=404, detail="entry not found")
try:
await entry.delete(session)
except AssertionError as e:
raise HTTPException(status_code=400, detail=e.args[0])

View file

@ -1,95 +0,0 @@
from fastapi import HTTPException
from ..domain.diary import Diary as DBDiary
from ..domain.meal import Meal as DBMeal
from ..domain.preset import Preset as DBPreset
from ..model.meal import (
CreateMealFromPresetPayload,
CreateMealPayload,
Meal,
RenameMealPayload,
SaveMealPayload,
)
from ..model.preset import Preset
from .base import AuthorizedController
class CreateMeal(AuthorizedController):
async def call(self, content: CreateMealPayload) -> Meal:
async with self.async_session.begin() as session:
if not await DBDiary.has_permission(
session, self.user.id, content.diary_id
):
raise HTTPException(status_code=404, detail="not found")
try:
meal = await DBMeal.create(session, content.diary_id, content.name)
return Meal.from_orm(meal)
except AssertionError as e:
raise HTTPException(status_code=400, detail=e.args[0])
class SaveMeal(AuthorizedController):
async def call(self, meal_id: int, payload: SaveMealPayload) -> Preset:
async with self.async_session.begin() as session:
meal = await DBMeal.get_by_id(session, self.user.id, meal_id)
if meal is None:
raise HTTPException(status_code=404, detail="meal not found")
try:
return Preset.from_orm(
await DBPreset.create(
session,
user_id=self.user.id,
name=payload.name or meal.name,
meal=meal,
)
)
except AssertionError as e:
raise HTTPException(status_code=400, detail=e.args[0])
class RenameMeal(AuthorizedController):
async def call(self, meal_id: int, payload: RenameMealPayload) -> Meal:
async with self.async_session.begin() as session:
meal = await DBMeal.get_by_id(session, self.user.id, meal_id)
if meal is None:
raise HTTPException(status_code=404, detail="meal not found")
meal.name = payload.name
await session.flush()
return Meal.from_orm(meal)
class DeleteMeal(AuthorizedController):
async def call(self, meal_id: int) -> None:
async with self.async_session.begin() as session:
meal = await DBMeal.get_by_id(session, self.user.id, meal_id)
if meal is None:
raise HTTPException(status_code=404, detail="meal not found")
try:
await meal.delete(session)
except AssertionError as e:
raise HTTPException(status_code=400, detail=e.args[0])
class CreateMealFromPreset(AuthorizedController):
async def call(self, content: CreateMealFromPresetPayload) -> Meal:
async with self.async_session.begin() as session:
if not await DBDiary.has_permission(
session, self.user.id, content.diary_id
):
raise HTTPException(status_code=404, detail="diary not found")
preset = await DBPreset.get(session, self.user.id, content.preset_id)
if preset is None:
raise HTTPException(status_code=404, detail="preset not found")
try:
meal = await DBMeal.create_from_preset(
session, content.diary_id, content.name, preset
)
return Meal.from_orm(meal)
except AssertionError as e:
raise HTTPException(status_code=400, detail=e.args[0])

View file

@ -1,43 +0,0 @@
from typing import AsyncIterator, Optional
from fastapi import HTTPException
from ..domain.preset import Preset as DBPreset
from ..model.preset import Preset, PresetDetails
from .base import AuthorizedController
class ListPresets(AuthorizedController):
async def call(
self, limit: int, offset: int, q: Optional[str]
) -> AsyncIterator[Preset]:
async with self.async_session() as session:
async for preset in DBPreset.list_all(
session, user_id=self.user.id, limit=limit, offset=offset, q=q
):
yield Preset.from_orm(preset)
class GetPreset(AuthorizedController):
async def call(self, id: int) -> PresetDetails:
async with self.async_session() as session:
preset = await DBPreset.get(session, self.user.id, id)
if preset is not None:
return PresetDetails.from_orm(preset)
raise HTTPException(status_code=404, detail="preset not found")
class DeletePreset(AuthorizedController):
async def call(
self,
id: int,
) -> None:
async with self.async_session.begin() as session:
preset = await DBPreset.get(session, self.user.id, id)
if preset is None:
raise HTTPException(status_code=404, detail="preset not found")
await preset.delete(session)

View file

@ -1,73 +0,0 @@
from typing import AsyncIterator, Optional
from fastapi import HTTPException
from ..domain.product import Product as DBProduct
from ..model.product import CreateProductPayload, Product
from ..utils import product_finder
from .base import AuthorizedController
class CreateProduct(AuthorizedController):
async def call(self, content: CreateProductPayload) -> Product:
async with self.async_session.begin() as session:
try:
product = await DBProduct.create(
session,
content.name,
content.carb,
content.protein,
content.fat,
content.fiber,
)
return Product.from_orm(product)
except AssertionError as e:
raise HTTPException(status_code=400, detail=e.args[0])
class ListProduct(AuthorizedController):
async def call(
self, limit: int, offset: int, q: Optional[str]
) -> AsyncIterator[Product]:
async with self.async_session() as session:
async for product in DBProduct.list_all(
session, limit=limit, offset=offset, q=q
):
yield Product.from_orm(product)
class GetProductByBarCode(AuthorizedController):
async def call(self, barcode: str) -> Product:
async with self.async_session() as session:
product = await DBProduct.get_by_barcode(session, barcode)
if product:
return Product.from_orm(product)
try:
product_data = product_finder.find(barcode)
except product_finder.NotFound:
raise HTTPException(status_code=404, detail="Product not found")
except product_finder.ParseError:
raise HTTPException(
status_code=400, detail="Product was found, but unable to import"
)
try:
product = await DBProduct.create(
session,
product_data.name,
product_data.carb,
product_data.protein,
product_data.fat,
product_data.fiber,
product_data.kcal,
barcode,
)
await session.commit()
return Product.from_orm(
await DBProduct.get_by_barcode(session, barcode)
)
except AssertionError as e:
raise HTTPException(status_code=400, detail=e.args[0])

View file

@ -1,14 +0,0 @@
from fastapi import HTTPException
from ..domain.product import Product as DBProduct
from .base import TasksSessionController
class CacheProductUsageData(TasksSessionController):
async def call(self) -> None:
async with self.async_session.begin() as session:
try:
await DBProduct.cache_usage_data(session)
await session.commit()
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))

View file

@ -1,58 +0,0 @@
from fastapi import HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from ..auth import (
authenticate_user,
create_access_token,
create_refresh_token,
verify_refresh_token,
)
from ..domain.user import User as DBUser
from ..model.token import RefreshTokenPayload, Token
from .base import BaseController
class CreateToken(BaseController):
async def call(self, content: OAuth2PasswordRequestForm) -> Token:
async with self.async_session.begin() as session:
user = await authenticate_user(session, content.username, content.password)
if user is None:
raise HTTPException(
status_code=401, detail="Invalid username or password"
)
refresh_token = await create_refresh_token(session, user)
access_token = create_access_token(user)
return Token(
access_token=access_token,
refresh_token=refresh_token.token,
token_type="bearer",
)
class RefreshToken(BaseController):
async def call(self, content: RefreshTokenPayload) -> Token:
async with self.async_session.begin() as session:
current_token = await verify_refresh_token(session, content.refresh_token)
if current_token is None:
raise HTTPException(status_code=401, detail="Invalid token")
user = await DBUser.get(session, current_token.user_id)
if user is None:
raise HTTPException(status_code=401, detail="Invalid token")
assert user is not None
await current_token.delete(session)
refresh_token = await create_refresh_token(session, user)
access_token = create_access_token(user)
return Token(
access_token=access_token,
refresh_token=refresh_token.token,
token_type="bearer",
)

View file

@ -1,19 +0,0 @@
from fastapi import HTTPException
from ..domain.user import User as DBUser
from ..model.user import CreateUserPayload, User
from .base import BaseController
class CreateUser(BaseController):
async def call(self, content: CreateUserPayload) -> User:
async with self.async_session.begin() as session:
try:
user = await DBUser.create(
session,
content.username,
content.password,
)
return User.from_orm(user)
except AssertionError as e:
raise HTTPException(status_code=400, detail=e.args[0])

View file

@ -1,22 +0,0 @@
from datetime import date
from typing import List
from pydantic import BaseModel
from .meal import Meal
class Diary(BaseModel):
"""Diary represents user diary for given day"""
id: int
date: date
meals: List[Meal]
calories: float
protein: float
carb: float
fat: float
fiber: float
class Config:
from_attributes = True

View file

@ -1,38 +0,0 @@
from typing import Optional
from pydantic import BaseModel
from .product import Product
class Entry(BaseModel):
"""Entry."""
id: int
grams: float
product: Product
meal_id: int
calories: float
protein: float
carb: float
fat: float
fiber: float
class Config:
from_attributes = True
class CreateEntryPayload(BaseModel):
"""CreateEntryPayload."""
grams: float
product_id: int
meal_id: int
class UpdateEntryPayload(BaseModel):
"""CreateEntryPayload."""
grams: Optional[float] = None
product_id: Optional[int] = None
meal_id: Optional[int] = None

View file

@ -1,50 +0,0 @@
from typing import List, Optional
from pydantic import BaseModel
from .entry import Entry
class Meal(BaseModel):
"""Meal."""
id: int
name: str
order: int
calories: float
protein: float
carb: float
fat: float
fiber: float
entries: List[Entry]
diary_id: int
class Config:
from_attributes = True
class CreateMealPayload(BaseModel):
"""CreateMealPayload."""
name: Optional[str]
diary_id: int
class RenameMealPayload(BaseModel):
"""RenameMealPayload."""
name: str
class SaveMealPayload(BaseModel):
"""SaveMealPayload."""
name: Optional[str]
class CreateMealFromPresetPayload(BaseModel):
"""CreateMealPayload."""
name: Optional[str]
diary_id: int
preset_id: int

View file

@ -1,32 +0,0 @@
from typing import List
from pydantic import BaseModel
from .preset_entry import PresetEntry
class Preset(BaseModel):
"""Preset."""
id: int
name: str
calories: float
protein: float
carb: float
fat: float
fiber: float
class Config:
from_attributes = True
class PresetDetails(Preset):
"""PresetDetails."""
entries: List[PresetEntry]
class ListPresetsPayload(BaseModel):
"""ListPresetsPayload."""
presets: List[Preset]

View file

@ -1,20 +0,0 @@
from pydantic import BaseModel
from .product import Product
class PresetEntry(BaseModel):
"""PresetEntry."""
id: int
grams: float
product: Product
preset_id: int
calories: float
protein: float
carb: float
fat: float
fiber: float
class Config:
from_attributes = True

View file

@ -1,12 +0,0 @@
from pydantic import BaseModel, ConfigDict
class User(BaseModel):
model_config = ConfigDict(from_attributes=True)
username: str
class CreateUserPayload(BaseModel):
username: str
password: str

View file

@ -15,8 +15,6 @@ class Settings(BaseSettings):
ALLOWED_ORIGINS: list[str] = ["*"] ALLOWED_ORIGINS: list[str] = ["*"]
API_KEY: str
PASSWORD_SCHEMES: list[str] = ["bcrypt"] PASSWORD_SCHEMES: list[str] = ["bcrypt"]

View file

@ -1,17 +0,0 @@
from datetime import date
from fastapi import APIRouter, Depends, Request
from ..controller.diary import GetDiary
from ..model.diary import Diary
router = APIRouter(tags=["diary"])
@router.get("", response_model=Diary)
async def get_diary(
request: Request,
date: date,
controller: GetDiary = Depends(GetDiary),
):
return await controller.call(date)

View file

@ -1,34 +0,0 @@
from fastapi import APIRouter, Depends, Request
from ..controller.entry import CreateEntry, DeleteEntry, UpdateEntry
from ..model.entry import CreateEntryPayload, Entry, UpdateEntryPayload
router = APIRouter(tags=["entry"])
@router.post("", response_model=Entry)
async def create_entry(
request: Request,
data: CreateEntryPayload,
contoller: CreateEntry = Depends(CreateEntry),
):
return await contoller.call(data)
@router.patch("/{entry_id}", response_model=Entry)
async def update_entry(
request: Request,
entry_id: int,
data: UpdateEntryPayload,
contoller: UpdateEntry = Depends(UpdateEntry),
):
return await contoller.call(entry_id, data)
@router.delete("/{entry_id}")
async def delete_entry(
request: Request,
entry_id: int,
contoller: DeleteEntry = Depends(DeleteEntry),
):
return await contoller.call(entry_id)

View file

@ -1,60 +0,0 @@
from fastapi import APIRouter, Depends, Request
from ..controller.meal import CreateMeal, CreateMealFromPreset, DeleteMeal, RenameMeal, SaveMeal
from ..model.meal import (
CreateMealFromPresetPayload,
CreateMealPayload,
Meal,
RenameMealPayload,
SaveMealPayload,
)
from ..model.preset import Preset
router = APIRouter(tags=["meal"])
@router.post("", response_model=Meal)
async def create_meal(
request: Request,
data: CreateMealPayload,
contoller: CreateMeal = Depends(CreateMeal),
):
return await contoller.call(data)
@router.post("/{meal_id}/save", response_model=Preset)
async def save_meal(
request: Request,
meal_id: int,
data: SaveMealPayload,
contoller: SaveMeal = Depends(SaveMeal),
):
return await contoller.call(meal_id, data)
@router.patch("/{meal_id}", response_model=Meal)
async def rename_meal(
request: Request,
meal_id: int,
data: RenameMealPayload,
contoller: RenameMeal = Depends(RenameMeal),
):
return await contoller.call(meal_id, data)
@router.delete("/{meal_id}")
async def delete_meal(
request: Request,
meal_id: int,
contoller: DeleteMeal = Depends(DeleteMeal),
):
return await contoller.call(meal_id)
@router.post("/from_preset", response_model=Meal)
async def create_meal_from_preset(
request: Request,
data: CreateMealFromPresetPayload,
contoller: CreateMealFromPreset = Depends(CreateMealFromPreset),
):
return await contoller.call(data)

View file

@ -1,37 +0,0 @@
from fastapi import APIRouter, Depends, Request
from ..controller.preset import DeletePreset, GetPreset, ListPresets
from ..model.preset import ListPresetsPayload, PresetDetails
router = APIRouter(tags=["preset"])
@router.get("", response_model=ListPresetsPayload)
async def list_presets(
request: Request,
limit: int = 10,
offset: int = 0,
q: str | None = None,
controller: ListPresets = Depends(ListPresets),
):
return ListPresetsPayload(
presets=[p async for p in controller.call(limit=limit, offset=offset, q=q)]
)
@router.get("/{preset_id}", response_model=PresetDetails)
async def get_preset(
request: Request,
preset_id: int,
controller: GetPreset = Depends(GetPreset),
):
return await controller.call(preset_id)
@router.delete("/{preset_id}")
async def delete_preset(
request: Request,
preset_id: int,
controller: DeletePreset = Depends(DeletePreset),
):
await controller.call(preset_id)

View file

@ -1,13 +0,0 @@
from fastapi import APIRouter, Depends, Request
from ..controller.tasks import CacheProductUsageData
router = APIRouter(prefix="/api", tags=["tasks"])
@router.post("/cache_product_usage_data")
async def cache_product_usage_data(
request: Request,
contoller: CacheProductUsageData = Depends(CacheProductUsageData),
):
return await contoller.call()

View file

@ -1,15 +0,0 @@
from fastapi import APIRouter, Depends, Request
from fooder.controller.user import CreateUser
from fooder.model.user import CreateUserPayload, User
router = APIRouter(tags=["user"])
@router.post("", response_model=User)
async def create_user(
request: Request,
data: CreateUserPayload,
contoller: CreateUser = Depends(CreateUser),
):
return await contoller.call(data)

View file

@ -10,6 +10,3 @@ platform = linux
warn_unused_configs = True warn_unused_configs = True
warn_unused_ignores = True warn_unused_ignores = True
[mypy-fooder.controller.*]
disable_error_code=override