Add training creation mechanism
This commit is contained in:
parent
d0407e3462
commit
ff59ae3ee9
6 changed files with 179 additions and 3 deletions
86
CLAUDE.md
Normal file
86
CLAUDE.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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"}
|
||||
|
|
|
|||
26
app/core/database/models/approach.py
Normal file
26
app/core/database/models/approach.py
Normal file
|
|
@ -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
|
||||
26
app/core/database/models/exercise.py
Normal file
26
app/core/database/models/exercise.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue