Add weekly RPE list, OpenGym push, and accessories
- Plan takes rpe_targets, one RPE per week; its length sets the block length. - src/language/opengym.py converts a plan into OpenGym routines queued as a rotation and pushes them through the API, with a state backup before each write. - scratch.py gains --opengym, --opengym-dry-run, --opengym-replace, --opengym-migrate and --opengym-accessories, plus the current block's numbers and accessory layout. - Fix the kg-to-lb factor in scratch.py (2.20462).
This commit is contained in:
1 parent
180b468c01
commit
f1de9ff3fb
4 files changed
+663
-231
No files matched your search
+244
-217
@@ -1,218 +1,245 @@
|
||||
Week,Day,Lift,Goal,Set,Reps,RPE,Weight,Set Volume
|
||||
1,1,Squat,Volume,1,4,4.0,125.0,500.0
|
||||
1,1,Squat,Volume,2,4,5.5,132.5,530.0
|
||||
1,1,Squat,Volume,3,4,7.0,137.5,550.0
|
||||
1,1,Squat,Volume,4,4,7.0,137.5,550.0
|
||||
1,1,Pause Squat,Volume,1,5,7,122.5,612.5
|
||||
1,1,Pause Squat,Volume,2,5,7,122.5,612.5
|
||||
1,1,Deadlift,Strength,1,5,4.0,120.0,600.0
|
||||
1,1,Deadlift,Strength,2,3,5.5,135.0,405.0
|
||||
1,1,Deadlift,Strength,3,1,7.0,152.5,152.5
|
||||
1,1,Deadlift,Strength,4,3,7.0,142.5,427.5
|
||||
1,1,Deadlift,Strength,5,3,7.0,142.5,427.5
|
||||
1,2,Squat,Strength,1,5,4.0,120.0,600.0
|
||||
1,2,Squat,Strength,2,3,5.5,135.0,405.0
|
||||
1,2,Squat,Strength,3,1,7.0,152.5,152.5
|
||||
1,2,Squat,Strength,4,3,7.0,142.5,427.5
|
||||
1,2,Squat,Strength,5,3,7.0,142.5,427.5
|
||||
1,2,Bench,Volume,1,4,4.0,102.5,410.0
|
||||
1,2,Bench,Volume,2,4,5.5,107.5,430.0
|
||||
1,2,Bench,Volume,3,4,7.0,112.5,450.0
|
||||
1,2,Bench,Volume,4,4,7.0,112.5,450.0
|
||||
1,2,Close Grip Bench,Volume,1,5,7,87.5,437.5
|
||||
1,2,Close Grip Bench,Volume,2,5,7,87.5,437.5
|
||||
1,3,Bench,Strength,1,5,4.0,100.0,500.0
|
||||
1,3,Bench,Strength,2,3,5.5,112.5,337.5
|
||||
1,3,Bench,Strength,3,1,7.0,125.0,125.0
|
||||
1,3,Bench,Strength,4,3,7.0,117.5,352.5
|
||||
1,3,Bench,Strength,5,3,7.0,117.5,352.5
|
||||
1,3,Deadlift,Volume,1,4,4.0,125.0,500.0
|
||||
1,3,Deadlift,Volume,2,4,5.5,132.5,530.0
|
||||
1,3,Deadlift,Volume,3,4,7.0,137.5,550.0
|
||||
1,3,Deadlift,Volume,4,4,7.0,137.5,550.0
|
||||
1,3,Pause Deadlift,Volume,1,5,7,122.5,612.5
|
||||
1,3,Pause Deadlift,Volume,2,5,7,122.5,612.5
|
||||
1,4,Squat,Strength,1,5,4.5,122.5,612.5
|
||||
1,4,Squat,Strength,2,3,6.0,137.5,412.5
|
||||
1,4,Squat,Strength,3,1,7.5,155.0,155.0
|
||||
1,4,Squat,Strength,4,3,7.5,145.0,435.0
|
||||
1,4,Bench,Strength,1,5,4.5,100.0,500.0
|
||||
1,4,Bench,Strength,2,3,6.0,112.5,337.5
|
||||
1,4,Bench,Strength,3,1,7.5,127.5,127.5
|
||||
1,4,Bench,Strength,4,3,7.5,120.0,360.0
|
||||
1,4,Deadlift,Strength,1,5,4.5,122.5,612.5
|
||||
1,4,Deadlift,Strength,2,3,6.0,137.5,412.5
|
||||
1,4,Deadlift,Strength,3,1,7.5,155.0,155.0
|
||||
1,4,Deadlift,Strength,4,3,7.5,145.0,435.0
|
||||
2,1,Squat,Volume,1,4,4.5,127.5,510.0
|
||||
2,1,Squat,Volume,2,4,6.0,132.5,530.0
|
||||
2,1,Squat,Volume,3,4,7.5,140.0,560.0
|
||||
2,1,Squat,Volume,4,4,7.5,140.0,560.0
|
||||
2,1,Pause Squat,Volume,1,5,7,122.5,612.5
|
||||
2,1,Pause Squat,Volume,2,5,7,122.5,612.5
|
||||
2,1,Deadlift,Strength,1,5,4.5,122.5,612.5
|
||||
2,1,Deadlift,Strength,2,3,6.0,137.5,412.5
|
||||
2,1,Deadlift,Strength,3,1,7.5,155.0,155.0
|
||||
2,1,Deadlift,Strength,4,3,7.5,145.0,435.0
|
||||
2,1,Deadlift,Strength,5,3,7.5,145.0,435.0
|
||||
2,2,Squat,Strength,1,5,4.5,122.5,612.5
|
||||
2,2,Squat,Strength,2,3,6.0,137.5,412.5
|
||||
2,2,Squat,Strength,3,1,7.5,155.0,155.0
|
||||
2,2,Squat,Strength,4,3,7.5,145.0,435.0
|
||||
2,2,Squat,Strength,5,3,7.5,145.0,435.0
|
||||
2,2,Bench,Volume,1,4,4.5,105.0,420.0
|
||||
2,2,Bench,Volume,2,4,6.0,110.0,440.0
|
||||
2,2,Bench,Volume,3,4,7.5,115.0,460.0
|
||||
2,2,Bench,Volume,4,4,7.5,115.0,460.0
|
||||
2,2,Close Grip Bench,Volume,1,5,7,87.5,437.5
|
||||
2,2,Close Grip Bench,Volume,2,5,7,87.5,437.5
|
||||
2,3,Bench,Strength,1,5,4.5,100.0,500.0
|
||||
2,3,Bench,Strength,2,3,6.0,112.5,337.5
|
||||
2,3,Bench,Strength,3,1,7.5,127.5,127.5
|
||||
2,3,Bench,Strength,4,3,7.5,120.0,360.0
|
||||
2,3,Bench,Strength,5,3,7.5,120.0,360.0
|
||||
2,3,Deadlift,Volume,1,4,4.5,127.5,510.0
|
||||
2,3,Deadlift,Volume,2,4,6.0,132.5,530.0
|
||||
2,3,Deadlift,Volume,3,4,7.5,140.0,560.0
|
||||
2,3,Deadlift,Volume,4,4,7.5,140.0,560.0
|
||||
2,3,Pause Deadlift,Volume,1,5,7,122.5,612.5
|
||||
2,3,Pause Deadlift,Volume,2,5,7,122.5,612.5
|
||||
2,4,Squat,Strength,1,5,5.0,125.0,625.0
|
||||
2,4,Squat,Strength,2,3,6.5,140.0,420.0
|
||||
2,4,Squat,Strength,3,1,8.0,157.5,157.5
|
||||
2,4,Squat,Strength,4,3,8.0,147.5,442.5
|
||||
2,4,Bench,Strength,1,5,5.0,102.5,512.5
|
||||
2,4,Bench,Strength,2,3,6.5,115.0,345.0
|
||||
2,4,Bench,Strength,3,1,8.0,130.0,130.0
|
||||
2,4,Bench,Strength,4,3,8.0,120.0,360.0
|
||||
2,4,Deadlift,Strength,1,5,5.0,125.0,625.0
|
||||
2,4,Deadlift,Strength,2,3,6.5,140.0,420.0
|
||||
2,4,Deadlift,Strength,3,1,8.0,157.5,157.5
|
||||
2,4,Deadlift,Strength,4,3,8.0,147.5,442.5
|
||||
3,1,Squat,Volume,1,4,5.0,130.0,520.0
|
||||
3,1,Squat,Volume,2,4,6.5,135.0,540.0
|
||||
3,1,Squat,Volume,3,4,8.0,142.5,570.0
|
||||
3,1,Squat,Volume,4,4,8.0,142.5,570.0
|
||||
3,1,Pause Squat,Volume,1,5,7,122.5,612.5
|
||||
3,1,Pause Squat,Volume,2,5,7,122.5,612.5
|
||||
3,1,Deadlift,Strength,1,5,5.0,125.0,625.0
|
||||
3,1,Deadlift,Strength,2,3,6.5,140.0,420.0
|
||||
3,1,Deadlift,Strength,3,1,8.0,157.5,157.5
|
||||
3,1,Deadlift,Strength,4,3,8.0,147.5,442.5
|
||||
3,1,Deadlift,Strength,5,3,8.0,147.5,442.5
|
||||
3,2,Squat,Strength,1,5,5.0,125.0,625.0
|
||||
3,2,Squat,Strength,2,3,6.5,140.0,420.0
|
||||
3,2,Squat,Strength,3,1,8.0,157.5,157.5
|
||||
3,2,Squat,Strength,4,3,8.0,147.5,442.5
|
||||
3,2,Squat,Strength,5,3,8.0,147.5,442.5
|
||||
3,2,Bench,Volume,1,4,5.0,107.5,430.0
|
||||
3,2,Bench,Volume,2,4,6.5,112.5,450.0
|
||||
3,2,Bench,Volume,3,4,8.0,117.5,470.0
|
||||
3,2,Bench,Volume,4,4,8.0,117.5,470.0
|
||||
3,2,Close Grip Bench,Volume,1,5,7,87.5,437.5
|
||||
3,2,Close Grip Bench,Volume,2,5,7,87.5,437.5
|
||||
3,3,Bench,Strength,1,5,5.0,102.5,512.5
|
||||
3,3,Bench,Strength,2,3,6.5,115.0,345.0
|
||||
3,3,Bench,Strength,3,1,8.0,130.0,130.0
|
||||
3,3,Bench,Strength,4,3,8.0,120.0,360.0
|
||||
3,3,Bench,Strength,5,3,8.0,120.0,360.0
|
||||
3,3,Deadlift,Volume,1,4,5.0,130.0,520.0
|
||||
3,3,Deadlift,Volume,2,4,6.5,135.0,540.0
|
||||
3,3,Deadlift,Volume,3,4,8.0,142.5,570.0
|
||||
3,3,Deadlift,Volume,4,4,8.0,142.5,570.0
|
||||
3,3,Pause Deadlift,Volume,1,5,7,122.5,612.5
|
||||
3,3,Pause Deadlift,Volume,2,5,7,122.5,612.5
|
||||
3,4,Squat,Strength,1,5,5.5,127.5,637.5
|
||||
3,4,Squat,Strength,2,3,7.0,142.5,427.5
|
||||
3,4,Squat,Strength,3,1,8.5,160.0,160.0
|
||||
3,4,Squat,Strength,4,3,8.5,150.0,450.0
|
||||
3,4,Bench,Strength,1,5,5.5,105.0,525.0
|
||||
3,4,Bench,Strength,2,3,7.0,117.5,352.5
|
||||
3,4,Bench,Strength,3,1,8.5,132.5,132.5
|
||||
3,4,Bench,Strength,4,3,8.5,122.5,367.5
|
||||
3,4,Deadlift,Strength,1,5,5.5,127.5,637.5
|
||||
3,4,Deadlift,Strength,2,3,7.0,142.5,427.5
|
||||
3,4,Deadlift,Strength,3,1,8.5,160.0,160.0
|
||||
3,4,Deadlift,Strength,4,3,8.5,150.0,450.0
|
||||
4,1,Squat,Volume,1,4,5.5,132.5,530.0
|
||||
4,1,Squat,Volume,2,4,7.0,137.5,550.0
|
||||
4,1,Squat,Volume,3,4,8.5,145.0,580.0
|
||||
4,1,Squat,Volume,4,4,8.5,145.0,580.0
|
||||
4,1,Pause Squat,Volume,1,5,7,122.5,612.5
|
||||
4,1,Pause Squat,Volume,2,5,7,122.5,612.5
|
||||
4,1,Deadlift,Strength,1,5,5.5,127.5,637.5
|
||||
4,1,Deadlift,Strength,2,3,7.0,142.5,427.5
|
||||
4,1,Deadlift,Strength,3,1,8.5,160.0,160.0
|
||||
4,1,Deadlift,Strength,4,3,8.5,150.0,450.0
|
||||
4,1,Deadlift,Strength,5,3,8.5,150.0,450.0
|
||||
4,2,Squat,Strength,1,5,5.5,127.5,637.5
|
||||
4,2,Squat,Strength,2,3,7.0,142.5,427.5
|
||||
4,2,Squat,Strength,3,1,8.5,160.0,160.0
|
||||
4,2,Squat,Strength,4,3,8.5,150.0,450.0
|
||||
4,2,Squat,Strength,5,3,8.5,150.0,450.0
|
||||
4,2,Bench,Volume,1,5,5.5,105.0,525.0
|
||||
4,2,Bench,Volume,2,5,7.0,110.0,550.0
|
||||
4,2,Bench,Volume,3,5,8.5,115.0,575.0
|
||||
4,2,Close Grip Bench,Volume,1,5,7,87.5,437.5
|
||||
4,2,Close Grip Bench,Volume,2,5,7,87.5,437.5
|
||||
4,3,Bench,Strength,1,5,5.5,105.0,525.0
|
||||
4,3,Bench,Strength,2,3,7.0,117.5,352.5
|
||||
4,3,Bench,Strength,3,1,8.5,132.5,132.5
|
||||
4,3,Bench,Strength,4,3,8.5,122.5,367.5
|
||||
4,3,Bench,Strength,5,3,8.5,122.5,367.5
|
||||
4,3,Deadlift,Volume,1,4,5.5,132.5,530.0
|
||||
4,3,Deadlift,Volume,2,4,7.0,137.5,550.0
|
||||
4,3,Deadlift,Volume,3,4,8.5,145.0,580.0
|
||||
4,3,Deadlift,Volume,4,4,8.5,145.0,580.0
|
||||
4,3,Pause Deadlift,Volume,1,5,7,122.5,612.5
|
||||
4,3,Pause Deadlift,Volume,2,5,7,122.5,612.5
|
||||
4,4,Squat,Strength,1,5,6.0,130.0,650.0
|
||||
4,4,Squat,Strength,2,3,7.5,145.0,435.0
|
||||
4,4,Squat,Strength,3,1,9.0,162.5,162.5
|
||||
4,4,Squat,Strength,4,3,9.0,152.5,457.5
|
||||
4,4,Bench,Strength,1,5,6.0,107.5,537.5
|
||||
4,4,Bench,Strength,2,3,7.5,120.0,360.0
|
||||
4,4,Bench,Strength,3,1,9.0,132.5,132.5
|
||||
4,4,Bench,Strength,4,3,9.0,125.0,375.0
|
||||
4,4,Deadlift,Strength,1,5,6.0,130.0,650.0
|
||||
4,4,Deadlift,Strength,2,3,7.5,145.0,435.0
|
||||
4,4,Deadlift,Strength,3,1,9.0,162.5,162.5
|
||||
4,4,Deadlift,Strength,4,3,9.0,152.5,457.5
|
||||
5,1,Squat,Volume,1,4,3.0,120.0,480.0
|
||||
5,1,Squat,Volume,2,4,4.5,127.5,510.0
|
||||
5,1,Squat,Volume,3,4,6.0,132.5,530.0
|
||||
5,1,Squat,Volume,4,4,6.0,132.5,530.0
|
||||
5,1,Deadlift,Strength,1,5,3.0,115.0,575.0
|
||||
5,1,Deadlift,Strength,2,3,4.5,132.5,397.5
|
||||
5,1,Deadlift,Strength,3,1,6.0,147.5,147.5
|
||||
5,1,Deadlift,Strength,4,2,6.0,142.5,285.0
|
||||
5,2,Squat,Strength,1,5,3.0,115.0,575.0
|
||||
5,2,Squat,Strength,2,3,4.5,132.5,397.5
|
||||
5,2,Squat,Strength,3,1,6.0,147.5,147.5
|
||||
5,2,Squat,Strength,4,2,6.0,142.5,285.0
|
||||
5,2,Bench,Volume,1,5,3.0,95.0,475.0
|
||||
5,2,Bench,Volume,2,5,4.5,100.0,500.0
|
||||
5,2,Bench,Volume,3,5,6.0,107.5,537.5
|
||||
5,3,Bench,Strength,1,5,3.0,95.0,475.0
|
||||
5,3,Bench,Strength,2,3,4.5,107.5,322.5
|
||||
5,3,Bench,Strength,3,1,6.0,120.0,120.0
|
||||
5,3,Bench,Strength,4,2,6.0,117.5,235.0
|
||||
5,3,Deadlift,Volume,1,4,3.0,120.0,480.0
|
||||
5,3,Deadlift,Volume,2,4,4.5,127.5,510.0
|
||||
5,3,Deadlift,Volume,3,4,6.0,132.5,530.0
|
||||
5,3,Deadlift,Volume,4,4,6.0,132.5,530.0
|
||||
5,4,Squat,Strength,1,5,6.0,130.0,650.0
|
||||
5,4,Squat,Strength,2,3,7.0,142.5,427.5
|
||||
5,4,Squat,Strength,3,1,7.0,152.5,152.5
|
||||
5,4,Squat,Strength,4,1,8.5,160.0,160.0
|
||||
5,4,Squat,Strength,5,1,10.0,170.0,170.0
|
||||
5,4,Bench,Strength,1,5,6.0,107.5,537.5
|
||||
5,4,Bench,Strength,2,3,7.0,117.5,352.5
|
||||
5,4,Bench,Strength,3,1,7.0,125.0,125.0
|
||||
5,4,Bench,Strength,4,1,8.5,132.5,132.5
|
||||
5,4,Bench,Strength,5,1,10.0,140.0,140.0
|
||||
5,4,Deadlift,Strength,1,5,6.0,130.0,650.0
|
||||
5,4,Deadlift,Strength,2,3,7.0,142.5,427.5
|
||||
5,4,Deadlift,Strength,3,1,7.0,152.5,152.5
|
||||
5,4,Deadlift,Strength,4,1,8.5,160.0,160.0
|
||||
5,4,Deadlift,Strength,5,1,10.0,170.0,170.0
|
||||
1,1,Squat,Volume,1,5,4.0,100.0,500.0
|
||||
1,1,Squat,Volume,2,5,5.5,107.5,537.5
|
||||
1,1,Squat,Volume,3,5,7.0,112.5,562.5
|
||||
1,1,Squat,Volume,4,5,7.0,112.5,562.5
|
||||
1,1,Pause Squat,Volume,1,6,7,100.0,600.0
|
||||
1,1,Pause Squat,Volume,2,6,7,100.0,600.0
|
||||
1,1,Bench,Volume,1,5,4.0,82.5,412.5
|
||||
1,1,Bench,Volume,2,5,5.5,87.5,437.5
|
||||
1,1,Bench,Volume,3,5,7.0,90.0,450.0
|
||||
1,1,Bench,Volume,4,5,7.0,90.0,450.0
|
||||
1,1,Close Grip Bench,Volume,1,4,7,75.0,300.0
|
||||
1,1,Close Grip Bench,Volume,2,4,7,75.0,300.0
|
||||
1,1,Close Grip Bench,Volume,3,4,7,75.0,300.0
|
||||
1,2,Squat,Volume,1,5,4.0,100.0,500.0
|
||||
1,2,Squat,Volume,2,5,5.5,107.5,537.5
|
||||
1,2,Squat,Volume,3,5,7.0,112.5,562.5
|
||||
1,2,Squat,Volume,4,5,7.0,112.5,562.5
|
||||
1,2,Pause Squat,Volume,1,6,7,100.0,600.0
|
||||
1,2,Pause Squat,Volume,2,6,7,100.0,600.0
|
||||
1,2,Deadlift,Volume,1,5,4.0,110.0,550.0
|
||||
1,2,Deadlift,Volume,2,5,5.5,117.5,587.5
|
||||
1,2,Deadlift,Volume,3,5,7.0,122.5,612.5
|
||||
1,2,Deadlift,Volume,4,5,7.0,122.5,612.5
|
||||
1,2,Pause Deadlift,Volume,1,5,7,112.5,562.5
|
||||
1,2,Pause Deadlift,Volume,2,5,7,112.5,562.5
|
||||
1,3,Bench,Volume,1,5,4.0,82.5,412.5
|
||||
1,3,Bench,Volume,2,5,5.5,87.5,437.5
|
||||
1,3,Bench,Volume,3,5,7.0,90.0,450.0
|
||||
1,3,Bench,Volume,4,5,7.0,90.0,450.0
|
||||
1,3,Close Grip Bench,Volume,1,4,7,75.0,300.0
|
||||
1,3,Close Grip Bench,Volume,2,4,7,75.0,300.0
|
||||
1,3,Close Grip Bench,Volume,3,4,7,75.0,300.0
|
||||
1,3,Deadlift,Volume,1,5,4.0,110.0,550.0
|
||||
1,3,Deadlift,Volume,2,5,5.5,117.5,587.5
|
||||
1,3,Deadlift,Volume,3,5,7.0,122.5,612.5
|
||||
1,3,Deadlift,Volume,4,5,7.0,122.5,612.5
|
||||
1,3,Pause Deadlift,Volume,1,5,7,112.5,562.5
|
||||
1,3,Pause Deadlift,Volume,2,5,7,112.5,562.5
|
||||
1,4,Squat,Strength,1,5,4.5,102.5,512.5
|
||||
1,4,Squat,Strength,2,3,6.0,115.0,345.0
|
||||
1,4,Squat,Strength,3,1,7.5,130.0,130.0
|
||||
1,4,Squat,Strength,4,3,7.5,120.0,360.0
|
||||
1,4,Squat,Strength,5,3,7.5,120.0,360.0
|
||||
1,4,Bench,Strength,1,5,4.5,82.5,412.5
|
||||
1,4,Bench,Strength,2,3,6.0,92.5,277.5
|
||||
1,4,Bench,Strength,3,1,7.5,105.0,105.0
|
||||
1,4,Bench,Strength,4,3,7.5,97.5,292.5
|
||||
1,4,Bench,Strength,5,3,7.5,97.5,292.5
|
||||
1,4,Deadlift,Strength,1,5,4.5,112.5,562.5
|
||||
1,4,Deadlift,Strength,2,3,6.0,125.0,375.0
|
||||
1,4,Deadlift,Strength,3,1,7.5,140.0,140.0
|
||||
1,4,Deadlift,Strength,4,2,7.5,135.0,270.0
|
||||
1,4,Deadlift,Strength,5,2,7.5,135.0,270.0
|
||||
2,1,Squat,Volume,1,5,4.5,102.5,512.5
|
||||
2,1,Squat,Volume,2,5,6.0,107.5,537.5
|
||||
2,1,Squat,Volume,3,5,7.5,112.5,562.5
|
||||
2,1,Squat,Volume,4,5,7.5,112.5,562.5
|
||||
2,1,Pause Squat,Volume,1,6,7,100.0,600.0
|
||||
2,1,Pause Squat,Volume,2,6,7,100.0,600.0
|
||||
2,1,Bench,Volume,1,5,4.5,82.5,412.5
|
||||
2,1,Bench,Volume,2,5,6.0,87.5,437.5
|
||||
2,1,Bench,Volume,3,5,7.5,92.5,462.5
|
||||
2,1,Bench,Volume,4,5,7.5,92.5,462.5
|
||||
2,1,Close Grip Bench,Volume,1,4,7,75.0,300.0
|
||||
2,1,Close Grip Bench,Volume,2,4,7,75.0,300.0
|
||||
2,1,Close Grip Bench,Volume,3,4,7,75.0,300.0
|
||||
2,2,Squat,Volume,1,5,4.5,102.5,512.5
|
||||
2,2,Squat,Volume,2,5,6.0,107.5,537.5
|
||||
2,2,Squat,Volume,3,5,7.5,112.5,562.5
|
||||
2,2,Squat,Volume,4,5,7.5,112.5,562.5
|
||||
2,2,Pause Squat,Volume,1,6,7,100.0,600.0
|
||||
2,2,Pause Squat,Volume,2,6,7,100.0,600.0
|
||||
2,2,Deadlift,Volume,1,6,4.5,107.5,645.0
|
||||
2,2,Deadlift,Volume,2,6,6.0,115.0,690.0
|
||||
2,2,Deadlift,Volume,3,6,7.5,120.0,720.0
|
||||
2,2,Pause Deadlift,Volume,1,5,7,112.5,562.5
|
||||
2,2,Pause Deadlift,Volume,2,5,7,112.5,562.5
|
||||
2,3,Bench,Volume,1,5,4.5,82.5,412.5
|
||||
2,3,Bench,Volume,2,5,6.0,87.5,437.5
|
||||
2,3,Bench,Volume,3,5,7.5,92.5,462.5
|
||||
2,3,Bench,Volume,4,5,7.5,92.5,462.5
|
||||
2,3,Close Grip Bench,Volume,1,4,7,75.0,300.0
|
||||
2,3,Close Grip Bench,Volume,2,4,7,75.0,300.0
|
||||
2,3,Close Grip Bench,Volume,3,4,7,75.0,300.0
|
||||
2,3,Deadlift,Volume,1,6,4.5,107.5,645.0
|
||||
2,3,Deadlift,Volume,2,6,6.0,115.0,690.0
|
||||
2,3,Deadlift,Volume,3,6,7.5,120.0,720.0
|
||||
2,3,Pause Deadlift,Volume,1,5,7,112.5,562.5
|
||||
2,3,Pause Deadlift,Volume,2,5,7,112.5,562.5
|
||||
2,4,Squat,Strength,1,5,5.0,105.0,525.0
|
||||
2,4,Squat,Strength,2,3,6.5,117.5,352.5
|
||||
2,4,Squat,Strength,3,1,8.0,130.0,130.0
|
||||
2,4,Squat,Strength,4,3,8.0,122.5,367.5
|
||||
2,4,Squat,Strength,5,3,8.0,122.5,367.5
|
||||
2,4,Bench,Strength,1,5,5.0,85.0,425.0
|
||||
2,4,Bench,Strength,2,3,6.5,95.0,285.0
|
||||
2,4,Bench,Strength,3,1,8.0,105.0,105.0
|
||||
2,4,Bench,Strength,4,3,8.0,100.0,300.0
|
||||
2,4,Bench,Strength,5,3,8.0,100.0,300.0
|
||||
2,4,Deadlift,Strength,1,5,5.0,115.0,575.0
|
||||
2,4,Deadlift,Strength,2,3,6.5,127.5,382.5
|
||||
2,4,Deadlift,Strength,3,1,8.0,142.5,142.5
|
||||
2,4,Deadlift,Strength,4,4,8.0,130.0,520.0
|
||||
3,1,Squat,Volume,1,5,5.0,105.0,525.0
|
||||
3,1,Squat,Volume,2,5,6.5,110.0,550.0
|
||||
3,1,Squat,Volume,3,5,8.0,115.0,575.0
|
||||
3,1,Squat,Volume,4,5,8.0,115.0,575.0
|
||||
3,1,Pause Squat,Volume,1,6,7,100.0,600.0
|
||||
3,1,Pause Squat,Volume,2,6,7,100.0,600.0
|
||||
3,1,Bench,Volume,1,5,5.0,85.0,425.0
|
||||
3,1,Bench,Volume,2,5,6.5,90.0,450.0
|
||||
3,1,Bench,Volume,3,5,8.0,92.5,462.5
|
||||
3,1,Bench,Volume,4,5,8.0,92.5,462.5
|
||||
3,1,Close Grip Bench,Volume,1,4,7,75.0,300.0
|
||||
3,1,Close Grip Bench,Volume,2,4,7,75.0,300.0
|
||||
3,1,Close Grip Bench,Volume,3,4,7,75.0,300.0
|
||||
3,2,Squat,Volume,1,5,5.0,105.0,525.0
|
||||
3,2,Squat,Volume,2,5,6.5,110.0,550.0
|
||||
3,2,Squat,Volume,3,5,8.0,115.0,575.0
|
||||
3,2,Squat,Volume,4,5,8.0,115.0,575.0
|
||||
3,2,Pause Squat,Volume,1,6,7,100.0,600.0
|
||||
3,2,Pause Squat,Volume,2,6,7,100.0,600.0
|
||||
3,2,Deadlift,Volume,1,6,5.0,110.0,660.0
|
||||
3,2,Deadlift,Volume,2,6,6.5,117.5,705.0
|
||||
3,2,Deadlift,Volume,3,6,8.0,122.5,735.0
|
||||
3,2,Pause Deadlift,Volume,1,5,7,112.5,562.5
|
||||
3,2,Pause Deadlift,Volume,2,5,7,112.5,562.5
|
||||
3,3,Bench,Volume,1,5,5.0,85.0,425.0
|
||||
3,3,Bench,Volume,2,5,6.5,90.0,450.0
|
||||
3,3,Bench,Volume,3,5,8.0,92.5,462.5
|
||||
3,3,Bench,Volume,4,5,8.0,92.5,462.5
|
||||
3,3,Close Grip Bench,Volume,1,4,7,75.0,300.0
|
||||
3,3,Close Grip Bench,Volume,2,4,7,75.0,300.0
|
||||
3,3,Close Grip Bench,Volume,3,4,7,75.0,300.0
|
||||
3,3,Deadlift,Volume,1,6,5.0,110.0,660.0
|
||||
3,3,Deadlift,Volume,2,6,6.5,117.5,705.0
|
||||
3,3,Deadlift,Volume,3,6,8.0,122.5,735.0
|
||||
3,3,Pause Deadlift,Volume,1,5,7,112.5,562.5
|
||||
3,3,Pause Deadlift,Volume,2,5,7,112.5,562.5
|
||||
3,4,Squat,Strength,1,5,5.5,107.5,537.5
|
||||
3,4,Squat,Strength,2,3,7.0,120.0,360.0
|
||||
3,4,Squat,Strength,3,1,8.5,132.5,132.5
|
||||
3,4,Squat,Strength,4,3,8.5,125.0,375.0
|
||||
3,4,Squat,Strength,5,3,8.5,125.0,375.0
|
||||
3,4,Bench,Strength,1,5,5.5,87.5,437.5
|
||||
3,4,Bench,Strength,2,3,7.0,97.5,292.5
|
||||
3,4,Bench,Strength,3,1,8.5,107.5,107.5
|
||||
3,4,Bench,Strength,4,3,8.5,100.0,300.0
|
||||
3,4,Bench,Strength,5,3,8.5,100.0,300.0
|
||||
3,4,Deadlift,Strength,1,5,5.5,117.5,587.5
|
||||
3,4,Deadlift,Strength,2,3,7.0,130.0,390.0
|
||||
3,4,Deadlift,Strength,3,1,8.5,145.0,145.0
|
||||
3,4,Deadlift,Strength,4,4,8.5,132.5,530.0
|
||||
4,1,Squat,Volume,1,5,5.5,107.5,537.5
|
||||
4,1,Squat,Volume,2,5,7.0,112.5,562.5
|
||||
4,1,Squat,Volume,3,5,8.5,117.5,587.5
|
||||
4,1,Squat,Volume,4,5,8.5,117.5,587.5
|
||||
4,1,Pause Squat,Volume,1,6,7,100.0,600.0
|
||||
4,1,Pause Squat,Volume,2,6,7,100.0,600.0
|
||||
4,1,Bench,Volume,1,5,5.5,87.5,437.5
|
||||
4,1,Bench,Volume,2,5,7.0,90.0,450.0
|
||||
4,1,Bench,Volume,3,5,8.5,95.0,475.0
|
||||
4,1,Bench,Volume,4,5,8.5,95.0,475.0
|
||||
4,1,Close Grip Bench,Volume,1,4,7,75.0,300.0
|
||||
4,1,Close Grip Bench,Volume,2,4,7,75.0,300.0
|
||||
4,1,Close Grip Bench,Volume,3,4,7,75.0,300.0
|
||||
4,2,Squat,Volume,1,5,5.5,107.5,537.5
|
||||
4,2,Squat,Volume,2,5,7.0,112.5,562.5
|
||||
4,2,Squat,Volume,3,5,8.5,117.5,587.5
|
||||
4,2,Squat,Volume,4,5,8.5,117.5,587.5
|
||||
4,2,Pause Squat,Volume,1,6,7,100.0,600.0
|
||||
4,2,Pause Squat,Volume,2,6,7,100.0,600.0
|
||||
4,2,Deadlift,Volume,1,4,5.5,120.0,480.0
|
||||
4,2,Deadlift,Volume,2,4,7.0,125.0,500.0
|
||||
4,2,Deadlift,Volume,3,4,8.5,132.5,530.0
|
||||
4,2,Deadlift,Volume,4,4,8.5,132.5,530.0
|
||||
4,2,Pause Deadlift,Volume,1,5,7,112.5,562.5
|
||||
4,2,Pause Deadlift,Volume,2,5,7,112.5,562.5
|
||||
4,3,Bench,Volume,1,5,5.5,87.5,437.5
|
||||
4,3,Bench,Volume,2,5,7.0,90.0,450.0
|
||||
4,3,Bench,Volume,3,5,8.5,95.0,475.0
|
||||
4,3,Bench,Volume,4,5,8.5,95.0,475.0
|
||||
4,3,Close Grip Bench,Volume,1,4,7,75.0,300.0
|
||||
4,3,Close Grip Bench,Volume,2,4,7,75.0,300.0
|
||||
4,3,Close Grip Bench,Volume,3,4,7,75.0,300.0
|
||||
4,3,Deadlift,Volume,1,4,5.5,120.0,480.0
|
||||
4,3,Deadlift,Volume,2,4,7.0,125.0,500.0
|
||||
4,3,Deadlift,Volume,3,4,8.5,132.5,530.0
|
||||
4,3,Deadlift,Volume,4,4,8.5,132.5,530.0
|
||||
4,3,Pause Deadlift,Volume,1,5,7,112.5,562.5
|
||||
4,3,Pause Deadlift,Volume,2,5,7,112.5,562.5
|
||||
4,4,Squat,Strength,1,5,6.0,107.5,537.5
|
||||
4,4,Squat,Strength,2,3,7.5,120.0,360.0
|
||||
4,4,Squat,Strength,3,1,9.0,135.0,135.0
|
||||
4,4,Squat,Strength,4,3,9.0,127.5,382.5
|
||||
4,4,Squat,Strength,5,3,9.0,127.5,382.5
|
||||
4,4,Bench,Strength,1,5,6.0,87.5,437.5
|
||||
4,4,Bench,Strength,2,3,7.5,97.5,292.5
|
||||
4,4,Bench,Strength,3,1,9.0,110.0,110.0
|
||||
4,4,Bench,Strength,4,3,9.0,102.5,307.5
|
||||
4,4,Bench,Strength,5,3,9.0,102.5,307.5
|
||||
4,4,Deadlift,Strength,1,5,6.0,117.5,587.5
|
||||
4,4,Deadlift,Strength,2,3,7.5,132.5,397.5
|
||||
4,4,Deadlift,Strength,3,1,9.0,147.5,147.5
|
||||
4,4,Deadlift,Strength,4,4,9.0,135.0,540.0
|
||||
5,1,Squat,Volume,1,5,3.0,97.5,487.5
|
||||
5,1,Squat,Volume,2,5,4.5,102.5,512.5
|
||||
5,1,Squat,Volume,3,5,6.0,107.5,537.5
|
||||
5,1,Squat,Volume,4,5,6.0,107.5,537.5
|
||||
5,1,Bench,Volume,1,5,3.0,77.5,387.5
|
||||
5,1,Bench,Volume,2,5,4.5,82.5,412.5
|
||||
5,1,Bench,Volume,3,5,6.0,87.5,437.5
|
||||
5,1,Bench,Volume,4,5,6.0,87.5,437.5
|
||||
5,2,Squat,Volume,1,5,3.0,97.5,487.5
|
||||
5,2,Squat,Volume,2,5,4.5,102.5,512.5
|
||||
5,2,Squat,Volume,3,5,6.0,107.5,537.5
|
||||
5,2,Squat,Volume,4,5,6.0,107.5,537.5
|
||||
5,2,Deadlift,Volume,1,4,3.0,110.0,440.0
|
||||
5,2,Deadlift,Volume,2,4,4.5,117.5,470.0
|
||||
5,2,Deadlift,Volume,3,4,6.0,122.5,490.0
|
||||
5,2,Deadlift,Volume,4,4,6.0,122.5,490.0
|
||||
5,3,Bench,Volume,1,5,3.0,77.5,387.5
|
||||
5,3,Bench,Volume,2,5,4.5,82.5,412.5
|
||||
5,3,Bench,Volume,3,5,6.0,87.5,437.5
|
||||
5,3,Bench,Volume,4,5,6.0,87.5,437.5
|
||||
5,3,Deadlift,Volume,1,4,3.0,110.0,440.0
|
||||
5,3,Deadlift,Volume,2,4,4.5,117.5,470.0
|
||||
5,3,Deadlift,Volume,3,4,6.0,122.5,490.0
|
||||
5,3,Deadlift,Volume,4,4,6.0,122.5,490.0
|
||||
5,4,Squat,Strength,1,5,6.0,107.5,537.5
|
||||
5,4,Squat,Strength,2,3,7.0,120.0,360.0
|
||||
5,4,Squat,Strength,3,1,7.0,127.5,127.5
|
||||
5,4,Squat,Strength,4,1,8.5,132.5,132.5
|
||||
5,4,Squat,Strength,5,1,10.0,142.5,142.5
|
||||
5,4,Bench,Strength,1,5,6.0,87.5,437.5
|
||||
5,4,Bench,Strength,2,3,7.0,97.5,292.5
|
||||
5,4,Bench,Strength,3,1,7.0,102.5,102.5
|
||||
5,4,Bench,Strength,4,1,8.5,107.5,107.5
|
||||
5,4,Bench,Strength,5,1,10.0,115.0,115.0
|
||||
5,4,Deadlift,Strength,1,5,6.0,117.5,587.5
|
||||
5,4,Deadlift,Strength,2,3,7.0,130.0,390.0
|
||||
5,4,Deadlift,Strength,3,1,7.0,137.5,137.5
|
||||
5,4,Deadlift,Strength,4,1,8.5,145.0,145.0
|
||||
5,4,Deadlift,Strength,5,1,10.0,155.0,155.0
|
||||
|
+67
-8
@@ -1,16 +1,44 @@
|
||||
from language import Plan, LiftGoal
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from datetime import date
|
||||
|
||||
sample_plan = Plan(squat_plan = [LiftGoal.VOLUME, LiftGoal.STRENGTH, LiftGoal.STRENGTH],
|
||||
bench_plan = [LiftGoal.VOLUME, LiftGoal.STRENGTH, LiftGoal.STRENGTH],
|
||||
deadlift_plan = [LiftGoal.STRENGTH, LiftGoal.VOLUME, LiftGoal.STRENGTH],
|
||||
from language import Plan, Lift, LiftGoal
|
||||
from language import opengym
|
||||
|
||||
sample_plan = Plan(squat_plan = [LiftGoal.VOLUME, LiftGoal.VOLUME, LiftGoal.STRENGTH],
|
||||
bench_plan = [LiftGoal.VOLUME, LiftGoal.VOLUME, LiftGoal.STRENGTH],
|
||||
deadlift_plan = [LiftGoal.VOLUME, LiftGoal.VOLUME, LiftGoal.STRENGTH],
|
||||
squat_targets=(3100, 2000),
|
||||
bench_targets=(2500, 1650),
|
||||
deadlift_targets=(3100, 2000),
|
||||
squat_1rm=170, bench_1rm=140, deadlift_1rm=170,
|
||||
num_weeks=4, num_workouts=4
|
||||
squat_1rm=142, bench_1rm=115, deadlift_1rm=155,
|
||||
rpe_targets=[7.0, 7.5, 8.0, 8.5], num_workouts=4
|
||||
)
|
||||
|
||||
for (i, week) in enumerate(sample_plan.generate_full_plan()):
|
||||
# Accessories by day of the week, in every week but the taper/mock meet week:
|
||||
# (name in opengym.ACCESSORY_IDS, sets, reps, insert before this exercise id, or None for the end).
|
||||
BENCH = opengym.EXERCISE_IDS[Lift.BENCH]
|
||||
ACCESSORIES = {
|
||||
1: [("external rotation", 1, 10, BENCH), ("triceps pushdown", 2, 10, None), ("reverse fly", 2, 10, None)],
|
||||
2: [("incline curl", 2, 10, None)],
|
||||
3: [("triceps pushdown", 2, 10, None), ("lateral raise", 2, 10, None)],
|
||||
}
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--opengym", action="store_true", help="push the plan to OpenGym as a rotation")
|
||||
parser.add_argument("--opengym-dry-run", action="store_true", help="show what --opengym would change, write nothing")
|
||||
parser.add_argument("--opengym-replace", action="store_true", help="replace routines from an earlier push")
|
||||
parser.add_argument("--opengym-migrate", nargs=2, metavar=("OLD_ID", "NEW_ID"),
|
||||
help="re-file logged history from one exercise id to another (with --opengym or --opengym-dry-run)")
|
||||
parser.add_argument("--opengym-accessories", action="store_true",
|
||||
help="add ACCESSORIES to routines already pushed, leaving the queue alone")
|
||||
parser.add_argument("--start", type=date.fromisoformat, default=date.today(), help="first day of the block")
|
||||
parser.add_argument("--label", default="Powerlifting block", help="rotation name in OpenGym")
|
||||
args = parser.parse_args()
|
||||
|
||||
full_plan = sample_plan.generate_full_plan()
|
||||
for (i, week) in enumerate(full_plan):
|
||||
print()
|
||||
print(f"Week {i + 1}:")
|
||||
for (j, day) in enumerate(week):
|
||||
@@ -18,6 +46,37 @@ for (i, week) in enumerate(sample_plan.generate_full_plan()):
|
||||
total_volume = sum(lift.total_volume for lift in day)
|
||||
for lift in day:
|
||||
print(f" {lift.lift}: {lift.sets}")
|
||||
print(f" Total Volume: {total_volume * 2.2165:.2f} lbs")
|
||||
print(f" Total Volume: {total_volume * 2.20462:.2f} lbs")
|
||||
|
||||
sample_plan.to_csv("sample_plan.csv")
|
||||
|
||||
if args.opengym or args.opengym_dry_run or args.opengym_accessories:
|
||||
client = opengym.Client(os.environ.get("OPENGYM_URL") or opengym.resolve_base_url())
|
||||
client.login(*opengym.rbw_credentials(os.environ.get("OPENGYM_RBW_ENTRY", "gym.rcjohnstone.com")))
|
||||
routines = opengym.plan_to_routines(full_plan)
|
||||
|
||||
def change(state):
|
||||
if args.opengym_accessories:
|
||||
opengym.add_accessories(state["routines"], ACCESSORIES, state.get("exWeights") or {})
|
||||
return
|
||||
opengym.add_accessories(routines, ACCESSORIES, state.get("exWeights") or {})
|
||||
if args.opengym_migrate:
|
||||
n = opengym.migrate_exercise(state, *args.opengym_migrate)
|
||||
print(f"migrate: {n} workouts re-filed from {args.opengym_migrate[0]} to {args.opengym_migrate[1]}")
|
||||
print(opengym.describe_change(state, routines))
|
||||
opengym.apply_plan(state, routines, args.start, args.label, args.opengym_replace)
|
||||
|
||||
print()
|
||||
if args.opengym_dry_run:
|
||||
state, rev = client.get_state()
|
||||
change(state)
|
||||
if args.opengym_accessories:
|
||||
for r in state["routines"]:
|
||||
print(r["name"], [e["id"] for e in r["ex"]])
|
||||
print(f"dry run against rev {rev}: nothing written")
|
||||
sys.exit(0)
|
||||
rev = client.update_state(change)
|
||||
if args.opengym_accessories:
|
||||
print(f"added accessories; OpenGym is now at rev {rev}")
|
||||
sys.exit(0)
|
||||
print(f"pushed {len(routines)} routines starting {args.start}; OpenGym is now at rev {rev}")
|
||||
@@ -0,0 +1,320 @@
|
||||
"""Push a generated Plan to an OpenGym instance as a rotation of routines.
|
||||
|
||||
OpenGym keeps one state document per user. Writing means GET /api/data, change it, and
|
||||
PUT it back with baseRev/baseWid; the server rejects the PUT with 409 if anything else
|
||||
wrote in between. Entries left out of a PUT are recorded as deletions on every device, so
|
||||
everything here only adds to or edits the state it just read.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from collections import Counter
|
||||
from datetime import date
|
||||
from itertools import groupby
|
||||
|
||||
from .lifts import Lift, LiftGoal, LiftSession
|
||||
|
||||
# OpenGym library exercise ids.
|
||||
EXERCISE_IDS = {
|
||||
Lift.SQUAT: "1435", # barbell low bar squat
|
||||
Lift.HIGH_BAR_SQUAT: "1436", # barbell high bar squat
|
||||
Lift.PAUSE_SQUAT: "5017", # pause squat
|
||||
Lift.BENCH: "0025", # barbell bench press
|
||||
Lift.CLOSE_GRIP_BENCH: "0030", # barbell close-grip bench press
|
||||
Lift.LARSEN_PRESS: "6270", # barbell larsen press
|
||||
Lift.DEADLIFT: "0032", # barbell deadlift
|
||||
Lift.PAUSE_DEADLIFT: "5014", # barbell pause deadlift
|
||||
}
|
||||
|
||||
# Accessories have no 1RM; their planned weight is the last one logged, if any.
|
||||
ACCESSORY_IDS = {
|
||||
"external rotation": "0235", # cable standing shoulder external rotation
|
||||
"reverse fly": "0383", # dumbbell reverse fly
|
||||
"lateral raise": "0334", # dumbbell lateral raise
|
||||
"triceps pushdown": "0241", # cable triceps pushdown (v-bar)
|
||||
"incline curl": "0318", # dumbbell incline curl
|
||||
}
|
||||
|
||||
# Not in the library; added to customEx on first use.
|
||||
HALT_DEADLIFT = {
|
||||
"id": "stack-halt-deadlift",
|
||||
"n": "halt deadlift (barbell)",
|
||||
"bp": "upper legs",
|
||||
"eq": "barbell",
|
||||
"tg": "glutes",
|
||||
"desc": "Deadlift from the floor to just above the knee, paused, then lowered.",
|
||||
"custom": True,
|
||||
}
|
||||
|
||||
ID_PREFIX = "stack-"
|
||||
ROTATION_ID = "stack-rotation"
|
||||
API_CONTAINER = "connor_opengym-api_1"
|
||||
API_NETWORK = "connor_internal"
|
||||
|
||||
|
||||
class OpenGymError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def resolve_base_url() -> str:
|
||||
"""Find the API container's address; Authelia blocks scripts on the public URL."""
|
||||
fmt = f'{{{{(index .NetworkSettings.Networks "{API_NETWORK}").IPAddress}}}}'
|
||||
out = subprocess.run(
|
||||
["sudo", "podman", "inspect", API_CONTAINER, "--format", fmt],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
ip = out.stdout.strip()
|
||||
if out.returncode != 0 or not ip:
|
||||
raise OpenGymError(
|
||||
f"could not find the {API_CONTAINER} address ({out.stderr.strip()}); set OPENGYM_URL"
|
||||
)
|
||||
return f"http://{ip}:3000"
|
||||
|
||||
|
||||
def rbw_credentials(entry: str) -> tuple[str, str]:
|
||||
"""Read (username, password) for an rbw entry."""
|
||||
def get(*args: str) -> str:
|
||||
out = subprocess.run(["rbw", "get", *args, entry], capture_output=True, text=True)
|
||||
if out.returncode != 0:
|
||||
raise OpenGymError(f"rbw get {entry} failed: {out.stderr.strip()}")
|
||||
return out.stdout.strip()
|
||||
|
||||
return get("--field", "username"), get()
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self, base_url: str, token: str | None = None):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.token = token
|
||||
|
||||
def _request(self, method: str, path: str, body: dict | None = None):
|
||||
data = json.dumps(body).encode() if body is not None else None
|
||||
req = urllib.request.Request(self.base_url + path, data=data, method=method)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
if self.token:
|
||||
req.add_header("Authorization", f"Bearer {self.token}")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return resp.status, resp.headers, json.loads(resp.read() or b"null")
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.headers, json.loads(e.read() or b"null")
|
||||
|
||||
def login(self, identifier: str, password: str) -> None:
|
||||
status, headers, body = self._request(
|
||||
"POST", "/api/login/password", {"identifier": identifier, "password": password}
|
||||
)
|
||||
if status != 200:
|
||||
raise OpenGymError(f"login failed: HTTP {status} {body}")
|
||||
for cookie in headers.get_all("Set-Cookie") or []:
|
||||
name, _, rest = cookie.partition("=")
|
||||
if name.strip() in ("__Host-gymsid", "gymsid"):
|
||||
self.token = rest.split(";", 1)[0]
|
||||
return
|
||||
raise OpenGymError("login succeeded but no session cookie came back")
|
||||
|
||||
def get_state(self) -> tuple[dict, int]:
|
||||
status, _, body = self._request("GET", "/api/data")
|
||||
if status != 200:
|
||||
raise OpenGymError(f"GET /api/data failed: HTTP {status} {body}")
|
||||
return body["state"], body["rev"]
|
||||
|
||||
def update_state(self, change, attempts: int = 3) -> int:
|
||||
"""GET, apply change(state) in place, PUT with baseRev; retry from a fresh GET on 409.
|
||||
|
||||
The state as read is saved under ~/.local/state/stack/ before each PUT.
|
||||
"""
|
||||
backup_dir = os.path.join(
|
||||
os.environ.get("XDG_STATE_HOME") or os.path.expanduser("~/.local/state"), "stack"
|
||||
)
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
for _ in range(attempts):
|
||||
state, rev = self.get_state()
|
||||
backup = os.path.join(backup_dir, f"opengym-rev{rev}-{int(time.time())}.json")
|
||||
with open(os.open(backup, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600), "w") as f:
|
||||
json.dump({"state": state, "rev": rev}, f)
|
||||
print(f"backup: {backup}")
|
||||
change(state)
|
||||
status, _, body = self._request(
|
||||
"PUT", "/api/data", {"state": state, "baseRev": rev, "baseWid": state.get("_wid")}
|
||||
)
|
||||
if status == 200:
|
||||
return body["rev"]
|
||||
if status != 409:
|
||||
raise OpenGymError(f"PUT /api/data failed: HTTP {status} {body}")
|
||||
raise OpenGymError(f"PUT /api/data kept conflicting after {attempts} attempts")
|
||||
|
||||
|
||||
def _rpe(x: float) -> str:
|
||||
return f"{x:g}"
|
||||
|
||||
|
||||
def _set_summary(session: LiftSession) -> str:
|
||||
"""Per-set RPEs, which OpenGym has no per-set field for, e.g. '5@4, 3@5.5, 1@7, 2x3@7'."""
|
||||
parts = []
|
||||
for (reps, rpe), group in groupby((s.reps, s.rpe) for s in session.sets):
|
||||
n = len(list(group))
|
||||
parts.append(f"{n}x{reps}@{rpe:g}" if n > 1 else f"{reps}@{rpe:g}")
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def exercise_entry(session: LiftSession) -> dict:
|
||||
"""One routine exercise with every set's reps and weight fixed and progression off."""
|
||||
reps = [s.reps for s in session.sets]
|
||||
top_rpe = max(s.rpe for s in session.sets)
|
||||
rest = 240 if session.goal == LiftGoal.STRENGTH else 180
|
||||
exercise_id = (
|
||||
HALT_DEADLIFT["id"] if session.lift == Lift.HALT_DEADLIFT else EXERCISE_IDS[session.lift]
|
||||
)
|
||||
return {
|
||||
"id": exercise_id,
|
||||
"sets": len(session.sets),
|
||||
"mode": "reps",
|
||||
"reps": Counter(reps).most_common(1)[0][0],
|
||||
"weight": session.sets[-1].weight,
|
||||
"prog": "off",
|
||||
"pyramid": reps,
|
||||
"pyramidWeight": [s.weight for s in session.sets],
|
||||
"restSec": rest,
|
||||
"warmupSets": 0,
|
||||
"rpeMin": top_rpe,
|
||||
"rpeMax": top_rpe,
|
||||
"note": f"{session.goal}: {_set_summary(session)} (reps@RPE)",
|
||||
}
|
||||
|
||||
|
||||
def plan_to_routines(full_plan: list[list[list[LiftSession]]]) -> list[dict]:
|
||||
"""One routine per training day, in order, named 'W1 D2 - Squat / Bench'."""
|
||||
routines = []
|
||||
last_week = len(full_plan)
|
||||
for w, week in enumerate(full_plan, 1):
|
||||
for d, day in enumerate(week, 1):
|
||||
main_lifts = [str(s.lift) for s in day if s.lift in (Lift.SQUAT, Lift.BENCH, Lift.DEADLIFT)]
|
||||
title = "Mock meet" if w == last_week and d == len(week) else " / ".join(dict.fromkeys(main_lifts))
|
||||
routines.append({
|
||||
"id": f"{ID_PREFIX}w{w}d{d}",
|
||||
"name": f"W{w} D{d} – {title}",
|
||||
"emoji": "barbell",
|
||||
"excludeFromProgression": True,
|
||||
"ex": [exercise_entry(s) for s in day],
|
||||
})
|
||||
return routines
|
||||
|
||||
|
||||
def accessory_entry(exercise_id: str, sets: int, reps: int, weight: float) -> dict:
|
||||
return {
|
||||
"id": exercise_id,
|
||||
"sets": sets,
|
||||
"mode": "reps",
|
||||
"reps": reps,
|
||||
"weight": weight,
|
||||
"prog": "off",
|
||||
"restSec": 90,
|
||||
"warmupSets": 0,
|
||||
"rpeMin": 7,
|
||||
"rpeMax": 8,
|
||||
"note": f"Accessory: {sets}x{reps}, 2-3 reps short of failure",
|
||||
}
|
||||
|
||||
|
||||
def add_accessories(
|
||||
routines: list[dict], accessories: dict[int, list[tuple]], ex_weights: dict
|
||||
) -> None:
|
||||
"""Add accessories to routines in place, by day of the week, in every week but the last.
|
||||
|
||||
accessories maps day (1-based) to (name, sets, reps, before) tuples, where name is a key
|
||||
of ACCESSORY_IDS and before is an exercise id to insert ahead of, or None for the end.
|
||||
Exercises a routine already has are skipped, so this can run on routines pushed earlier.
|
||||
"""
|
||||
weeks = [_week_day(r)[0] for r in routines if _is_ours(r)]
|
||||
last_week = max(weeks, default=0)
|
||||
for routine in routines:
|
||||
if not _is_ours(routine):
|
||||
continue
|
||||
week, day = _week_day(routine)
|
||||
if week == last_week:
|
||||
continue
|
||||
for name, sets, reps, before in accessories.get(day, []):
|
||||
exercise_id = ACCESSORY_IDS[name]
|
||||
if any(e["id"] == exercise_id for e in routine["ex"]):
|
||||
continue
|
||||
weight = (ex_weights.get(exercise_id) or {}).get("w", 0)
|
||||
entry = accessory_entry(exercise_id, sets, reps, weight)
|
||||
ids = [e["id"] for e in routine["ex"]]
|
||||
at = ids.index(before) if before in ids else len(ids)
|
||||
routine["ex"].insert(at, entry)
|
||||
|
||||
|
||||
def _week_day(routine: dict) -> tuple[int, int]:
|
||||
"""(week, day) from an id made by plan_to_routines, e.g. stack-w2d3 -> (2, 3)."""
|
||||
week, _, day = routine["id"].removeprefix(f"{ID_PREFIX}w").partition("d")
|
||||
return int(week), int(day)
|
||||
|
||||
|
||||
def _is_ours(routine: dict) -> bool:
|
||||
return str(routine.get("id", "")).startswith(ID_PREFIX)
|
||||
|
||||
|
||||
def apply_plan(state: dict, routines: list[dict], start: date, label: str, replace: bool) -> None:
|
||||
"""Add the routines to state and queue them as the active rotation, in place."""
|
||||
existing = [r for r in state.get("routines") or [] if _is_ours(r)]
|
||||
if existing and not replace:
|
||||
raise OpenGymError(
|
||||
f"{len(existing)} routines from an earlier push are already there; use --opengym-replace"
|
||||
)
|
||||
state["routines"] = [r for r in state.get("routines") or [] if not _is_ours(r)] + routines
|
||||
|
||||
uses_halt = any(e["id"] == HALT_DEADLIFT["id"] for r in routines for e in r["ex"])
|
||||
custom = state.setdefault("customEx", [])
|
||||
if uses_halt and not any(c.get("id") == HALT_DEADLIFT["id"] for c in custom):
|
||||
custom.append(dict(HALT_DEADLIFT))
|
||||
|
||||
ids = [r["id"] for r in routines]
|
||||
state["rotation"] = {"id": ROTATION_ID, "sequence": ids, "label": label}
|
||||
state["queue"] = {
|
||||
"ids": ids,
|
||||
"since": int(time.time() * 1000),
|
||||
"startsOn": start.isoformat(),
|
||||
"label": label,
|
||||
"rotationId": ROTATION_ID,
|
||||
}
|
||||
state["scheduleMode"] = "rotation"
|
||||
|
||||
|
||||
def describe_change(state: dict, routines: list[dict]) -> str:
|
||||
"""What apply_plan would change, for a dry run."""
|
||||
lines = []
|
||||
ours = [r for r in state.get("routines") or [] if _is_ours(r)]
|
||||
others = len(state.get("routines") or []) - len(ours)
|
||||
lines.append(f"routines: {others} of yours kept, {len(ours)} from an earlier push replaced, {len(routines)} added")
|
||||
if state.get("rotation") and state["rotation"].get("id") != ROTATION_ID:
|
||||
lines.append(f"rotation: replaces '{state['rotation'].get('label')}'")
|
||||
if state.get("scheduleMode") != "rotation":
|
||||
lines.append(f"scheduleMode: {state.get('scheduleMode')} -> rotation")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def migrate_exercise(state: dict, old_id: str, new_id: str) -> int:
|
||||
"""Re-file logged history from old_id to new_id in place; returns workouts changed."""
|
||||
changed = 0
|
||||
for workout in state.get("workouts") or []:
|
||||
entries = workout.get("entries") or []
|
||||
if any(e.get("id") == old_id for e in entries):
|
||||
if any(e.get("id") == new_id for e in entries):
|
||||
raise OpenGymError(
|
||||
f"workout {workout.get('id')} on {workout.get('d')} logs both {old_id} and {new_id}"
|
||||
)
|
||||
for e in entries:
|
||||
if e.get("id") == old_id:
|
||||
e["id"] = new_id
|
||||
changed += 1
|
||||
weights = state.get("exWeights") or {}
|
||||
if old_id in weights:
|
||||
old = weights.pop(old_id)
|
||||
new = weights.get(new_id)
|
||||
if new is None or old.get("d", "") > new.get("d", ""):
|
||||
weights[new_id] = old
|
||||
return changed
|
||||
+32
-6
@@ -4,7 +4,7 @@ import warnings
|
||||
from itertools import product
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, computed_field, model_validator
|
||||
from pydantic import BaseModel, model_validator
|
||||
|
||||
from .lifts import Lift, LiftGoal, LiftSession, WorkoutItem, Sport
|
||||
from .session import generate_lift_session, generate_mock_comp_session
|
||||
@@ -31,8 +31,11 @@ class Plan(BaseModel):
|
||||
|
||||
# Fields with defaults
|
||||
sport: Sport = Sport.POWERLIFTING
|
||||
num_weeks: int = 4
|
||||
num_workouts: int = 4
|
||||
# One RPE per training week; the block length is len(rpe_targets). The taper/mock comp
|
||||
# week is added after these. If omitted, num_weeks picks a default progression.
|
||||
rpe_targets: list[float] | None = None
|
||||
num_weeks: int | None = None
|
||||
|
||||
# Internal constants
|
||||
_RPE_TARGETS = {
|
||||
@@ -55,10 +58,33 @@ class Plan(BaseModel):
|
||||
)
|
||||
return self
|
||||
|
||||
@computed_field
|
||||
def rpe_targets(self) -> list[float]:
|
||||
"""Get RPE targets based on number of weeks."""
|
||||
return self._RPE_TARGETS[self.num_weeks]
|
||||
@model_validator(mode="after")
|
||||
def resolve_rpe_targets(self) -> "Plan":
|
||||
"""Fill in rpe_targets/num_weeks from each other and check the RPEs are usable."""
|
||||
if self.rpe_targets is None:
|
||||
weeks = 4 if self.num_weeks is None else self.num_weeks
|
||||
if weeks not in self._RPE_TARGETS:
|
||||
raise ValueError(
|
||||
f"no default RPE progression for {weeks} weeks "
|
||||
f"(defaults exist for {sorted(self._RPE_TARGETS)}); pass rpe_targets"
|
||||
)
|
||||
self.rpe_targets = list(self._RPE_TARGETS[weeks])
|
||||
if not self.rpe_targets:
|
||||
raise ValueError("rpe_targets must contain at least one week")
|
||||
if self.num_weeks is None:
|
||||
self.num_weeks = len(self.rpe_targets)
|
||||
elif self.num_weeks != len(self.rpe_targets):
|
||||
raise ValueError(
|
||||
f"num_weeks={self.num_weeks} but rpe_targets has {len(self.rpe_targets)} entries"
|
||||
)
|
||||
# Ramp sets go down to RPE - 3 and the SBD day adds 0.5, and the RPE table covers
|
||||
# 3.0-10.0 in half steps, so a week's RPE has to be a half step in 6.0-9.5.
|
||||
for rpe in self.rpe_targets:
|
||||
if rpe * 2 != int(rpe * 2) or not 6.0 <= rpe <= 9.5:
|
||||
raise ValueError(
|
||||
f"weekly RPE {rpe} must be a multiple of 0.5 between 6.0 and 9.5"
|
||||
)
|
||||
return self
|
||||
|
||||
def generate_week(self) -> list[list[WorkoutItem]]:
|
||||
"""Generate optimal workout distribution via exhaustive search.
|
||||
|
||||
Reference in new issue
Block a user