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
|
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"}
|
||||||
|
|
|
||||||
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:
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue