Add training executor for SQL requests

This commit is contained in:
t0xa 2025-10-12 20:42:10 +03:00
parent 4aa8b7a5c3
commit d0407e3462
14 changed files with 167 additions and 52 deletions

View file

@ -1,11 +1,32 @@
from datetime import datetime
from fastapi import APIRouter from fastapi import APIRouter
from app.core.database.connection import SQLiteExecutor
from app.core.database.models.training import TrainingOps
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
sqllite_executor = SQLiteExecutor()
router = APIRouter() router = APIRouter()
@router.get("/obsidian/") @router.get("/obsidian/")
async def obsidian_trainings_list(): async def obsidian_trainings_list():
return { return {"data": parse_training_data()}
"data": parse_training_data()
}
@router.get("/test/create/")
async def create_sample_training():
test_training = TrainingDTO(date=datetime.now().date(), trainer="Stepka")
training_executor = TrainingOps(sqllite_executor)
result_id = await training_executor.create(test_training)
return {"status": "ok", "create_id": result_id}
@router.get("/test/get/list/")
async def get_trainings_list():
training_executor = TrainingOps(sqllite_executor)
results = await training_executor.list()
return {"status": "ok", "create_id": results}

View file

@ -9,7 +9,8 @@ class Settings(BaseSettings):
version: str = "0.0.1" version: str = "0.0.1"
debug: bool = False debug: bool = False
# TODO: Add sqlite 3 settings # SQLite database settings
SQLITE_DATABASE_PATH: str = "fitness.db"
# Postgres database settings # Postgres database settings
POSTGRES_DATABASE_URL: Optional[str] = None POSTGRES_DATABASE_URL: Optional[str] = None

View file

@ -0,0 +1,36 @@
from typing import Dict, List, Any
import aiosqlite
from app.config import settings
class SQLiteExecutor:
"""Executor для SQL запросов в БД SQLlite"""
def __init__(self) -> None:
self._db_path = settings.SQLITE_DATABASE_PATH
async def execute_query(
self, query: str, params: tuple = ()
) -> List[Dict[str, Any]]:
"""Выполнить запрос, который не должен вносить изменения в БД.
:param query: SQL запрос
:param params: Параметры, которые нужно передать вместе с запросом
:return: Список, выдаваемый aiosqlite
"""
async with aiosqlite.connect(self._db_path) as db:
db.row_factory = aiosqlite.Row
cursor = await db.execute(query, params)
rows = await cursor.fetchall()
return [dict(row) for row in rows]
async def execute_mod_query(self, query: str, params: tuple = ()) -> int:
"""Выполнить запрос, который что-то изменяет в БД.
:param query: SQL запрос
:param params: Параметры, которые нужно передать вместе с запросом
:return: Список, выдаваемый aiosqlite
"""
async with aiosqlite.connect(self._db_path) as db:
cursor = await db.execute(query, params)
await db.commit()
return cursor.lastrowid if cursor.lastrowid else cursor.rowcount

View file

View file

@ -0,0 +1,18 @@
from typing import List, Dict, Any
from app.core.database.connection import SQLiteExecutor
from app.core.dto.training import TrainingDTO
class TrainingOps:
def __init__(self, executor: SQLiteExecutor) -> None:
self._executor = executor
async def create(self, training: TrainingDTO) -> int:
query = "INSERT INTO trainings (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"
return await self._executor.execute_query(query)

0
app/core/dto/__init__.py Normal file
View file

25
app/core/dto/training.py Normal file
View file

@ -0,0 +1,25 @@
from typing import List, Optional
from pydantic import BaseModel
from datetime import date
class ApproachDTO(BaseModel):
id: Optional[int] = None
exercise_id: Optional[int] = None
weight: float
reps: int
class ExerciseDTO(BaseModel):
id: Optional[int] = None
training_id: Optional[int] = None
name: str
splitted_weight: bool = False
approaches: Optional[List[ApproachDTO]] = None
class TrainingDTO(BaseModel):
id: Optional[int] = None
date: date
trainer: Optional[str] = None
exercises: Optional[List[ExerciseDTO]] = None

View file

@ -1,23 +0,0 @@
from typing import List, Optional
from pydantic import BaseModel
from datetime import date
class Approach(BaseModel):
weight: float
reps: int
class Exercise(BaseModel):
name: str
splitted_weight: bool = False
approaches: List[Approach]
class Training(BaseModel):
date: date
exercises: Optional[List[Exercise]]
class Coach(BaseModel):
name: str

View file

@ -2,7 +2,7 @@ import re
from typing import List, Tuple from typing import List, Tuple
from datetime import datetime from datetime import datetime
from app.core.models.training import Training from app.core.dto.training import TrainingDTO
from app.core.parsers.apple_mapper import unique_apple_exercises_mapper from app.core.parsers.apple_mapper import unique_apple_exercises_mapper
from app.core.parsers.base import BaseNotesParser from app.core.parsers.base import BaseNotesParser
@ -47,18 +47,18 @@ class AppleNotesParser(BaseNotesParser):
return True, date, trainer, year_count return True, date, trainer, year_count
return False, "", "", "" return False, "", "", ""
def create_training_from_date(self, date_str: str) -> Training: def create_training_from_date(self, date_str: str) -> TrainingDTO:
"""Create Training object from date string with fallback parsing.""" """Create Training object from date string with fallback parsing."""
try: try:
return Training( return TrainingDTO(
date=datetime.strptime(date_str, "%d.%m.%Y").date(), exercises=[] date=datetime.strptime(date_str, "%d.%m.%Y").date(), exercises=[]
) )
except ValueError: except ValueError:
return Training( return TrainingDTO(
date=datetime.strptime(date_str, "%d.%m.%y").date(), exercises=[] date=datetime.strptime(date_str, "%d.%m.%y").date(), exercises=[]
) )
def parse(self, data: str) -> List[Training]: def parse(self, data: str) -> List[TrainingDTO]:
"""Parse Apple Notes training data from string input.""" """Parse Apple Notes training data from string input."""
# Override the data file reading with direct string input # Override the data file reading with direct string input
original_method = self.read_data_file original_method = self.read_data_file
@ -72,13 +72,13 @@ class AppleNotesParser(BaseNotesParser):
self.read_data_file = original_method self.read_data_file = original_method
def parse_training_data() -> List[Training]: def parse_training_data() -> List[TrainingDTO]:
"""Parse Apple Notes training data.""" """Parse Apple Notes training data."""
parser = AppleNotesParser() parser = AppleNotesParser()
return parser.parse_training_data() return parser.parse_training_data()
def remap_unique_exercises(apple_trainings: List[Training]) -> List[Training]: def remap_unique_exercises(apple_trainings: List[TrainingDTO]) -> List[TrainingDTO]:
"""Remap exercise names using Apple-specific mapping (deprecated - use parser.parse_and_map_training_data()).""" """Remap exercise names using Apple-specific mapping (deprecated - use parser.parse_and_map_training_data())."""
parser = AppleNotesParser() parser = AppleNotesParser()
return parser.apply_exercise_mapping(apple_trainings) return parser.apply_exercise_mapping(apple_trainings)

View file

@ -1,7 +1,7 @@
import os import os
from typing import List, Tuple, Dict, Optional from typing import List, Tuple, Dict, Optional
from app.core.models.training import Approach, Exercise, Training from app.core.dto.training import ApproachDTO, ExerciseDTO, TrainingDTO
from app.core.utils.date_refactor import parse_training_date from app.core.utils.date_refactor import parse_training_date
@ -25,7 +25,7 @@ class BaseNotesParser:
content = f.read() content = f.read()
return content return content
def serialize_exercise(self, reps: str, weight: str, name: str) -> Exercise: def serialize_exercise(self, reps: str, weight: str, name: str) -> ExerciseDTO:
"""Convert raw exercise data into Exercise object with approaches.""" """Convert raw exercise data into Exercise object with approaches."""
reps_list: List[int] = [int(rep) for rep in reps.split("-")] reps_list: List[int] = [int(rep) for rep in reps.split("-")]
weight_splitted: bool = False weight_splitted: bool = False
@ -45,24 +45,24 @@ class BaseNotesParser:
approaches = [] approaches = []
if not weight: if not weight:
for rep_index in range(0, len(reps_list)): for rep_index in range(0, len(reps_list)):
approach = Approach(weight=0.0, reps=reps_list[rep_index]) approach = ApproachDTO(weight=0.0, reps=reps_list[rep_index])
approaches.append(approach) approaches.append(approach)
else: else:
weight_pointer = 0 weight_pointer = 0
for rep_index in range(0, len(reps_list)): for rep_index in range(0, len(reps_list)):
approach = Approach( approach = ApproachDTO(
weight=weight_list[weight_pointer], reps=reps_list[rep_index] weight=weight_list[weight_pointer], reps=reps_list[rep_index]
) )
if rep_index < len(weight_list) - 1: if rep_index < len(weight_list) - 1:
weight_pointer += 1 weight_pointer += 1
approaches.append(approach) approaches.append(approach)
exercise = Exercise( exercise = ExerciseDTO(
name=name, approaches=approaches, splitted_weight=weight_splitted name=name, approaches=approaches, splitted_weight=weight_splitted
) )
return exercise return exercise
def parse_training_exercises(self, exercise_line: str) -> Exercise: def parse_training_exercises(self, exercise_line: str) -> ExerciseDTO:
"""Parse exercise data from a table row.""" """Parse exercise data from a table row."""
stripped: List[str] = [entry.strip() for entry in exercise_line.split("|")][ stripped: List[str] = [entry.strip() for entry in exercise_line.split("|")][
1:-1 1:-1
@ -90,11 +90,11 @@ class BaseNotesParser:
"""Parse training header. Override in subclasses for specific formats.""" """Parse training header. Override in subclasses for specific formats."""
raise NotImplementedError("Subclasses must implement parse_training_header") raise NotImplementedError("Subclasses must implement parse_training_header")
def create_training_from_date(self, date_str: str) -> Training: def create_training_from_date(self, date_str: str) -> TrainingDTO:
"""Create Training object from date string using utility function.""" """Create Training object from date string using utility function."""
return Training(date=parse_training_date(date_str), exercises=[]) return TrainingDTO(date=parse_training_date(date_str), exercises=[])
def parse_training_data(self) -> List[Training]: def parse_training_data(self) -> List[TrainingDTO]:
"""Main parsing method. Override for specific parsing logic.""" """Main parsing method. Override for specific parsing logic."""
training_data = self.filter_training_data( training_data = self.filter_training_data(
self.read_data_file(self.data_file_name) self.read_data_file(self.data_file_name)
@ -120,7 +120,7 @@ class BaseNotesParser:
return [train for train in trains if train is not None] return [train for train in trains if train is not None]
def apply_exercise_mapping(self, trainings: List[Training]) -> List[Training]: def apply_exercise_mapping(self, trainings: List[TrainingDTO]) -> List[TrainingDTO]:
"""Apply exercise name mapping to all trainings.""" """Apply exercise name mapping to all trainings."""
for training in trainings: for training in trainings:
if not training or not training.exercises: if not training or not training.exercises:
@ -133,7 +133,7 @@ class BaseNotesParser:
exercise.name = mapped_name exercise.name = mapped_name
return trainings return trainings
def parse_and_map_training_data(self) -> List[Training]: def parse_and_map_training_data(self) -> List[TrainingDTO]:
"""Parse training data and apply exercise mapping.""" """Parse training data and apply exercise mapping."""
trainings = self.parse_training_data() trainings = self.parse_training_data()
return self.apply_exercise_mapping(trainings) return self.apply_exercise_mapping(trainings)

View file

@ -1,7 +1,7 @@
import re import re
from typing import List, Tuple from typing import List, Tuple
from app.core.models.training import Training from app.core.dto.training import TrainingDTO
from app.core.parsers.obsidian_mapper import obsidian_unique_exercies_mapping from app.core.parsers.obsidian_mapper import obsidian_unique_exercies_mapping
from app.core.parsers.base import BaseNotesParser from app.core.parsers.base import BaseNotesParser
@ -35,7 +35,7 @@ class ObsidianNotesParser(BaseNotesParser):
return True, date, trainer, year_count return True, date, trainer, year_count
return False, "", "", "" return False, "", "", ""
def parse(self, data: str) -> List[Training]: def parse(self, data: str) -> List[TrainingDTO]:
"""Parse Obsidian training data from string input.""" """Parse Obsidian training data from string input."""
# Override the data file reading with direct string input # Override the data file reading with direct string input
original_method = self.read_data_file original_method = self.read_data_file
@ -49,13 +49,13 @@ class ObsidianNotesParser(BaseNotesParser):
self.read_data_file = original_method self.read_data_file = original_method
def parse_training_data() -> List[Training]: def parse_training_data() -> List[TrainingDTO]:
"""Parse Obsidian Notes training data.""" """Parse Obsidian Notes training data."""
parser = ObsidianNotesParser() parser = ObsidianNotesParser()
return parser.parse_training_data() return parser.parse_training_data()
def remap_unique_exercises(obsidian_trainings: List[Training]) -> List[Training]: def remap_unique_exercises(obsidian_trainings: List[TrainingDTO]) -> List[TrainingDTO]:
"""Remap exercise names using Obsidian-specific mapping (deprecated - use parser.parse_and_map_training_data()).""" """Remap exercise names using Obsidian-specific mapping (deprecated - use parser.parse_and_map_training_data())."""
parser = ObsidianNotesParser() parser = ObsidianNotesParser()
return parser.apply_exercise_mapping(obsidian_trainings) return parser.apply_exercise_mapping(obsidian_trainings)

View file

@ -1,9 +1,9 @@
from typing import List from typing import List
from app.core.parsers.apple import AppleNotesParser from app.core.parsers.apple import AppleNotesParser
from app.core.parsers.obsidian import ObsidianNotesParser from app.core.parsers.obsidian import ObsidianNotesParser
from app.core.models.training import Training from app.core.dto.training import TrainingDTO
def parse_old_data() -> List[Training]: def parse_old_data() -> List[TrainingDTO]:
"""Method for parsing all old data from apple and obsidian notes with exercise mapping applied """Method for parsing all old data from apple and obsidian notes with exercise mapping applied
Returns: Returns:

View file

@ -0,0 +1,37 @@
-- Migration: 0003_fix_trainings_id
-- Description: Fix trainings table to use proper SQLite autoincrement ID
-- Drop the existing trainings table (data will be lost)
DROP TABLE IF EXISTS trainings;
-- Recreate with proper SQLite syntax
CREATE TABLE trainings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date DATE NOT NULL,
trainer VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Also fix exercises table if it exists
DROP TABLE IF EXISTS exercises;
CREATE TABLE exercises (
id INTEGER PRIMARY KEY AUTOINCREMENT,
training_id INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
splitted_weight BOOLEAN DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (training_id) REFERENCES trainings(id) ON DELETE CASCADE
);
-- Fix approaches table if it exists
DROP TABLE IF EXISTS approaches;
CREATE TABLE approaches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
exercise_id INTEGER NOT NULL,
weight REAL,
reps INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (exercise_id) REFERENCES exercises(id) ON DELETE CASCADE
);
INSERT INTO schema_migrations (version) VALUES ('0003_fix_trainings_id');