-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsync_exercises.py
More file actions
185 lines (149 loc) · 6 KB
/
sync_exercises.py
File metadata and controls
185 lines (149 loc) · 6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
"""
Historical exercise sync script for MyFitnessPal to Postgres.
This script syncs exercise entries from MyFitnessPal to Postgres database.
Use this for initial historical backfill or re-syncing after schema changes.
After that, regular sync.py will include exercises automatically.
"""
import sys
import time
from datetime import date, timedelta
try:
import config
except ImportError:
print("\n" + "=" * 60)
print("ERROR: config.py not found!")
print("=" * 60)
print("\nPlease follow these steps:")
print("1. Copy config.example.py to config.py")
print("2. Edit config.py with your Postgres credentials")
print("3. Run this script again")
sys.exit(1)
try:
import myfitnesspal
except ImportError:
print("\n" + "=" * 60)
print("ERROR: myfitnesspal library not installed")
print("=" * 60)
print("\nPlease install it:")
print(" pip install myfitnesspal")
print("\nOr install all dependencies:")
print(" pip install -r requirements.txt")
sys.exit(1)
from auth import ensure_logged_in
from db_schema import upsert_exercise
from models import parse_exercise
def get_date_value(date_config):
"""
Get actual date value from config (handles callables, date objects, and strings).
Args:
date_config: Can be date object, callable, or string
Returns:
date object
"""
if callable(date_config):
return date_config()
elif isinstance(date_config, date):
return date_config
elif isinstance(date_config, str):
# Parse string format: YYYY-MM-DD
return date.fromisoformat(date_config)
else:
raise ValueError(f"Invalid date configuration: {date_config}")
def main():
"""Main exercise sync function. Returns exit code for Task Scheduler."""
print("=" * 60)
print("MyFitnessPal Exercise Sync")
print("=" * 60)
# Authenticate
print("\n" + "=" * 60)
print("AUTHENTICATION")
print("=" * 60)
try:
client = ensure_logged_in()
except TimeoutError as e:
print(f"\n[TIMEOUT] {e}")
return 2 # Exit code 2 = Timeout waiting for login
except Exception as e:
print(f"\n[ERROR] Authentication failed: {e}")
return 1 # Exit code 1 = Authentication error
# Get date range from config
start_date = get_date_value(config.START_DATE)
end_date = get_date_value(config.END_DATE)
# Validate date range
if start_date > end_date:
print(f"\nERROR: START_DATE ({start_date}) cannot be after END_DATE ({end_date})")
return 1
# Calculate days to sync
days_to_sync = (end_date - start_date).days + 1
print("\n" + "=" * 60)
print("SYNCING EXERCISES")
print("=" * 60)
print(f"Database: {config.POSTGRES_CONFIG['database']}")
print(f"Date range: {start_date} to {end_date} ({days_to_sync} days)")
print(f"Rate limit: {config.RATE_LIMIT_SECONDS} seconds between requests")
print("=" * 60)
total_exercises = 0
errors = []
# Iterate through date range
current_date = start_date
day_num = 1
while current_date <= end_date:
print(f"\n[{day_num}/{days_to_sync}] {current_date.strftime('%Y-%m-%d')}... ", end='', flush=True)
target_date = current_date
try:
day = client.get_date(target_date.year, target_date.month, target_date.day)
# Sync exercises
exercise_count = 0
if hasattr(day, 'exercises') and day.exercises:
for exercise in day.exercises:
# Get exercise type (cardiovascular or strength training)
exercise_type = exercise.name.lower()
# Get all entries for this exercise type
entries = exercise.get_as_list()
# Create one database row per entry
for entry in entries:
try:
data = parse_exercise(target_date, exercise_type, entry)
upsert_exercise(data)
exercise_count += 1
# Print exercise details
entry_name = entry.get('name', 'unknown')
calories = data.get('calories_burned', 0)
print(f"\n + {entry_name}: {calories} cal", end='', flush=True)
except Exception as e:
entry_name = entry.get('name', 'unknown') if isinstance(entry, dict) else 'unknown'
error_msg = f"{target_date} - Exercise '{entry_name}': {e}"
errors.append(error_msg)
print(f"\n ERROR: {error_msg}", end='', flush=True)
total_exercises += exercise_count
if exercise_count == 0:
print("No exercises")
else:
print(f"\n Total: {exercise_count} exercises")
except Exception as e:
print(f"ERROR: {e}")
errors.append(f"{target_date} - Full day error: {e}")
# Move to next date
current_date += timedelta(days=1)
day_num += 1
# Rate limiting (don't wait after last request)
if current_date <= end_date:
time.sleep(config.RATE_LIMIT_SECONDS)
# Summary
print("\n" + "=" * 60)
print("SYNC COMPLETE")
print("=" * 60)
print(f"Total exercises synced: {total_exercises}")
if errors:
print(f"\nWarning: {len(errors)} errors encountered")
print("\nFirst 10 errors:")
for error in errors[:10]:
print(f" - {error}")
return 3 # Exit code 3 = Partial success (some errors)
else:
print("\nNo errors!")
return 0 # Exit code 0 = Success
print("\n" + "=" * 60)
if __name__ == "__main__":
exit_code = main()
sys.exit(exit_code)