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 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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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 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)
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
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