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.""" """Parser for Apple Notes format training data."""
def __init__(self): def __init__(self):
super().__init__("apple.md") super().__init__("apple.md", unique_apple_exercises_mapper)
def filter_training_data(self, training_data: str) -> str: def filter_training_data(self, training_data: str) -> str:
"""Filter Apple-specific training data format.""" """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]: def remap_unique_exercises(apple_trainings: List[Training]) -> List[Training]:
"""Remap exercise names using Apple-specific mapping.""" """Remap exercise names using Apple-specific mapping (deprecated - use parser.parse_and_map_training_data())."""
for apple_training in apple_trainings: parser = AppleNotesParser()
if not apple_training or not apple_training.exercises: return parser.apply_exercise_mapping(apple_trainings)
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

13
main.py
View file

@ -1,4 +1,17 @@
from parsers.text_data_parser import parse_old_data from parsers.text_data_parser import parse_old_data
from collections import Counter
text_trainings = parse_old_data() 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)) 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 рукой поочерёдно": "Тяга горизонтального блока (по очереди)", "Тяга горизонтального блока 1 рукой поочерёдно": "Тяга горизонтального блока (по очереди)",
"Тяга горизонтального блока одной рукой": "Тяга горизонтального блока (по очереди)", "Тяга горизонтального блока одной рукой": "Тяга горизонтального блока (по очереди)",
"Пресс на скамье": "Пресс на наклонной скамье"
} }

View file

@ -10,7 +10,7 @@ class ObsidianNotesParser(BaseNotesParser):
"""Parser for Obsidian Notes format training data.""" """Parser for Obsidian Notes format training data."""
def __init__(self): def __init__(self):
super().__init__("obsidian.md") super().__init__("obsidian.md", obsidian_unique_exercies_mapping)
def filter_training_data(self, training_data: str) -> str: def filter_training_data(self, training_data: str) -> str:
"""Filter Obsidian-specific training data format.""" """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]: def remap_unique_exercises(obsidian_trainings: List[Training]) -> List[Training]:
"""Remap exercise names using Obsidian-specific mapping.""" """Remap exercise names using Obsidian-specific mapping (deprecated - use parser.parse_and_map_training_data())."""
for obsidian_training in obsidian_trainings: parser = ObsidianNotesParser()
if not obsidian_training or not obsidian_training.exercises: return parser.apply_exercise_mapping(obsidian_trainings)
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

View file

@ -1,7 +1,5 @@
import os import os
import re from typing import List, Tuple, Dict, Optional
from typing import List, Tuple
from datetime import datetime
from obsidian.py_models import Approach, Exercise, Training from obsidian.py_models import Approach, Exercise, Training
from utils.date_refactor import parse_training_date from utils.date_refactor import parse_training_date
@ -10,9 +8,10 @@ from utils.date_refactor import parse_training_date
class BaseNotesParser: class BaseNotesParser:
"""Base class for parsing training data from different note formats.""" """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.data_file_name = data_file_name
self.project_root = os.getcwd() self.project_root = os.getcwd()
self.exercise_mapper = exercise_mapper or {}
def get_data_path(self) -> str: def get_data_path(self) -> str:
return os.path.join(self.project_root, "data") return os.path.join(self.project_root, "data")
@ -37,7 +36,9 @@ class BaseNotesParser:
if any(split_anchor in weight_str_list[0] for split_anchor in ["x", "х"]): if any(split_anchor in weight_str_list[0] for split_anchor in ["x", "х"]):
weight_splitted = True weight_splitted = True
splitter = "x" if "x" in weight_str_list[0] else "х" 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: else:
weight_list = [float(w) for w in weight_str_list] weight_list = [float(w) for w in weight_str_list]
@ -49,7 +50,9 @@ class BaseNotesParser:
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(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: if rep_index < len(weight_list) - 1:
weight_pointer += 1 weight_pointer += 1
approaches.append(approach) approaches.append(approach)
@ -61,7 +64,9 @@ class BaseNotesParser:
def parse_training_exercises(self, exercise_line: str) -> Exercise: def parse_training_exercises(self, exercise_line: str) -> Exercise:
"""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("|")][1:-1] stripped: List[str] = [entry.strip() for entry in exercise_line.split("|")][
1:-1
]
for entry in stripped: for entry in stripped:
if entry in ["Упражнение", "Вес", "Подходы"]: if entry in ["Упражнение", "Вес", "Подходы"]:
raise ValueError raise ValueError
@ -79,7 +84,9 @@ class BaseNotesParser:
"""Filter and clean training data. Override in subclasses for specific formats.""" """Filter and clean training data. Override in subclasses for specific formats."""
return training_data 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.""" """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")
@ -89,13 +96,15 @@ class BaseNotesParser:
def parse_training_data(self) -> List[Training]: def parse_training_data(self) -> List[Training]:
"""Main parsing method. Override for specific parsing logic.""" """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() lines = training_data.splitlines()
current_training = None current_training = None
trains = [] trains = []
for index, line in enumerate(lines): 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: if index == len(lines) - 1:
trains.append(current_training) trains.append(current_training)
if header_parsed: if header_parsed:
@ -110,3 +119,22 @@ class BaseNotesParser:
pass 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 typing import List
from apple.notes_parser import parse_training_data as aptd from apple.notes_parser import AppleNotesParser
from obsidian.notes_parser import parse_training_data as optd from obsidian.notes_parser import ObsidianNotesParser
from obsidian.py_models import Training from obsidian.py_models import Training
def parse_old_data() -> List[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: Returns:
List of trainings List of trainings with standardized exercise names
""" """
o_dates = [train for train in optd()] obsidian_parser = ObsidianNotesParser()
a_dates = [train for train in aptd()] apple_parser = AppleNotesParser()
return o_dates + a_dates
o_trainings = obsidian_parser.parse_and_map_training_data()
a_trainings = apple_parser.parse_and_map_training_data()
return o_trainings + a_trainings