diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..737d084 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,86 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +F1tness Parser is a FastAPI-based web application that parses fitness training data from two note formats: +- **Obsidian Notes**: Markdown tables with `# DD.MM.YYYY (trainer-session)` headers +- **Apple Notes**: Bold markdown tables with `**DD.MM.YYYY (trainer-session)**` headers + +The application uses a layered architecture with parsers, models, services, and API endpoints. + +## Development Commands + +### Local Development +```bash +# Start development server +python run.py + +# Start with Docker Compose (includes PostgreSQL) +docker compose up --build + +# Run migrations manually +python -c "from migrations.runner import MigrationRunner; from app.config import settings; import asyncio; asyncio.run(MigrationRunner(settings.get_postgres_database_url()).run_migrations())" +``` + +### Testing +```bash +# Run tests +pytest + +# Run specific test file +pytest tests/test_core/test_obsidian_parser.py + +# Run tests with coverage +pytest --cov=app +``` + +### Type Checking +```bash +# Run type checking +mypy app/ +``` + +## Architecture + +### Core Components + +**Parser Architecture**: The system uses a base parser class (`app/core/parsers/base.py`) with format-specific implementations: +- `ObsidianNotesParser` - handles Obsidian markdown format +- `AppleNotesParser` - handles Apple notes format +- Each parser includes exercise name mapping for normalization + +**Data Models**: Located in `app/core/dto/training.py`: +- `Training` - represents a workout session +- `Exercise` - individual exercise with multiple approaches +- `Approach` - single set with weight and reps + +**API Structure**: +- `/api/v1/` - REST API endpoints +- `/app/` - Web interface endpoints +- Static files served from `app/static/` +- Templates in `app/templates/` + +### Database + +The application supports both SQLite (default) and PostgreSQL. Database migrations are handled by a custom migration runner in `migrations/runner.py` that reads SQL files from `migrations/sql/`. + +Configuration is managed through `app/config.py` using Pydantic settings with `.env` file support. + +### Key Files + +- `app/main.py` - FastAPI application setup with lifespan management +- `app/config.py` - Application configuration and database settings +- `run.py` - Development server entry point +- `migrations/runner.py` - Custom database migration system +- `compose.yaml` - Docker Compose setup with PostgreSQL + +## Environment Setup + +Copy `.env.example` to `.env` and configure: +- PostgreSQL connection settings +- Debug mode +- PYTHONPATH (for Docker) + +The application will run database migrations automatically on startup through the FastAPI lifespan event. diff --git a/app/api/v1/endpoints/trainings.py b/app/api/v1/endpoints/trainings.py index 0c168c0..07b8d79 100644 --- a/app/api/v1/endpoints/trainings.py +++ b/app/api/v1/endpoints/trainings.py @@ -1,8 +1,11 @@ from datetime import datetime +import logging from fastapi import APIRouter from app.core.database.connection import SQLiteExecutor +from app.core.database.models.approach import ApproachOps from app.core.database.models.training import TrainingOps +from app.core.database.models.exercise import ExerciseOps from app.core.dto.training import TrainingDTO from app.core.parsers.obsidian import parse_training_data @@ -30,3 +33,30 @@ async def get_trainings_list(): training_executor = TrainingOps(sqllite_executor) results = await training_executor.list() return {"status": "ok", "create_id": results} + + +@router.delete("/test/delete/") +async def delete_by_ids(): + training_executor = TrainingOps(sqllite_executor) + await training_executor.delete(list_of_ids=[4,5,6]) + return {"status": "ok"} + + +@router.post("/test/create/") +async def create_full_training(): + training_executor = TrainingOps(sqllite_executor) + exercise_executor = ExerciseOps(sqllite_executor) + approach_executor = ApproachOps(sqllite_executor) + sample_training_data: TrainingDTO = parse_training_data()[-1] + logging.info(sample_training_data) + training_id: int = await training_executor.create(sample_training_data) + if sample_training_data.exercises: + for exercise in sample_training_data.exercises: + exercise.training_id = training_id + exercise_id: int = await exercise_executor.create(exercise) + if exercise.approaches: + for approach in exercise.approaches: + approach.exercise_id = exercise_id + await approach_executor.create(approach) + + return {"status": "ok"} diff --git a/app/core/database/models/approach.py b/app/core/database/models/approach.py new file mode 100644 index 0000000..3cfcb19 --- /dev/null +++ b/app/core/database/models/approach.py @@ -0,0 +1,26 @@ +from typing import List, Dict, Any +from app.core.database.connection import SQLiteExecutor +from app.core.dto.training import ApproachDTO + + +class ApproachOps: + TABLE_NAME: str = "approaches" + + def __init__(self, executor: SQLiteExecutor) -> None: + self._executor = executor + + async def create(self, approach: ApproachDTO) -> int: + query = f"INSERT INTO {self.TABLE_NAME} (exercise_id, weight, reps) VALUES (?, ?, ?)" + return await self._executor.execute_mod_query( + query, (approach.exercise_id, approach.weight, approach.reps) + ) + + async def list(self) -> List[Dict[str, Any]]: + query = f"SELECT * FROM {self.TABLE_NAME}" + return await self._executor.execute_query(query) + + async def delete(self, list_of_ids: List[int]) -> None: + placeholders: str = ",".join("?" * len(list_of_ids)) + query = f"DELETE FROM {self.TABLE_NAME} WHERE id IN ({placeholders})" + await self._executor.execute_mod_query(query, tuple(list_of_ids)) + return None diff --git a/app/core/database/models/exercise.py b/app/core/database/models/exercise.py new file mode 100644 index 0000000..a453799 --- /dev/null +++ b/app/core/database/models/exercise.py @@ -0,0 +1,26 @@ +from typing import List, Dict, Any +from app.core.database.connection import SQLiteExecutor +from app.core.dto.training import ExerciseDTO + + +class ExerciseOps: + TABLE_NAME: str = "exercises" + + def __init__(self, executor: SQLiteExecutor) -> None: + self._executor = executor + + async def create(self, exercise: ExerciseDTO) -> int: + query = f"INSERT INTO {self.TABLE_NAME} (training_id, name, splitted_weight) VALUES (?, ?, ?)" + return await self._executor.execute_mod_query( + query, (exercise.training_id, exercise.name, exercise.splitted_weight) + ) + + async def list(self) -> List[Dict[str, Any]]: + query = f"SELECT * FROM {self.TABLE_NAME}" + return await self._executor.execute_query(query) + + async def delete(self, list_of_ids: List[int]) -> None: + placeholders: str = ",".join("?" * len(list_of_ids)) + query = f"DELETE FROM {self.TABLE_NAME} WHERE id IN ({placeholders})" + await self._executor.execute_mod_query(query, tuple(list_of_ids)) + return None diff --git a/app/core/database/models/training.py b/app/core/database/models/training.py index 80a7d7a..5ea30c3 100644 --- a/app/core/database/models/training.py +++ b/app/core/database/models/training.py @@ -4,15 +4,23 @@ from app.core.dto.training import TrainingDTO class TrainingOps: + TABLE_NAME: str = "trainings" + def __init__(self, executor: SQLiteExecutor) -> None: self._executor = executor async def create(self, training: TrainingDTO) -> int: - query = "INSERT INTO trainings (date, trainer) VALUES (?, ?)" + query = f"INSERT INTO {self.TABLE_NAME} (date, trainer) VALUES (?, ?)" return await self._executor.execute_mod_query( query, (training.date, training.trainer) ) async def list(self) -> List[Dict[str, Any]]: - query = "SELECT * FROM trainings" + query = f"SELECT * FROM {self.TABLE_NAME}" return await self._executor.execute_query(query) + + async def delete(self, list_of_ids: List[int]) -> None: + placeholders: str = ",".join("?" * len(list_of_ids)) + query = f"DELETE FROM {self.TABLE_NAME} WHERE id IN ({placeholders})" + await self._executor.execute_mod_query(query, tuple(list_of_ids)) + return None diff --git a/app/core/dto/training.py b/app/core/dto/training.py index c853add..6900456 100644 --- a/app/core/dto/training.py +++ b/app/core/dto/training.py @@ -22,4 +22,4 @@ class TrainingDTO(BaseModel): id: Optional[int] = None date: date trainer: Optional[str] = None - exercises: Optional[List[ExerciseDTO]] = None + exercises: Optional[List[ApproachDTO]] = None