garsync 0.1.2__py3-none-any.whl
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.
- garsync/__init__.py +3 -0
- garsync/__main__.py +4 -0
- garsync/api/__init__.py +5 -0
- garsync/api/deps.py +38 -0
- garsync/api/main.py +83 -0
- garsync/api/routes/__init__.py +1 -0
- garsync/api/routes/activities.py +38 -0
- garsync/api/routes/biometrics.py +26 -0
- garsync/api/routes/sleep.py +26 -0
- garsync/api/routes/stats.py +124 -0
- garsync/api/routes/sync.py +38 -0
- garsync/api/schemas.py +121 -0
- garsync/cli.py +108 -0
- garsync/client.py +169 -0
- garsync/db/__init__.py +20 -0
- garsync/db/connection.py +19 -0
- garsync/db/repository.py +400 -0
- garsync/db/schema.py +88 -0
- garsync/exporter.py +20 -0
- garsync/models.py +47 -0
- garsync/pipeline.py +128 -0
- garsync-0.1.2.dist-info/METADATA +85 -0
- garsync-0.1.2.dist-info/RECORD +25 -0
- garsync-0.1.2.dist-info/WHEEL +4 -0
- garsync-0.1.2.dist-info/entry_points.txt +3 -0
garsync/db/schema.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""DDL definitions and schema migration for garsync SQLite database."""
|
|
2
|
+
|
|
3
|
+
import sqlite3
|
|
4
|
+
|
|
5
|
+
CURRENT_VERSION = 1
|
|
6
|
+
|
|
7
|
+
_SCHEMA_V1 = """
|
|
8
|
+
CREATE TABLE IF NOT EXISTS activities (
|
|
9
|
+
activity_id INTEGER PRIMARY KEY,
|
|
10
|
+
activity_name TEXT,
|
|
11
|
+
activity_type TEXT,
|
|
12
|
+
start_time TEXT,
|
|
13
|
+
duration_seconds REAL,
|
|
14
|
+
distance_meters REAL,
|
|
15
|
+
average_heart_rate INTEGER,
|
|
16
|
+
max_heart_rate INTEGER,
|
|
17
|
+
calories REAL,
|
|
18
|
+
raw_data TEXT,
|
|
19
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
20
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS biometrics (
|
|
24
|
+
date TEXT PRIMARY KEY,
|
|
25
|
+
resting_heart_rate INTEGER,
|
|
26
|
+
hrv_balance TEXT,
|
|
27
|
+
body_battery_highest INTEGER,
|
|
28
|
+
body_battery_lowest INTEGER,
|
|
29
|
+
stress_average INTEGER,
|
|
30
|
+
raw_data TEXT,
|
|
31
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
32
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS sleep (
|
|
36
|
+
date TEXT PRIMARY KEY,
|
|
37
|
+
sleep_start TEXT,
|
|
38
|
+
sleep_end TEXT,
|
|
39
|
+
total_sleep_seconds INTEGER,
|
|
40
|
+
deep_sleep_seconds INTEGER,
|
|
41
|
+
light_sleep_seconds INTEGER,
|
|
42
|
+
rem_sleep_seconds INTEGER,
|
|
43
|
+
awake_sleep_seconds INTEGER,
|
|
44
|
+
sleep_score INTEGER,
|
|
45
|
+
raw_data TEXT,
|
|
46
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
47
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS sync_log (
|
|
51
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
sync_type TEXT NOT NULL,
|
|
53
|
+
records_synced INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
status TEXT NOT NULL DEFAULT 'success',
|
|
55
|
+
error_message TEXT,
|
|
56
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
60
|
+
version INTEGER PRIMARY KEY
|
|
61
|
+
);
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_version(conn: sqlite3.Connection) -> int | None:
|
|
66
|
+
"""Get current schema version, or None if table doesn't exist."""
|
|
67
|
+
try:
|
|
68
|
+
cursor = conn.execute("SELECT version FROM schema_version")
|
|
69
|
+
row = cursor.fetchone()
|
|
70
|
+
return row["version"] if row else None
|
|
71
|
+
except sqlite3.OperationalError:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def init_db(conn: sqlite3.Connection) -> None:
|
|
76
|
+
"""Initialize database schema. Idempotent — safe to call repeatedly."""
|
|
77
|
+
current = _get_version(conn)
|
|
78
|
+
if current == CURRENT_VERSION:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
conn.executescript(_SCHEMA_V1)
|
|
82
|
+
|
|
83
|
+
if current is None:
|
|
84
|
+
conn.execute(
|
|
85
|
+
"INSERT OR REPLACE INTO schema_version (version) VALUES (?)",
|
|
86
|
+
(CURRENT_VERSION,),
|
|
87
|
+
)
|
|
88
|
+
conn.commit()
|
garsync/exporter.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import TextIO
|
|
3
|
+
|
|
4
|
+
from garsync.models import SyncResult
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class JSONExporter:
|
|
10
|
+
@staticmethod
|
|
11
|
+
def export(result: SyncResult, output: TextIO) -> None:
|
|
12
|
+
"""Export the sync result to the provided file object or stdout."""
|
|
13
|
+
try:
|
|
14
|
+
# Output using pydantic's JSON generation
|
|
15
|
+
output.write(result.model_dump_json(indent=2))
|
|
16
|
+
output.write("\n")
|
|
17
|
+
logger.info("Successfully exported sync result.")
|
|
18
|
+
except Exception as e:
|
|
19
|
+
logger.error(f"Failed to export data: {e}")
|
|
20
|
+
raise
|
garsync/models.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NormalizedActivity(BaseModel):
|
|
8
|
+
activity_id: int
|
|
9
|
+
activity_name: Optional[str] = None
|
|
10
|
+
activity_type: Optional[str] = None
|
|
11
|
+
start_time: Optional[datetime] = None
|
|
12
|
+
duration_seconds: float
|
|
13
|
+
distance_meters: Optional[float] = None
|
|
14
|
+
average_heart_rate: Optional[int] = None
|
|
15
|
+
max_heart_rate: Optional[int] = None
|
|
16
|
+
calories: Optional[float] = None
|
|
17
|
+
raw_data: dict[str, Any] = Field(default_factory=dict, exclude=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DailyBiometrics(BaseModel):
|
|
21
|
+
date: date
|
|
22
|
+
resting_heart_rate: Optional[int] = None
|
|
23
|
+
hrv_balance: Optional[str] = None
|
|
24
|
+
body_battery_highest: Optional[int] = None
|
|
25
|
+
body_battery_lowest: Optional[int] = None
|
|
26
|
+
stress_average: Optional[int] = None
|
|
27
|
+
raw_data: dict[str, Any] = Field(default_factory=dict, exclude=True)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SleepData(BaseModel):
|
|
31
|
+
date: date
|
|
32
|
+
sleep_start: Optional[datetime] = None
|
|
33
|
+
sleep_end: Optional[datetime] = None
|
|
34
|
+
total_sleep_seconds: Optional[int] = None
|
|
35
|
+
deep_sleep_seconds: Optional[int] = None
|
|
36
|
+
light_sleep_seconds: Optional[int] = None
|
|
37
|
+
rem_sleep_seconds: Optional[int] = None
|
|
38
|
+
awake_sleep_seconds: Optional[int] = None
|
|
39
|
+
sleep_score: Optional[int] = None
|
|
40
|
+
raw_data: dict[str, Any] = Field(default_factory=dict, exclude=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SyncResult(BaseModel):
|
|
44
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
|
45
|
+
activities: list[NormalizedActivity] = Field(default_factory=list)
|
|
46
|
+
biometrics: list[DailyBiometrics] = Field(default_factory=list)
|
|
47
|
+
sleep: list[SleepData] = Field(default_factory=list)
|
garsync/pipeline.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Core synchronization pipeline logic."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import sqlite3
|
|
6
|
+
from datetime import date
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from garsync.client import GarminClient
|
|
10
|
+
from garsync.db import (
|
|
11
|
+
ActivityRepository,
|
|
12
|
+
BiometricsRepository,
|
|
13
|
+
SleepRepository,
|
|
14
|
+
SyncLogRepository,
|
|
15
|
+
)
|
|
16
|
+
from garsync.models import DailyBiometrics, NormalizedActivity, SleepData
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def activity_to_row(activity: NormalizedActivity) -> dict[str, Any]:
|
|
22
|
+
"""Convert activity model to DB row dict."""
|
|
23
|
+
return {
|
|
24
|
+
"activity_id": activity.activity_id,
|
|
25
|
+
"activity_name": activity.activity_name,
|
|
26
|
+
"activity_type": activity.activity_type,
|
|
27
|
+
"start_time": activity.start_time.isoformat() if activity.start_time else None,
|
|
28
|
+
"duration_seconds": activity.duration_seconds,
|
|
29
|
+
"distance_meters": activity.distance_meters,
|
|
30
|
+
"average_heart_rate": activity.average_heart_rate,
|
|
31
|
+
"max_heart_rate": activity.max_heart_rate,
|
|
32
|
+
"calories": activity.calories,
|
|
33
|
+
"raw_data": json.dumps(activity.raw_data),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def biometrics_to_row(bio: DailyBiometrics) -> dict[str, Any]:
|
|
38
|
+
"""Convert biometrics model to DB row dict."""
|
|
39
|
+
return {
|
|
40
|
+
"date": bio.date.isoformat(),
|
|
41
|
+
"resting_heart_rate": bio.resting_heart_rate,
|
|
42
|
+
"hrv_balance": bio.hrv_balance,
|
|
43
|
+
"body_battery_highest": bio.body_battery_highest,
|
|
44
|
+
"body_battery_lowest": bio.body_battery_lowest,
|
|
45
|
+
"stress_average": bio.stress_average,
|
|
46
|
+
"raw_data": json.dumps(bio.raw_data),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def sleep_to_row(sleep: SleepData) -> dict[str, Any]:
|
|
51
|
+
"""Convert sleep model to DB row dict."""
|
|
52
|
+
return {
|
|
53
|
+
"date": sleep.date.isoformat(),
|
|
54
|
+
"sleep_start": sleep.sleep_start.isoformat() if sleep.sleep_start else None,
|
|
55
|
+
"sleep_end": sleep.sleep_end.isoformat() if sleep.sleep_end else None,
|
|
56
|
+
"total_sleep_seconds": sleep.total_sleep_seconds,
|
|
57
|
+
"deep_sleep_seconds": sleep.deep_sleep_seconds,
|
|
58
|
+
"light_sleep_seconds": sleep.light_sleep_seconds,
|
|
59
|
+
"rem_sleep_seconds": sleep.rem_sleep_seconds,
|
|
60
|
+
"awake_sleep_seconds": sleep.awake_sleep_seconds,
|
|
61
|
+
"sleep_score": sleep.sleep_score,
|
|
62
|
+
"raw_data": json.dumps(sleep.raw_data),
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SyncService:
|
|
67
|
+
"""Orchestrates the synchronization process between Garmin and SQLite."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, client: GarminClient, db_conn: sqlite3.Connection):
|
|
70
|
+
self.client = client
|
|
71
|
+
self.db_conn = db_conn
|
|
72
|
+
self.activity_repo = ActivityRepository(db_conn)
|
|
73
|
+
self.biometrics_repo = BiometricsRepository(db_conn)
|
|
74
|
+
self.sleep_repo = SleepRepository(db_conn)
|
|
75
|
+
self.sync_log_repo = SyncLogRepository(db_conn)
|
|
76
|
+
|
|
77
|
+
def sync_range(self, dates: list[date], activities_limit: int = 100) -> dict[str, int]:
|
|
78
|
+
"""Run a full sync for the given range of dates."""
|
|
79
|
+
results = {
|
|
80
|
+
"activities": 0,
|
|
81
|
+
"biometrics": 0,
|
|
82
|
+
"sleep": 0,
|
|
83
|
+
"errors": 0,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# 1. Activities (usually fetched in bulk, not per day)
|
|
87
|
+
try:
|
|
88
|
+
activities = self.client.fetch_activities(limit=activities_limit)
|
|
89
|
+
for activity in activities:
|
|
90
|
+
self.activity_repo.upsert(activity_to_row(activity))
|
|
91
|
+
results["activities"] = len(activities)
|
|
92
|
+
self.sync_log_repo.log("activities", len(activities), "success")
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Failed to sync activities: {e}")
|
|
95
|
+
self.sync_log_repo.log("activities", 0, "error", str(e))
|
|
96
|
+
results["errors"] += 1
|
|
97
|
+
|
|
98
|
+
# 2. Daily metrics (Biometrics and Sleep)
|
|
99
|
+
for d in dates:
|
|
100
|
+
date_str = d.isoformat()
|
|
101
|
+
|
|
102
|
+
# Biometrics
|
|
103
|
+
try:
|
|
104
|
+
bio = self.client.fetch_biometrics(d)
|
|
105
|
+
self.biometrics_repo.upsert(biometrics_to_row(bio))
|
|
106
|
+
results["biometrics"] += 1
|
|
107
|
+
self.sync_log_repo.log("biometrics", 1, "success")
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.warning(f"Failed to sync biometrics for {date_str}: {e}")
|
|
110
|
+
self.sync_log_repo.log("biometrics", 0, "error", f"{date_str}: {e}")
|
|
111
|
+
results["errors"] += 1
|
|
112
|
+
|
|
113
|
+
# Sleep
|
|
114
|
+
try:
|
|
115
|
+
sleep = self.client.fetch_sleep(d)
|
|
116
|
+
self.sleep_repo.upsert(sleep_to_row(sleep))
|
|
117
|
+
results["sleep"] += 1
|
|
118
|
+
self.sync_log_repo.log("sleep", 1, "success")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.warning(f"Failed to sync sleep for {date_str}: {e}")
|
|
121
|
+
self.sync_log_repo.log("sleep", 0, "error", f"{date_str}: {e}")
|
|
122
|
+
results["errors"] += 1
|
|
123
|
+
|
|
124
|
+
return results
|
|
125
|
+
|
|
126
|
+
def get_latest_synced_date(self) -> Optional[str]:
|
|
127
|
+
"""Get the most recent date present in the biometrics table."""
|
|
128
|
+
return self.biometrics_repo.get_latest_date()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: garsync
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Manu
|
|
6
|
+
Author-email: mlorentedev@gmail.com
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Dist: fastapi (>=0.115.0,<1.0.0)
|
|
13
|
+
Requires-Dist: garminconnect (>=0.2.38,<0.3.0)
|
|
14
|
+
Requires-Dist: pandas (<3)
|
|
15
|
+
Requires-Dist: plotly (>=6.5.2,<7.0.0)
|
|
16
|
+
Requires-Dist: pydantic (>=2.12.5,<3.0.0)
|
|
17
|
+
Requires-Dist: rich (>=14.3.3,<15.0.0)
|
|
18
|
+
Requires-Dist: streamlit (>=1.54.0,<2.0.0)
|
|
19
|
+
Requires-Dist: tenacity (>=9.1.4,<10.0.0)
|
|
20
|
+
Requires-Dist: typer (>=0.24.1,<0.25.0)
|
|
21
|
+
Requires-Dist: uvicorn[standard] (>=0.34.0,<1.0.0)
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# GarSync
|
|
25
|
+
|
|
26
|
+
[](https://github.com/mlorentedev/garsync/actions)
|
|
27
|
+
[](https://mlorentedev.github.io/garsync/)
|
|
28
|
+
[](https://www.python.org/downloads/release/python-3120/)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
|
|
31
|
+
Personal fitness data pipeline that syncs Garmin Connect data to a local SQLite database and visualizes it through a modern Astro dashboard.
|
|
32
|
+
|
|
33
|
+
> **Explore the full documentation at: [mlorentedev.github.io/garsync](https://mlorentedev.github.io/garsync/)**
|
|
34
|
+
|
|
35
|
+
## The Problem
|
|
36
|
+
|
|
37
|
+
Garmin Connect provides great data, but it's locked in a proprietary cloud.
|
|
38
|
+
- **Data Silos:** Hard to export full history for custom analysis.
|
|
39
|
+
- **Limited Visualization:** You are stuck with the official app's charts.
|
|
40
|
+
- **No Local Ownership:** If you lose access or the service is down, your training history is gone.
|
|
41
|
+
|
|
42
|
+
## Value Proposition
|
|
43
|
+
|
|
44
|
+
| Feature | Garmin Connect App | GarSync |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| **Data Ownership** | Proprietary Cloud | Local SQLite (Full Control) |
|
|
47
|
+
| **Customization** | Fixed Dashboards | Extensible Astro + Chart.js |
|
|
48
|
+
| **Access** | Web/Mobile Only | REST API + SQL + CLI |
|
|
49
|
+
| **Automation** | Manual Export | Scheduled Incremental Sync |
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
### 1. Prerequisites
|
|
54
|
+
- Python 3.12+ and [Poetry](https://python-poetry.org/)
|
|
55
|
+
- Node.js 22+ (for the dashboard)
|
|
56
|
+
- [SOPS](https://github.com/getsops/sops) + [Age](https://github.com/FiloSottile/age) (for secrets)
|
|
57
|
+
|
|
58
|
+
### 2. Setup
|
|
59
|
+
```bash
|
|
60
|
+
make setup
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3. Configure Secrets
|
|
64
|
+
Edit your Garmin credentials using SOPS:
|
|
65
|
+
```bash
|
|
66
|
+
sops secrets.env.enc
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Sync & Launch
|
|
70
|
+
```bash
|
|
71
|
+
make sync DAYS=30
|
|
72
|
+
make dev
|
|
73
|
+
```
|
|
74
|
+
Visit `http://localhost:4321` (Dev UI).
|
|
75
|
+
|
|
76
|
+
## Architecture
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Garmin Connect Cloud → GarSync CLI → SQLite DB → FastAPI → Astro Dashboard
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT — see [LICENSE](LICENSE).
|
|
85
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
garsync/__init__.py,sha256=N4JzTlVpYSC8_19wA5Wvy2EiaFN3RFVazgKXt2kNTQk,54
|
|
2
|
+
garsync/__main__.py,sha256=c7_Wqc0333GCquhDzrJlasL1w276YskcIrpN_FxnL48,66
|
|
3
|
+
garsync/api/__init__.py,sha256=g1bknUtmuhDLueNqTbAVMFRmqnsQ1oKS7d0zTBfuJlA,114
|
|
4
|
+
garsync/api/deps.py,sha256=8TIYd1uZLI1ZmupPzZQ4DgFnM7Vv5oJu_6k2Afx1gwI,1027
|
|
5
|
+
garsync/api/main.py,sha256=vc6bVGb9hDwcl-nV0fO8zSPHMqQ090VuejlN8fCbzyc,2600
|
|
6
|
+
garsync/api/routes/__init__.py,sha256=Y-_8J4QUygHeUp-gG7-xTsU9MQqX7sxdavkgXD2gYtk,41
|
|
7
|
+
garsync/api/routes/activities.py,sha256=dMuyDs9F540o7mmmVUiQXsTe-RshSG-fK8Z6EK0q-Rs,1206
|
|
8
|
+
garsync/api/routes/biometrics.py,sha256=fnVhrdIC4VEP8wnpm9eyTi8y5sTtw_RJtCFYlZXdp0c,845
|
|
9
|
+
garsync/api/routes/sleep.py,sha256=NBGDbM58zFK1ThAtknX4Z5grwfyiUM1cvHvgNttjkJ0,791
|
|
10
|
+
garsync/api/routes/stats.py,sha256=36ztYdPV_wdtxQeIaIjtNHmsN4AH4_EPiS_WzInRJi4,4398
|
|
11
|
+
garsync/api/routes/sync.py,sha256=a2X-LjtgJAxg3K_oeJd4YsCzEhPHeSLahDSu1psZ9I0,1212
|
|
12
|
+
garsync/api/schemas.py,sha256=i8gUUu0tPLuKQtjGbGPK1cx_I9HcEPtW7RX1rlH62VI,2884
|
|
13
|
+
garsync/cli.py,sha256=OYSkns9LaUEsTN6pW83r_BYjdu2AVPhc8Rl6U71nH3E,4021
|
|
14
|
+
garsync/client.py,sha256=0HUOOB_2mUdVD6CJIW3CYLUSym7ABVzz3PxqIm9-bfo,7518
|
|
15
|
+
garsync/db/__init__.py,sha256=El1UnEtMRWJspbTAAJ-ryk1eh2ATgHG_mKex42Hc850,479
|
|
16
|
+
garsync/db/connection.py,sha256=vLvu2WPIRnE9KtcpyE8EjK_rUTVjDAv0y2w7u0dYKIA,549
|
|
17
|
+
garsync/db/repository.py,sha256=4pprxqNUMYYmM1iPKCR31CdtZJe81ZfCwtC8yZBiDQc,15973
|
|
18
|
+
garsync/db/schema.py,sha256=jAFRTbKOhzQsJKRKw55Bqh1a7DhaOmzYzVqpPqi3BTc,2755
|
|
19
|
+
garsync/exporter.py,sha256=hSa7jpmWV5LW3MDy6GP4xGDp6K1wiZpWy1LMVcvQBOU,618
|
|
20
|
+
garsync/models.py,sha256=eAoNDHvrujg5v2Qxo_1CUhcaaVD3O_ihpHltk7b_jzs,1658
|
|
21
|
+
garsync/pipeline.py,sha256=Fl5Q7o6WfKDdlvObXWGGqeQvFkVieJ08I3TFBsjqHOY,4919
|
|
22
|
+
garsync-0.1.2.dist-info/METADATA,sha256=eBhAcJ6Ujed6wjITYrdyHRC1tRKyxEyK4sg9G7QwL5g,2785
|
|
23
|
+
garsync-0.1.2.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
24
|
+
garsync-0.1.2.dist-info/entry_points.txt,sha256=42438UHL-jkL4CYPs09g_kZ90MYjQou1XAQ0yxasjIE,43
|
|
25
|
+
garsync-0.1.2.dist-info/RECORD,,
|