Initial commit
This commit is contained in:
commit
99d4903d81
13 files changed
+328
No files matched your search
+19
@@ -0,0 +1,19 @@
|
||||
.idea
|
||||
build
|
||||
**/runinfo
|
||||
**/__pycache__
|
||||
**/htmlcov
|
||||
.venv
|
||||
test/report.txt
|
||||
test/coverage.txt
|
||||
**/dist
|
||||
**/*.egg-info
|
||||
**/*.eggs
|
||||
.coverage
|
||||
coverage.xml
|
||||
CLAUDE.md
|
||||
public/
|
||||
|
||||
*.log
|
||||
*.hd5
|
||||
*.h5
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
@@ -0,0 +1,23 @@
|
||||
.PHONY: install install-dev test test-cov clean
|
||||
|
||||
install:
|
||||
pip install -e .
|
||||
|
||||
install-dev:
|
||||
pip install -e ".[dev]"
|
||||
|
||||
test:
|
||||
pytest
|
||||
|
||||
test-cov:
|
||||
pytest --cov=stack --cov-report=term-missing
|
||||
|
||||
clean:
|
||||
rm -rf build/
|
||||
rm -rf dist/
|
||||
rm -rf *.egg-info/
|
||||
rm -rf src/*.egg-info/
|
||||
rm -rf .pytest_cache/
|
||||
rm -rf .coverage
|
||||
rm -rf htmlcov/
|
||||
find . -type d -name __pycache__ -exec rm -rf {} +
|
||||
@@ -0,0 +1,52 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "stack"
|
||||
version = "0.1.0"
|
||||
description = "A command line application"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
requires-python = ">=3.12"
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
dependencies = [
|
||||
"PyYAML>=6.0",
|
||||
"pydantic>=2.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
"pytest-cov>=4.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
stack-builder = "builder.cli:main"
|
||||
stack-parser = "parser.cli:main"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
pythonpath = ["src"]
|
||||
addopts = "-v --tb=short"
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src/builder", "src/parser", "src/language"]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"if __name__ == .__main__.:",
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
""" The builder application for the stack workout "language" """
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Entry point for running the package as a module: python -m builder."""
|
||||
|
||||
from builder.cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Command line interface for stack."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from builder import __version__
|
||||
|
||||
|
||||
def create_parser() -> argparse.ArgumentParser:
|
||||
"""Create and configure the argument parser."""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="stack",
|
||||
description="A command line application",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--version",
|
||||
action="version",
|
||||
version=f"%(prog)s {__version__}",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""Main entry point for the CLI."""
|
||||
parser = create_parser()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
print("Hello from stack!")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Language module for workout definition configuration."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
|
||||
import yaml
|
||||
from enum import Enum
|
||||
from typing import Collection
|
||||
from pydantic import BaseModel, computed_field
|
||||
|
||||
|
||||
class Sport(Enum):
|
||||
"""You need to know the sport to know how to plan for different goals. For now we'll just do powerlifting."""
|
||||
|
||||
POWERLIFTING = 1
|
||||
|
||||
|
||||
class LiftGoal(Enum):
|
||||
"""Different set/rep schemes for different goals."""
|
||||
|
||||
STRENGTH = 1
|
||||
VOLUME = 2
|
||||
|
||||
|
||||
class Lift(Enum):
|
||||
"""Not an exhaustive list yet"""
|
||||
|
||||
SQUAT = 1
|
||||
HIGH_BAR_SQUAT = 2
|
||||
PAUSE_SQUAT = 3
|
||||
BENCH = 4
|
||||
CLOSE_GRIP_BENCH = 5
|
||||
LARSEN_PRESS = 6
|
||||
DEADLIFT = 7
|
||||
PAUSE_DEADLIFT = 8
|
||||
HALT_DEADLIFT = 9
|
||||
|
||||
|
||||
def intensity_from_rpe_reps(rpe: float, reps: int) -> float:
|
||||
"""Calculate suggested intensity percentage from planned RPE and planned reps"""
|
||||
rpe_dimension = [5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0]
|
||||
reps_dimension = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
grid = [
|
||||
[100, 95.5, 92.2, 89.2, 86.3, 83.7, 81.1, 78.6, 76.2, 73.9],
|
||||
[97.8, 93.9, 90.7, 87.8, 85.0, 82.4, 79.9, 77.4, 75.1, 72.3],
|
||||
[95.5, 92.2, 89.2, 86.3, 83.7, 81.1, 78.6, 76.2, 73.9, 70.7],
|
||||
[93.9, 90.7, 87.8, 85.0, 82.4, 79.9, 77.4, 75.1, 72.3, 69.4],
|
||||
[92.2, 89.2, 86.3, 83.7, 81.1, 78.6, 76.2, 73.9, 70.7, 68.0],
|
||||
[90.7, 87.8, 85.0, 82.4, 79.9, 77.4, 75.1, 72.3, 69.4, 66.7],
|
||||
[89.2, 86.3, 83.7, 81.1, 78.6, 76.2, 73.9, 70.7, 68.0, 65.3],
|
||||
[87.8, 85.0, 82.4, 79.9, 77.4, 75.1, 72.3, 69.4, 66.7, 64.0],
|
||||
[86.3, 83.7, 81.1, 78.6, 76.2, 73.9, 70.7, 68.0, 65.3, 62.6],
|
||||
[85.0, 82.4, 79.9, 77.4, 75.1, 72.3, 69.4, 66.7, 64.0, 61.3],
|
||||
[83.7, 81.1, 78.6, 76.2, 73.9, 70.7, 68.0, 65.3, 62.6, 60.0],
|
||||
]
|
||||
rpe_index = rpe_dimension.index(rpe)
|
||||
reps_index = reps_dimension.index(reps)
|
||||
return grid[reps_index][rpe_index]
|
||||
|
||||
|
||||
class Plan(BaseModel):
|
||||
"""Class representing a workout plan."""
|
||||
|
||||
# Fields
|
||||
squat_plan: list[LiftGoal]
|
||||
bench_plan: list[LiftGoal]
|
||||
deadlift_plan: list[LiftGoal]
|
||||
volume_day_amount: int
|
||||
strength_day_amount: int
|
||||
squat_1rm: int
|
||||
bench_1rm: int
|
||||
deadlift_1rm: int
|
||||
|
||||
# Fields with defaults
|
||||
sport: Sport = Sport.POWERLIFTING
|
||||
num_weeks: int = 4
|
||||
num_workouts: int = 4
|
||||
|
||||
# Internal constants
|
||||
_RPE_TARGETS = {
|
||||
3: [7.0, 8.0, 9.0],
|
||||
4: [7.0, 8.0, 8.5, 9.0],
|
||||
5: [7.0, 7.5, 8.0, 8.5, 9.0],
|
||||
}
|
||||
|
||||
@computed_field
|
||||
def rpe_targets(self) -> list[float]:
|
||||
"""Get RPE targets based on number of sets"""
|
||||
return self._RPE_TARGETS[self.volume_day_amount]
|
||||
|
||||
def generate_week(self) -> list[list[tuple[Lift, LiftGoal]]]:
|
||||
"""
|
||||
Generate the list of workouts for the plan
|
||||
|
||||
So we have a specified number of each lift to get in per week (including their "goal", so whether it's a volume or strength day). On top of this we have a specified number of total workouts per week. We need to distribute the lifts across the workouts as evenly as possible for a given week.
|
||||
"""
|
||||
workouts: list[list[tuple[Lift, LiftGoal]]] = [[] for _ in range(self.num_workouts)]
|
||||
lift_plans = {
|
||||
Lift.SQUAT: self.squat_plan,
|
||||
Lift.BENCH: self.bench_plan,
|
||||
Lift.DEADLIFT: self.deadlift_plan,
|
||||
}
|
||||
|
||||
workout_index = 0
|
||||
for lift, goals in lift_plans.items():
|
||||
for goal in goals:
|
||||
workouts[workout_index].append((lift, goal))
|
||||
workout_index = (workout_index + 1) % self.num_workouts
|
||||
|
||||
return workouts
|
||||
@@ -0,0 +1,3 @@
|
||||
""" The parser application for the stack workout "language" """
|
||||
|
||||
__version__ = "0.1.0"
|
||||
@@ -0,0 +1,6 @@
|
||||
""" Entry point for parsing a stack file: python -m parser """
|
||||
|
||||
from parser.cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Command line interface for stack."""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
from parser import __version__
|
||||
|
||||
|
||||
def create_parser() -> argparse.ArgumentParser:
|
||||
"""Create and configure the argument parser."""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="stack",
|
||||
description="A command line application",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--version",
|
||||
action="version",
|
||||
version=f"%(prog)s {__version__}",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""Main entry point for the CLI."""
|
||||
parser = create_parser()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
print("Hello from stack!")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1 @@
|
||||
"""Tests for stack."""
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Tests for the CLI module."""
|
||||
|
||||
from builder import cli as bcli
|
||||
from parser import cli as pcli
|
||||
|
||||
|
||||
def test_builder_version_flag(capsys):
|
||||
"""Test that --version prints version info."""
|
||||
parser = bcli.create_parser()
|
||||
try:
|
||||
parser.parse_args(["--version"])
|
||||
except SystemExit:
|
||||
pass
|
||||
captured = capsys.readouterr()
|
||||
assert "stack" in captured.out
|
||||
|
||||
|
||||
def test_builder_main_returns_zero():
|
||||
"""Test that main returns 0 on success."""
|
||||
result = bcli.main([])
|
||||
assert result == 0
|
||||
|
||||
|
||||
def test_parser_version_flag(capsys):
|
||||
"""Test that --version prints version info."""
|
||||
parser = pcli.create_parser()
|
||||
try:
|
||||
parser.parse_args(["--version"])
|
||||
except SystemExit:
|
||||
pass
|
||||
captured = capsys.readouterr()
|
||||
assert "stack" in captured.out
|
||||
|
||||
|
||||
def test_parser_main_returns_zero():
|
||||
"""Test that main returns 0 on success."""
|
||||
result = pcli.main([])
|
||||
assert result == 0
|
||||
Reference in new issue
Block a user