Add training creation mechanism

This commit is contained in:
t0xa 2025-10-13 23:32:10 +03:00
parent d0407e3462
commit ff59ae3ee9
6 changed files with 179 additions and 3 deletions

86
CLAUDE.md Normal file
View 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.

View file

@ -1,8 +1,11 @@
from datetime import datetime from datetime import datetime
import logging
from fastapi import APIRouter from fastapi import APIRouter
from app.core.database.connection import SQLiteExecutor 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.training import TrainingOps
from app.core.database.models.exercise import ExerciseOps
from app.core.dto.training import TrainingDTO from app.core.dto.training import TrainingDTO
from app.core.parsers.obsidian import parse_training_data from app.core.parsers.obsidian import parse_training_data
@ -30,3 +33,30 @@ async def get_trainings_list():
training_executor = TrainingOps(sqllite_executor) training_executor = TrainingOps(sqllite_executor)
results = await training_executor.list() results = await training_executor.list()
return {"status": "ok", "create_id": results} 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"}

View 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

View 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

View file

@ -4,15 +4,23 @@ from app.core.dto.training import TrainingDTO
class TrainingOps: class TrainingOps:
TABLE_NAME: str = "trainings"
def __init__(self, executor: SQLiteExecutor) -> None: def __init__(self, executor: SQLiteExecutor) -> None:
self._executor = executor self._executor = executor
async def create(self, training: TrainingDTO) -> int: 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( return await self._executor.execute_mod_query(
query, (training.date, training.trainer) query, (training.date, training.trainer)
) )
async def list(self) -> List[Dict[str, Any]]: 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) 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

View file

@ -22,4 +22,4 @@ class TrainingDTO(BaseModel):
id: Optional[int] = None id: Optional[int] = None
date: date date: date
trainer: Optional[str] = None trainer: Optional[str] = None
exercises: Optional[List[ExerciseDTO]] = None exercises: Optional[List[ApproachDTO]] = None