repwise 2.0.0__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {repwise-2.0.0/src/repwise.egg-info → repwise-2.1.0}/PKG-INFO +1 -1
- {repwise-2.0.0 → repwise-2.1.0}/pyproject.toml +1 -1
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/__init__.py +1 -1
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/config.py +71 -15
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/domain/models.py +6 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/domain/progression.py +39 -3
- {repwise-2.0.0 → repwise-2.1.0/src/repwise.egg-info}/PKG-INFO +1 -1
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_config.py +66 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_progression.py +92 -0
- {repwise-2.0.0 → repwise-2.1.0}/LICENSE +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/README.md +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/setup.cfg +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/__main__.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/app/__init__.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/app/checking.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/app/fetch.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/app/importing.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/app/listing.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/app/logout.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/app/report.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/app/update.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/checker.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/cli/__init__.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/cli/completion.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/cli/parser.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/domain/__init__.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/domain/effort.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/domain/matching.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/dumps.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/errors.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/garmin/__init__.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/garmin/catalog.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/garmin/client.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/garmin/payloads.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/importer.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/log.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/planner.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise/yamlio.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise.egg-info/SOURCES.txt +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise.egg-info/dependency_links.txt +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise.egg-info/entry_points.txt +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise.egg-info/requires.txt +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/src/repwise.egg-info/top_level.txt +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_catalog.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_checker.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_checking.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_cli.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_client.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_completion.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_dumps.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_effort.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_fetch.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_importer.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_log.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_logout.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_main.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_matching.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_models.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_payloads.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_planner.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_report.py +0 -0
- {repwise-2.0.0 → repwise-2.1.0}/tests/test_update.py +0 -0
|
@@ -50,17 +50,21 @@ _REQUIRED = ("name", "garmin_name", "rep_low", "rep_high", "sets", "load")
|
|
|
50
50
|
|
|
51
51
|
@dataclass(frozen=True)
|
|
52
52
|
class LoadRules:
|
|
53
|
-
"""What a load type does:
|
|
53
|
+
"""What a load type does: its step size, and how light and heavy it goes.
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
dumbbell rack goes up in ones
|
|
57
|
-
|
|
55
|
+
All three are properties of the equipment rather than of any one exercise -
|
|
56
|
+
a dumbbell rack goes up in ones, starts at one and ends at whatever the
|
|
57
|
+
heaviest pair is - so they are declared once per load type and only
|
|
58
|
+
overridden where an exercise really differs.
|
|
58
59
|
"""
|
|
59
60
|
|
|
60
61
|
steps: dict[str, float] = field(default_factory=dict)
|
|
61
62
|
#: Missing entries mean no floor, so a config written before deloads
|
|
62
63
|
#: existed keeps loading and simply never stops a deload.
|
|
63
64
|
minimums: dict[str, float] = field(default_factory=dict)
|
|
65
|
+
#: Missing entries mean no ceiling, which is the usual case: a gym's rack
|
|
66
|
+
#: outlasts you, and only equipment you own runs out.
|
|
67
|
+
maximums: dict[str, float] = field(default_factory=dict)
|
|
64
68
|
|
|
65
69
|
|
|
66
70
|
def _xdg_config_home() -> str:
|
|
@@ -285,6 +289,57 @@ class Problems:
|
|
|
285
289
|
raise ConfigError(f"{len(self.found)} problems:\n{listed}")
|
|
286
290
|
|
|
287
291
|
|
|
292
|
+
def _bounds(
|
|
293
|
+
raw: dict, load: str, loads: LoadRules, where: str, problems: Problems
|
|
294
|
+
) -> tuple[float, float | None]:
|
|
295
|
+
"""How light and how heavy this exercise may be loaded.
|
|
296
|
+
|
|
297
|
+
Resolved together because they only mean anything against each other: a
|
|
298
|
+
ceiling under its own floor leaves progression choosing between two
|
|
299
|
+
impossible loads, and that is only visible once both are known.
|
|
300
|
+
|
|
301
|
+
The two ends default differently, and deliberately. No floor is zero, which
|
|
302
|
+
is the honest answer for anything lifted with no equipment of its own. No
|
|
303
|
+
ceiling is `None` rather than zero, because zero is a real maximum -
|
|
304
|
+
nothing may be added at all - and equipment that does not run out before
|
|
305
|
+
you do is the common case. Either way a config written before deloads or
|
|
306
|
+
ceilings existed keeps its meaning exactly.
|
|
307
|
+
|
|
308
|
+
Anything contradictory is dropped rather than honoured. The file will not
|
|
309
|
+
load either way - `problems` sees to that - so the only question is which
|
|
310
|
+
spec the rest of the checks get to run against.
|
|
311
|
+
"""
|
|
312
|
+
declared_minimum = raw.get("min_weight")
|
|
313
|
+
if declared_minimum is None:
|
|
314
|
+
minimum = float(loads.minimums.get(load, 0.0))
|
|
315
|
+
else:
|
|
316
|
+
minimum = float(declared_minimum)
|
|
317
|
+
if minimum < 0:
|
|
318
|
+
problems.add(f"{where}: {raw['name']!r} has a negative min_weight")
|
|
319
|
+
minimum = 0.0
|
|
320
|
+
|
|
321
|
+
declared_maximum = raw.get("max_weight")
|
|
322
|
+
if declared_maximum is None:
|
|
323
|
+
ceiling = loads.maximums.get(load)
|
|
324
|
+
if ceiling is None:
|
|
325
|
+
return minimum, None
|
|
326
|
+
maximum = float(ceiling)
|
|
327
|
+
else:
|
|
328
|
+
maximum = float(declared_maximum)
|
|
329
|
+
|
|
330
|
+
if maximum < 0:
|
|
331
|
+
problems.add(f"{where}: {raw['name']!r} has a negative max_weight")
|
|
332
|
+
return minimum, None
|
|
333
|
+
if maximum < minimum:
|
|
334
|
+
problems.add(
|
|
335
|
+
f"{where}: {raw['name']!r} has a max_weight of {maximum:g} below "
|
|
336
|
+
f"its min_weight of {minimum:g}, so no load fits between them"
|
|
337
|
+
)
|
|
338
|
+
return minimum, None
|
|
339
|
+
|
|
340
|
+
return minimum, maximum
|
|
341
|
+
|
|
342
|
+
|
|
288
343
|
def _bodyweight_factor(raw: dict, where: str, problems: Problems) -> float:
|
|
289
344
|
"""The share of the lifter this exercise carries, if it says.
|
|
290
345
|
|
|
@@ -352,17 +407,16 @@ def _build_exercise(
|
|
|
352
407
|
problems.add(f"{where}: {raw['name']!r} has a negative start_weight")
|
|
353
408
|
start_weight = 0.0
|
|
354
409
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
min_weight = 0.0
|
|
410
|
+
min_weight, max_weight = _bounds(raw, load, loads, where, problems)
|
|
411
|
+
if max_weight is not None and start_weight > max_weight:
|
|
412
|
+
# `start_weight` is read only when the step is created, which is the
|
|
413
|
+
# one moment nothing else could catch this: no session has been logged
|
|
414
|
+
# yet, so progression never gets a chance to refuse the weight.
|
|
415
|
+
problems.add(
|
|
416
|
+
f"{where}: {raw['name']!r} would start at {start_weight:g} kg, "
|
|
417
|
+
f"above its own max_weight of {max_weight:g}"
|
|
418
|
+
)
|
|
419
|
+
max_weight = None
|
|
366
420
|
|
|
367
421
|
bodyweight_factor = _bodyweight_factor(raw, where, problems)
|
|
368
422
|
|
|
@@ -382,6 +436,7 @@ def _build_exercise(
|
|
|
382
436
|
notes=raw.get("notes"),
|
|
383
437
|
start_weight=start_weight,
|
|
384
438
|
min_weight=min_weight,
|
|
439
|
+
max_weight=max_weight,
|
|
385
440
|
bodyweight_factor=bodyweight_factor,
|
|
386
441
|
)
|
|
387
442
|
|
|
@@ -473,6 +528,7 @@ def load_config(path: str | None = None) -> Config:
|
|
|
473
528
|
loads = LoadRules(
|
|
474
529
|
steps=settings.get("weight_steps") or {},
|
|
475
530
|
minimums=settings.get("min_weights") or {},
|
|
531
|
+
maximums=settings.get("max_weights") or {},
|
|
476
532
|
)
|
|
477
533
|
garmin_raw = settings.get("garmin") or {}
|
|
478
534
|
defaults = GarminSettings()
|
|
@@ -38,6 +38,12 @@ class ExerciseSpec:
|
|
|
38
38
|
#: the lightest pair of dumbbells, the top plate of the stack. A deload
|
|
39
39
|
#: stops here rather than prescribing a weight that does not exist.
|
|
40
40
|
min_weight: float = 0.0
|
|
41
|
+
#: The heaviest this exercise can be loaded: the largest pair of dumbbells
|
|
42
|
+
#: you own, the last plate in the bag, the bottom of the stack. Topping out
|
|
43
|
+
#: the rep range stops here rather than prescribing a weight you cannot
|
|
44
|
+
#: load. `None` is no ceiling, and is the right default - a gym rack runs
|
|
45
|
+
#: out long after you do, and it is home equipment that really ends.
|
|
46
|
+
max_weight: float | None = None
|
|
41
47
|
#: The share of your bodyweight this movement carries: 1.0 for a calf raise
|
|
42
48
|
#: or a weighted pull-up, 0 for anything you lie or sit down to do. Never
|
|
43
49
|
#: guessed from the category - a lat pull-down is categorised `PULL_UP` and
|
|
@@ -304,8 +304,39 @@ def _advance(
|
|
|
304
304
|
# different weight moves the prescription by more than one step. Name
|
|
305
305
|
# what was lifted, otherwise the jump looks arbitrary.
|
|
306
306
|
lifted = f" at {weight:g} kg" if rebased else ""
|
|
307
|
+
heavier = weight + spec.weight_step
|
|
308
|
+
# `max_weight` is the heaviest load that exists to be prescribed, so a
|
|
309
|
+
# step past it is shortened to land *on* it rather than refused. The
|
|
310
|
+
# last pair on the rack is still a pair, and stopping short of a weight
|
|
311
|
+
# you own would leave the top of your equipment permanently unused for
|
|
312
|
+
# the sake of a step size the equipment never promised to divide into.
|
|
313
|
+
#
|
|
314
|
+
# Not the mirror of the deload's floor, deliberately. A short step up
|
|
315
|
+
# is a smaller increase than usual, which is always safe to prescribe;
|
|
316
|
+
# a short step *down* is a smaller decrease than usual, which may not
|
|
317
|
+
# be enough to break the stall that asked for it. So the ceiling is
|
|
318
|
+
# rounded to and the floor is refused.
|
|
319
|
+
if spec.max_weight is not None and heavier > spec.max_weight:
|
|
320
|
+
if weight >= spec.max_weight:
|
|
321
|
+
# Nothing left to add. The target settles at the top of the
|
|
322
|
+
# range, which is where bodyweight work already lives: once
|
|
323
|
+
# the load has run out, the range is all there is to progress.
|
|
324
|
+
# "past" rather than "at" for a load above the ceiling, which
|
|
325
|
+
# is what a target set before the ceiling was declared leaves
|
|
326
|
+
# behind. Rule 3 does not pull it down - taking load off is the
|
|
327
|
+
# deload's job - so the report has to be able to say so.
|
|
328
|
+
sits = "already at" if weight == spec.max_weight else "past"
|
|
329
|
+
return (
|
|
330
|
+
Target(spec.rep_high, weight),
|
|
331
|
+
f"top of the range, {sits} the {spec.max_weight:g} kg maximum",
|
|
332
|
+
)
|
|
333
|
+
return (
|
|
334
|
+
Target(spec.rep_low, spec.max_weight),
|
|
335
|
+
f"hit {floor} on every set{lifted}, top of the range, "
|
|
336
|
+
f"up to the {spec.max_weight:g} kg maximum",
|
|
337
|
+
)
|
|
307
338
|
return (
|
|
308
|
-
Target(spec.rep_low,
|
|
339
|
+
Target(spec.rep_low, heavier),
|
|
309
340
|
f"hit {floor} on every set{lifted}, top of the range",
|
|
310
341
|
)
|
|
311
342
|
|
|
@@ -330,6 +361,11 @@ def _advance(
|
|
|
330
361
|
if not rebased and floor > current.reps:
|
|
331
362
|
beat = f"beat target ({floor} on every set), "
|
|
332
363
|
stalled = f"hit after {streak} miss{'es' if streak > 1 else ''}, " if streak else ""
|
|
364
|
+
# A timed hold is progressed in seconds -- `as_time` put them where the
|
|
365
|
+
# reps go -- so the step has to be named in the unit the exercise is
|
|
366
|
+
# actually measured in. "add 1 rep" beside a 25 s -> 26 s target names
|
|
367
|
+
# something the exercise does not have.
|
|
368
|
+
unit = "second" if spec.time_based else "rep"
|
|
333
369
|
plural = "" if spec.rep_step == 1 else "s"
|
|
334
370
|
|
|
335
371
|
if lead >= spec.sets:
|
|
@@ -338,12 +374,12 @@ def _advance(
|
|
|
338
374
|
reps = min(base + spec.rep_step, spec.rep_high)
|
|
339
375
|
return (
|
|
340
376
|
Target(reps, weight),
|
|
341
|
-
f"{beat}{stalled}add {spec.rep_step}
|
|
377
|
+
f"{beat}{stalled}add {spec.rep_step} {unit}{plural}{moved}",
|
|
342
378
|
)
|
|
343
379
|
|
|
344
380
|
return (
|
|
345
381
|
Target(base, weight, lead),
|
|
346
|
-
f"{beat}{stalled}add {spec.rep_step}
|
|
382
|
+
f"{beat}{stalled}add {spec.rep_step} {unit}{plural} on "
|
|
347
383
|
f"{units} of {spec.sets} sets{moved}",
|
|
348
384
|
)
|
|
349
385
|
|
|
@@ -698,6 +698,72 @@ def test_a_negative_min_weight_is_rejected(write_config):
|
|
|
698
698
|
load_config(write_config(text))
|
|
699
699
|
|
|
700
700
|
|
|
701
|
+
# --- how heavy a load can go ----------------------------------------------
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def test_max_weights_come_from_the_load_type(write_config):
|
|
705
|
+
text = FIXTURE.replace(
|
|
706
|
+
" weight_steps:",
|
|
707
|
+
" max_weights:\n barbell: 100.0\n\n weight_steps:",
|
|
708
|
+
)
|
|
709
|
+
config = load_config(write_config(text))
|
|
710
|
+
assert config["Workout A"].exercises[0].max_weight == 100.0
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def test_an_exercise_can_set_its_own_max_weight(write_config):
|
|
714
|
+
text = FIXTURE.replace(
|
|
715
|
+
" load: barbell", " load: barbell\n max_weight: 60.0"
|
|
716
|
+
)
|
|
717
|
+
config = load_config(write_config(text))
|
|
718
|
+
assert config["Workout A"].exercises[0].max_weight == 60.0
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def test_an_exercise_max_weight_beats_the_load_type(write_config):
|
|
722
|
+
"""Only this one movement is capped, and the rack is not."""
|
|
723
|
+
text = FIXTURE.replace(
|
|
724
|
+
" weight_steps:",
|
|
725
|
+
" max_weights:\n barbell: 100.0\n\n weight_steps:",
|
|
726
|
+
).replace(
|
|
727
|
+
" load: barbell", " load: barbell\n max_weight: 60.0"
|
|
728
|
+
)
|
|
729
|
+
config = load_config(write_config(text))
|
|
730
|
+
assert config["Workout A"].exercises[0].max_weight == 60.0
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def test_no_max_weights_at_all_means_no_ceiling(write_config):
|
|
734
|
+
"""Unset is None rather than zero: zero would be a real maximum."""
|
|
735
|
+
config = load_config(write_config(FIXTURE))
|
|
736
|
+
assert config["Workout A"].exercises[0].max_weight is None
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def test_a_negative_max_weight_is_rejected(write_config):
|
|
740
|
+
text = FIXTURE.replace(
|
|
741
|
+
" load: barbell", " load: barbell\n max_weight: -5"
|
|
742
|
+
)
|
|
743
|
+
with pytest.raises(ConfigError, match="negative max_weight"):
|
|
744
|
+
load_config(write_config(text))
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def test_a_max_weight_below_the_min_weight_is_rejected(write_config):
|
|
748
|
+
"""Nothing could be prescribed between them, so neither end is honoured."""
|
|
749
|
+
text = FIXTURE.replace(
|
|
750
|
+
" load: barbell",
|
|
751
|
+
" load: barbell\n min_weight: 20.0\n max_weight: 10.0",
|
|
752
|
+
)
|
|
753
|
+
with pytest.raises(ConfigError, match="below its min_weight"):
|
|
754
|
+
load_config(write_config(text))
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def test_a_start_weight_above_the_max_weight_is_rejected(write_config):
|
|
758
|
+
"""The one moment no session exists yet to correct it."""
|
|
759
|
+
text = FIXTURE.replace(
|
|
760
|
+
" load: barbell",
|
|
761
|
+
" load: barbell\n start_weight: 30.0\n max_weight: 10.0",
|
|
762
|
+
)
|
|
763
|
+
with pytest.raises(ConfigError, match="above its own max_weight"):
|
|
764
|
+
load_config(write_config(text))
|
|
765
|
+
|
|
766
|
+
|
|
701
767
|
# --- reading a stored weight as a real load --------------------------------
|
|
702
768
|
|
|
703
769
|
|
|
@@ -433,6 +433,17 @@ def test_a_timed_hold_ramps_in_seconds():
|
|
|
433
433
|
assert target.spread(3) == "47+2"
|
|
434
434
|
|
|
435
435
|
|
|
436
|
+
def test_a_timed_hold_says_what_it_adds_in_seconds():
|
|
437
|
+
"""The step is a second, not a rep: a hold has no reps to add one to."""
|
|
438
|
+
_, why = next_target(PLANK, Target(47, 0.0), held(47, 47, 47))
|
|
439
|
+
assert "add 1 second" in why, why
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def test_a_timed_hold_names_seconds_on_a_partial_advance():
|
|
443
|
+
_, why = next_target(PLANK, Target(47, 0.0), held(47, 47, 47), streak=1)
|
|
444
|
+
assert "add 1 second on 2 of 3 sets" in why, why
|
|
445
|
+
|
|
446
|
+
|
|
436
447
|
# --- how a target is written ----------------------------------------------
|
|
437
448
|
|
|
438
449
|
|
|
@@ -645,3 +656,84 @@ def test_a_deload_and_the_climb_back_are_the_same_ladder():
|
|
|
645
656
|
BARBELL_SQUAT, eased, [P(9, 20.0), P(9, 20.0), P(8, 20.0)], streak=0
|
|
646
657
|
)
|
|
647
658
|
assert back == Target(9, 20.0), "levelled up, flat again"
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
# --- topping out ----------------------------------------------------------
|
|
662
|
+
#
|
|
663
|
+
# The mirror of the deload's floor. Rule 3 adds a step every time the range is
|
|
664
|
+
# cleared, and equipment you own runs out; without a ceiling the target climbs
|
|
665
|
+
# to a weight no session can be logged against.
|
|
666
|
+
|
|
667
|
+
HOME_PRESS = spec(
|
|
668
|
+
name="Dumbbell Floor Press",
|
|
669
|
+
garmin_name="DUMBBELL_FLOOR_PRESS",
|
|
670
|
+
garmin_category="BENCH_PRESS",
|
|
671
|
+
rep_low=10,
|
|
672
|
+
rep_high=16,
|
|
673
|
+
sets=3,
|
|
674
|
+
load="dumbbell",
|
|
675
|
+
weight_step=2.5,
|
|
676
|
+
min_weight=2.5,
|
|
677
|
+
max_weight=10.0,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def test_the_weight_never_goes_above_the_maximum():
|
|
682
|
+
"""12.5 kg dumbbells you do not own is a target no session can answer."""
|
|
683
|
+
target, why = next_target(HOME_PRESS, Target(16, 10.0), [P(16, 10.0)] * 3)
|
|
684
|
+
assert target == Target(16, 10.0), why
|
|
685
|
+
assert "already at the 10 kg maximum" in why
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def test_a_capped_exercise_holds_at_the_top_of_the_range():
|
|
689
|
+
"""The same ending as bodyweight: the range is all there is left."""
|
|
690
|
+
target, _ = next_target(HOME_PRESS, Target(16, 10.0), [P(18, 10.0)] * 3)
|
|
691
|
+
assert target == Target(HOME_PRESS.rep_high, 10.0)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def test_the_last_step_is_shortened_to_land_on_the_maximum():
|
|
695
|
+
"""9 + 2.5 is 11.5, which does not exist - but the 10 kg pair does."""
|
|
696
|
+
target, why = next_target(HOME_PRESS, Target(16, 9.0), [P(16, 9.0)] * 3)
|
|
697
|
+
assert target == Target(HOME_PRESS.rep_low, 10.0), why
|
|
698
|
+
assert "up to the 10 kg maximum" in why
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def test_the_shortened_step_is_taken_once_and_then_holds():
|
|
702
|
+
"""Reaching the ceiling is a rung to climb, not a place to hover below."""
|
|
703
|
+
stepped, why = next_target(HOME_PRESS, Target(16, 9.0), [P(16, 9.0)] * 3)
|
|
704
|
+
assert stepped == Target(10, 10.0), why
|
|
705
|
+
|
|
706
|
+
held, why = next_target(HOME_PRESS, stepped, [P(16, 10.0)] * 3)
|
|
707
|
+
assert held == Target(HOME_PRESS.rep_high, 10.0), why
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def test_a_load_already_past_the_maximum_is_not_pulled_down():
|
|
711
|
+
"""Rule 3 is the rule that adds load; taking it off is the deload's job."""
|
|
712
|
+
target, why = next_target(HOME_PRESS, Target(16, 12.5), [P(16, 12.5)] * 3)
|
|
713
|
+
assert target == Target(16, 12.5), why
|
|
714
|
+
assert "past the 10 kg maximum" in why, "past it, rather than at it"
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def test_below_the_maximum_the_weight_still_climbs():
|
|
718
|
+
"""The ceiling is a stop, not a brake: every step under it is taken."""
|
|
719
|
+
target, why = next_target(HOME_PRESS, Target(16, 7.5), [P(16, 7.5)] * 3)
|
|
720
|
+
assert target == Target(HOME_PRESS.rep_low, 10.0), why
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def test_no_maximum_means_the_weight_keeps_climbing():
|
|
724
|
+
"""The default, and what every config written before ceilings did."""
|
|
725
|
+
assert SQUAT.max_weight is None
|
|
726
|
+
target, _ = next_target(SQUAT, Target(10, 20.0), [P(10, 20.0)] * 3)
|
|
727
|
+
assert target == Target(SQUAT.rep_low, 25.0)
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def test_a_capped_exercise_still_deloads():
|
|
731
|
+
"""Running out of weight upwards says nothing about coming back down."""
|
|
732
|
+
target, why = next_target(HOME_PRESS, Target(10, 10.0), [P(8, 10.0)] * 3, streak=1)
|
|
733
|
+
assert target == Target(HOME_PRESS.rep_low, 7.5), why
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def test_a_session_at_the_cap_rebases_off_an_impossible_target():
|
|
737
|
+
"""The way back from a target already past the ceiling: lift what you have."""
|
|
738
|
+
target, why = next_target(HOME_PRESS, Target(10, 12.5), [P(12, 10.0)] * 3)
|
|
739
|
+
assert target.weight == 10.0, why
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|