Inject exercise mapper to parser

This commit is contained in:
t0xa 2025-08-31 15:07:55 +03:00
parent 740244a04d
commit 4b93c5a9a0
6 changed files with 88 additions and 56 deletions

View file

@ -11,7 +11,7 @@ class AppleNotesParser(BaseNotesParser):
"""Parser for Apple Notes format training data."""
def __init__(self):
super().__init__("apple.md")
super().__init__("apple.md", unique_apple_exercises_mapper)
def filter_training_data(self, training_data: str) -> str:
"""Filter Apple-specific training data format."""
@ -66,15 +66,6 @@ def parse_training_data() -> List[Training]:
def remap_unique_exercises(apple_trainings: List[Training]) -> List[Training]:
"""Remap exercise names using Apple-specific mapping."""
for apple_training in apple_trainings:
if not apple_training or not apple_training.exercises:
continue
for apple_exercise in apple_training.exercises:
if not apple_exercise:
continue
print(f"{apple_training.date} : {apple_exercise}")
mapped_name = unique_apple_exercises_mapper.get(apple_exercise.name)
if mapped_name is not None:
apple_exercise.name = mapped_name
return apple_trainings
"""Remap exercise names using Apple-specific mapping (deprecated - use parser.parse_and_map_training_data())."""
parser = AppleNotesParser()
return parser.apply_exercise_mapping(apple_trainings)

13
main.py
View file

@ -1,4 +1,17 @@
from parsers.text_data_parser import parse_old_data
from collections import Counter
text_trainings = parse_old_data()
for training in text_trainings:
print(f"{training.date} :: {[ex.name for ex in training.exercises]}")
print(len(text_trainings))
exercise_names = []
for training in text_trainings:
for exercise in training.exercises:
exercise_names.append(exercise.name)
exercise_counts = Counter(exercise_names)
print("\nExercise counts:")
for name, count in sorted(exercise_counts.items()):
print(f"{name}: {count}")

View file

@ -40,6 +40,7 @@ list_of_unique_names = [
"Жим от груди сидя в тренажере (рычаги)",
"Жим от плеч вверх в тренажере (рычаги)",
"Икроножные сидя",
"Пресс на наклонной скамье"
]
@ -152,5 +153,6 @@ obsidian_unique_exercies_mapping = {
"Тяга горизонтального блока": "Тяга горизонтального блока",
"Тяга горизонтального блока 1 рукой поочерёдно": "Тяга горизонтального блока (по очереди)",
"Тяга горизонтального блока одной рукой": "Тяга горизонтального блока (по очереди)",
"Пресс на скамье": "Пресс на наклонной скамье"
}

View file

@ -10,7 +10,7 @@ class ObsidianNotesParser(BaseNotesParser):
"""Parser for Obsidian Notes format training data."""
def __init__(self):
super().__init__("obsidian.md")
super().__init__("obsidian.md", obsidian_unique_exercies_mapping)
def filter_training_data(self, training_data: str) -> str:
"""Filter Obsidian-specific training data format."""
@ -36,12 +36,6 @@ def parse_training_data() -> List[Training]:
def remap_unique_exercises(obsidian_trainings: List[Training]) -> List[Training]:
"""Remap exercise names using Obsidian-specific mapping."""
for obsidian_training in obsidian_trainings:
if not obsidian_training or not obsidian_training.exercises:
continue
for obsidian_exercise in obsidian_training.exercises:
mapped_name = obsidian_unique_exercies_mapping.get(obsidian_exercise.name)
if mapped_name is not None:
obsidian_exercise.name = mapped_name
return obsidian_trainings
"""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,7 +1,5 @@
import os
import re
from typing import List, Tuple
from datetime import datetime
from typing import List, Tuple, Dict, Optional
from obsidian.py_models import Approach, Exercise, Training
from utils.date_refactor import parse_training_date
@ -9,38 +7,41 @@ from utils.date_refactor import parse_training_date
class BaseNotesParser:
"""Base class for parsing training data from different note formats."""
def __init__(self, data_file_name: str):
def __init__(self, data_file_name: str, exercise_mapper: Optional[Dict[str, str]] = None):
self.data_file_name = data_file_name
self.project_root = os.getcwd()
self.exercise_mapper = exercise_mapper or {}
def get_data_path(self) -> str:
return os.path.join(self.project_root, "data")
def get_data_file_path(self, file_name: str) -> str:
return os.path.join(self.get_data_path(), file_name)
def read_data_file(self, file_name: str) -> str:
path_to_file = self.get_data_file_path(file_name)
with open(path_to_file, "r") as f:
content = f.read()
return content
def serialize_exercise(self, reps: str, weight: str, name: str) -> Exercise:
"""Convert raw exercise data into Exercise object with approaches."""
reps_list: List[int] = [int(rep) for rep in reps.split("-")]
weight_splitted: bool = False
weight_list: List[float] = []
if weight:
weight_str_list: List[str] = [weight for weight in weight.split("-")]
if any(split_anchor in weight_str_list[0] for split_anchor in ["x", "х"]):
weight_splitted = True
splitter = "x" if "x" in weight_str_list[0] else "х"
weight_list = [float(xweight.split(splitter)[0]) for xweight in weight_str_list]
weight_list = [
float(xweight.split(splitter)[0]) for xweight in weight_str_list
]
else:
weight_list = [float(w) for w in weight_str_list]
approaches = []
if not weight:
for rep_index in range(0, len(reps_list)):
@ -49,19 +50,23 @@ class BaseNotesParser:
else:
weight_pointer = 0
for rep_index in range(0, len(reps_list)):
approach = Approach(weight=weight_list[weight_pointer], reps=reps_list[rep_index])
approach = Approach(
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(
name=name, approaches=approaches, splitted_weight=weight_splitted
)
return exercise
def parse_training_exercises(self, exercise_line: str) -> Exercise:
"""Parse exercise data from a table row."""
stripped: List[str] = [entry.strip() for entry in exercise_line.split("|")][1:-1]
stripped: List[str] = [entry.strip() for entry in exercise_line.split("|")][
1:-1
]
for entry in stripped:
if entry in ["Упражнение", "Вес", "Подходы"]:
raise ValueError
@ -74,28 +79,32 @@ class BaseNotesParser:
name=stripped[0], weight=stripped[1], reps=stripped[2]
)
raise ValueError("No valid exercise data found")
def filter_training_data(self, training_data: str) -> str:
"""Filter and clean training data. Override in subclasses for specific formats."""
return training_data
def parse_training_header(self, training_data_line: str) -> Tuple[bool, str, str, str]:
def parse_training_header(
self, training_data_line: str
) -> Tuple[bool, str, str, str]:
"""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:
"""Create Training object from date string using utility function."""
return Training(date=parse_training_date(date_str), exercises=[])
def parse_training_data(self) -> List[Training]:
"""Main parsing method. Override for specific parsing logic."""
training_data = self.filter_training_data(self.read_data_file(self.data_file_name))
training_data = self.filter_training_data(
self.read_data_file(self.data_file_name)
)
lines = training_data.splitlines()
current_training = None
trains = []
for index, line in enumerate(lines):
header_parsed, date, trainer, year_count = self.parse_training_header(line)
header_parsed, date, _, _ = self.parse_training_header(line)
if index == len(lines) - 1:
trains.append(current_training)
if header_parsed:
@ -108,5 +117,24 @@ class BaseNotesParser:
current_training.exercises.append(exr)
except ValueError:
pass
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]:
"""Apply exercise name mapping to all trainings."""
for training in trainings:
if not training or not training.exercises:
continue
for exercise in training.exercises:
if not exercise:
continue
mapped_name = self.exercise_mapper.get(exercise.name)
if mapped_name is not None:
exercise.name = mapped_name
return trainings
def parse_and_map_training_data(self) -> List[Training]:
"""Parse training data and apply exercise mapping."""
trainings = self.parse_training_data()
return self.apply_exercise_mapping(trainings)

View file

@ -1,15 +1,19 @@
from typing import List
from apple.notes_parser import parse_training_data as aptd
from obsidian.notes_parser import parse_training_data as optd
from apple.notes_parser import AppleNotesParser
from obsidian.notes_parser import ObsidianNotesParser
from obsidian.py_models import Training
def parse_old_data() -> List[Training]:
"""Method for parsing all old data from apple and obsidian notes
"""Method for parsing all old data from apple and obsidian notes with exercise mapping applied
Returns:
List of trainings
List of trainings with standardized exercise names
"""
o_dates = [train for train in optd()]
a_dates = [train for train in aptd()]
return o_dates + a_dates
obsidian_parser = ObsidianNotesParser()
apple_parser = AppleNotesParser()
o_trainings = obsidian_parser.parse_and_map_training_data()
a_trainings = apple_parser.parse_and_map_training_data()
return o_trainings + a_trainings