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 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
sqllite_executor = SQLiteExecutor()
router = APIRouter()
@router.get("/obsidian/")
async def obsidian_trainings_list():
return {
"data": parse_training_data()
}
return {"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"
debug: bool = False
# TODO: Add sqlite 3 settings
# SQLite database settings
SQLITE_DATABASE_PATH: str = "fitness.db"
# Postgres database settings
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 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.base import BaseNotesParser
@ -47,18 +47,18 @@ class AppleNotesParser(BaseNotesParser):
return True, date, trainer, year_count
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."""
try:
return Training(
return TrainingDTO(
date=datetime.strptime(date_str, "%d.%m.%Y").date(), exercises=[]
)
except ValueError:
return Training(
return TrainingDTO(
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."""
# Override the data file reading with direct string input
original_method = self.read_data_file
@ -72,13 +72,13 @@ class AppleNotesParser(BaseNotesParser):
self.read_data_file = original_method
def parse_training_data() -> List[Training]:
def parse_training_data() -> List[TrainingDTO]:
"""Parse Apple Notes training data."""
parser = AppleNotesParser()
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())."""
parser = AppleNotesParser()
return parser.apply_exercise_mapping(apple_trainings)

View file

@ -1,7 +1,7 @@
import os
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
@ -25,7 +25,7 @@ class BaseNotesParser:
content = f.read()
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."""
reps_list: List[int] = [int(rep) for rep in reps.split("-")]
weight_splitted: bool = False
@ -45,24 +45,24 @@ class BaseNotesParser:
approaches = []
if not weight:
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)
else:
weight_pointer = 0
for rep_index in range(0, len(reps_list)):
approach = Approach(
approach = ApproachDTO(
weight=weight_list[weight_pointer], reps=reps_list[rep_index]
)
if rep_index < len(weight_list) - 1:
weight_pointer += 1
approaches.append(approach)
exercise = Exercise(
exercise = ExerciseDTO(
name=name, approaches=approaches, splitted_weight=weight_splitted
)
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."""
stripped: List[str] = [entry.strip() for entry in exercise_line.split("|")][
1:-1
@ -90,11 +90,11 @@ class BaseNotesParser:
"""Parse training header. Override in subclasses for specific formats."""
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."""
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."""
training_data = self.filter_training_data(
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]
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."""
for training in trainings:
if not training or not training.exercises:
@ -133,7 +133,7 @@ class BaseNotesParser:
exercise.name = mapped_name
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."""
trainings = self.parse_training_data()
return self.apply_exercise_mapping(trainings)

View file

@ -1,7 +1,7 @@
import re
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.base import BaseNotesParser
@ -35,7 +35,7 @@ class ObsidianNotesParser(BaseNotesParser):
return True, date, trainer, year_count
return False, "", "", ""
def parse(self, data: str) -> List[Training]:
def parse(self, data: str) -> List[TrainingDTO]:
"""Parse Obsidian training data from string input."""
# Override the data file reading with direct string input
original_method = self.read_data_file
@ -49,13 +49,13 @@ class ObsidianNotesParser(BaseNotesParser):
self.read_data_file = original_method
def parse_training_data() -> List[Training]:
def parse_training_data() -> List[TrainingDTO]:
"""Parse Obsidian Notes training data."""
parser = ObsidianNotesParser()
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())."""
parser = ObsidianNotesParser()
return parser.apply_exercise_mapping(obsidian_trainings)

View file

@ -1,9 +1,9 @@
from typing import List
from app.core.parsers.apple import AppleNotesParser
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
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');