diff --git a/app/api/v1/endpoints/trainings.py b/app/api/v1/endpoints/trainings.py index 4c3d7b6..0c168c0 100644 --- a/app/api/v1/endpoints/trainings.py +++ b/app/api/v1/endpoints/trainings.py @@ -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} diff --git a/app/config.py b/app/config.py index 6086eeb..7e2f71c 100644 --- a/app/config.py +++ b/app/config.py @@ -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 diff --git a/app/core/models/__init__.py b/app/core/database/__init__.py similarity index 100% rename from app/core/models/__init__.py rename to app/core/database/__init__.py diff --git a/app/core/database/connection.py b/app/core/database/connection.py new file mode 100644 index 0000000..97d508a --- /dev/null +++ b/app/core/database/connection.py @@ -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 diff --git a/app/core/database/models/__init__.py b/app/core/database/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/database/models/training.py b/app/core/database/models/training.py new file mode 100644 index 0000000..80a7d7a --- /dev/null +++ b/app/core/database/models/training.py @@ -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) diff --git a/app/core/dto/__init__.py b/app/core/dto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/dto/training.py b/app/core/dto/training.py new file mode 100644 index 0000000..c853add --- /dev/null +++ b/app/core/dto/training.py @@ -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 diff --git a/app/core/models/training.py b/app/core/models/training.py deleted file mode 100644 index d429b64..0000000 --- a/app/core/models/training.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/core/parsers/apple.py b/app/core/parsers/apple.py index c655504..96075f5 100644 --- a/app/core/parsers/apple.py +++ b/app/core/parsers/apple.py @@ -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) \ No newline at end of file + return parser.apply_exercise_mapping(apple_trainings) diff --git a/app/core/parsers/base.py b/app/core/parsers/base.py index c8e960a..4b10e6a 100644 --- a/app/core/parsers/base.py +++ b/app/core/parsers/base.py @@ -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) diff --git a/app/core/parsers/obsidian.py b/app/core/parsers/obsidian.py index 8cee048..d6ed8b1 100644 --- a/app/core/parsers/obsidian.py +++ b/app/core/parsers/obsidian.py @@ -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) diff --git a/app/core/parsers/text_data.py b/app/core/parsers/text_data.py index 13dcfaa..294217c 100644 --- a/app/core/parsers/text_data.py +++ b/app/core/parsers/text_data.py @@ -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: diff --git a/migrations/sql/0003_fix_trainings_id.sql b/migrations/sql/0003_fix_trainings_id.sql new file mode 100644 index 0000000..d5bb969 --- /dev/null +++ b/migrations/sql/0003_fix_trainings_id.sql @@ -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');