Add training executor for SQL requests
This commit is contained in:
parent
4aa8b7a5c3
commit
d0407e3462
14 changed files with 167 additions and 52 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
36
app/core/database/connection.py
Normal file
36
app/core/database/connection.py
Normal 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
|
||||
0
app/core/database/models/__init__.py
Normal file
0
app/core/database/models/__init__.py
Normal file
18
app/core/database/models/training.py
Normal file
18
app/core/database/models/training.py
Normal 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
0
app/core/dto/__init__.py
Normal file
25
app/core/dto/training.py
Normal file
25
app/core/dto/training.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
37
migrations/sql/0003_fix_trainings_id.sql
Normal file
37
migrations/sql/0003_fix_trainings_id.sql
Normal 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');
|
||||
Loading…
Reference in a new issue