mirror of
https://github.com/This-weiyuqing/mi-fitness-health.git
synced 2026-10-18 11:13:48 +08:00
Initialize standalone Mi Fitness MCP repository
This commit is contained in:
commit
6771fc841c
19 files changed
+2805
No files matched your search
@@ -0,0 +1,6 @@
|
||||
"""Service exports."""
|
||||
|
||||
from mi_fitness_mcp.services.query_service import QueryService
|
||||
from mi_fitness_mcp.services.sync_service import SyncService
|
||||
|
||||
__all__ = ["QueryService", "SyncService"]
|
||||
@@ -0,0 +1,223 @@
|
||||
"""Background sync scheduler for Mi Fitness MCP."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable
|
||||
|
||||
from mi_fitness_mcp.adapters.base import DataAdapter
|
||||
from mi_fitness_mcp.storage import Database
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SyncScheduler:
|
||||
"""Scheduler for automatic background synchronization."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
adapter: DataAdapter,
|
||||
db: Database,
|
||||
sync_interval_minutes: int = 60,
|
||||
):
|
||||
"""Initialize sync scheduler.
|
||||
|
||||
Args:
|
||||
adapter: Data source adapter
|
||||
db: Database instance
|
||||
sync_interval_minutes: How often to sync (default: 1 hour)
|
||||
"""
|
||||
self.adapter = adapter
|
||||
self.db = db
|
||||
self.sync_interval = timedelta(minutes=sync_interval_minutes)
|
||||
self._running = False
|
||||
self._task: asyncio.Task | None = None
|
||||
self._last_sync: datetime | None = None
|
||||
self._sync_callback: Callable | None = None
|
||||
|
||||
def set_sync_callback(self, callback: Callable) -> None:
|
||||
"""Set callback to be called after each sync.
|
||||
|
||||
Args:
|
||||
callback: Function to call with sync results
|
||||
"""
|
||||
self._sync_callback = callback
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the sync scheduler."""
|
||||
if self._running:
|
||||
logger.warning("Scheduler already running")
|
||||
return
|
||||
|
||||
self._running = True
|
||||
self._task = asyncio.create_task(self._run_scheduler())
|
||||
logger.info(f"Sync scheduler started (interval: {self.sync_interval})")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the sync scheduler."""
|
||||
if not self._running:
|
||||
return
|
||||
|
||||
self._running = False
|
||||
if self._task:
|
||||
self._task.cancel()
|
||||
try:
|
||||
await self._task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
logger.info("Sync scheduler stopped")
|
||||
|
||||
async def _run_scheduler(self) -> None:
|
||||
"""Main scheduler loop."""
|
||||
from mi_fitness_mcp.services.sync_service import SyncService
|
||||
|
||||
sync_service = SyncService(self.adapter, self.db)
|
||||
|
||||
while self._running:
|
||||
try:
|
||||
# Check if it's time to sync
|
||||
if self._should_sync():
|
||||
await self._perform_sync(sync_service)
|
||||
|
||||
# Wait for next check
|
||||
await asyncio.sleep(60) # Check every minute
|
||||
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in sync scheduler: {e}")
|
||||
await asyncio.sleep(60) # Wait before retrying
|
||||
|
||||
def _should_sync(self) -> bool:
|
||||
"""Check if sync should be performed."""
|
||||
if not self._last_sync:
|
||||
return True
|
||||
|
||||
time_since_last = datetime.utcnow() - self._last_sync
|
||||
return time_since_last >= self.sync_interval
|
||||
|
||||
async def _perform_sync(self, sync_service) -> None:
|
||||
"""Perform synchronization of all data types."""
|
||||
logger.info("Starting automatic sync")
|
||||
|
||||
results = {}
|
||||
data_types = self.adapter.get_available_data_types()
|
||||
|
||||
for data_type in data_types:
|
||||
try:
|
||||
result = sync_service.sync_data_type(data_type)
|
||||
results[data_type] = result
|
||||
logger.info(
|
||||
f"Synced {data_type}: {result['added']} added, "
|
||||
f"{result['updated']} updated"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to sync {data_type}: {e}")
|
||||
results[data_type] = {"error": str(e)}
|
||||
|
||||
self._last_sync = datetime.utcnow()
|
||||
|
||||
# Call callback if set
|
||||
if self._sync_callback:
|
||||
try:
|
||||
self._sync_callback(results)
|
||||
except Exception as e:
|
||||
logger.error(f"Sync callback failed: {e}")
|
||||
|
||||
logger.info("Automatic sync completed")
|
||||
|
||||
def get_status(self) -> dict:
|
||||
"""Get scheduler status."""
|
||||
return {
|
||||
"running": self._running,
|
||||
"last_sync": self._last_sync.isoformat() if self._last_sync else None,
|
||||
"sync_interval_minutes": self.sync_interval.total_seconds() / 60,
|
||||
"next_sync": (
|
||||
(self._last_sync + self.sync_interval).isoformat()
|
||||
if self._last_sync
|
||||
else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class AutoSyncManager:
|
||||
"""Manager for automatic synchronization configuration."""
|
||||
|
||||
def __init__(self, config_path: str = "config/autosync.json"):
|
||||
"""Initialize auto-sync manager.
|
||||
|
||||
Args:
|
||||
config_path: Path to autosync configuration file
|
||||
"""
|
||||
self.config_path = config_path
|
||||
self._scheduler: SyncScheduler | None = None
|
||||
|
||||
def load_config(self) -> dict:
|
||||
"""Load auto-sync configuration."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(self.config_path)
|
||||
if not path.exists():
|
||||
return {
|
||||
"enabled": False,
|
||||
"sync_interval_minutes": 60,
|
||||
"data_types": ["daily_activity", "sleep", "workouts"],
|
||||
}
|
||||
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_config(self, config: dict) -> None:
|
||||
"""Save auto-sync configuration."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(self.config_path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(path, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
async def start_scheduler(
|
||||
self,
|
||||
adapter: DataAdapter,
|
||||
db: Database,
|
||||
) -> SyncScheduler | None:
|
||||
"""Start scheduler with loaded configuration.
|
||||
|
||||
Args:
|
||||
adapter: Data source adapter
|
||||
db: Database instance
|
||||
|
||||
Returns:
|
||||
SyncScheduler instance or None if not enabled
|
||||
"""
|
||||
config = self.load_config()
|
||||
|
||||
if not config.get("enabled", False):
|
||||
logger.info("Auto-sync is disabled")
|
||||
return None
|
||||
|
||||
interval = config.get("sync_interval_minutes", 60)
|
||||
|
||||
self._scheduler = SyncScheduler(
|
||||
adapter=adapter,
|
||||
db=db,
|
||||
sync_interval_minutes=interval,
|
||||
)
|
||||
|
||||
await self._scheduler.start()
|
||||
return self._scheduler
|
||||
|
||||
async def stop_scheduler(self) -> None:
|
||||
"""Stop the running scheduler."""
|
||||
if self._scheduler:
|
||||
await self._scheduler.stop()
|
||||
self._scheduler = None
|
||||
|
||||
def get_scheduler_status(self) -> dict | None:
|
||||
"""Get current scheduler status."""
|
||||
if not self._scheduler:
|
||||
return None
|
||||
return self._scheduler.get_status()
|
||||
@@ -0,0 +1,301 @@
|
||||
"""Query service for retrieving data from database."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from mi_fitness_mcp.storage import Database
|
||||
|
||||
|
||||
class QueryService:
|
||||
"""Service for querying fitness data from local database."""
|
||||
|
||||
def __init__(self, db: Database, user_id: str):
|
||||
"""Initialize query service.
|
||||
|
||||
Args:
|
||||
db: Database instance
|
||||
user_id: User identifier
|
||||
"""
|
||||
self.db = db
|
||||
self.user_id = user_id
|
||||
|
||||
def get_daily_summaries(self, start_date: str, end_date: str) -> list[dict[str, Any]]:
|
||||
"""Get daily activity summaries for date range."""
|
||||
records = self.db.query_daily_activity(self.user_id, start_date, end_date)
|
||||
|
||||
# Group by date and aggregate
|
||||
summaries = {}
|
||||
for record in records:
|
||||
date = record["date"]
|
||||
if date not in summaries:
|
||||
summaries[date] = {
|
||||
"date": date,
|
||||
"steps": 0,
|
||||
"distance_m": 0,
|
||||
"active_kcal": 0,
|
||||
"total_kcal": 0,
|
||||
"floors": 0,
|
||||
"active_minutes": 0,
|
||||
}
|
||||
|
||||
summaries[date]["steps"] += record.get("steps", 0)
|
||||
summaries[date]["distance_m"] += record.get("distance_m", 0)
|
||||
summaries[date]["active_kcal"] += record.get("active_kcal", 0)
|
||||
if record.get("total_kcal"):
|
||||
summaries[date]["total_kcal"] += record["total_kcal"]
|
||||
if record.get("floors"):
|
||||
summaries[date]["floors"] += record["floors"]
|
||||
if record.get("active_minutes"):
|
||||
summaries[date]["active_minutes"] += record["active_minutes"]
|
||||
|
||||
return list(summaries.values())
|
||||
|
||||
def get_metric_series(
|
||||
self,
|
||||
metric: str,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
granularity: str = "day",
|
||||
aggregation: str = "sum",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get time series for a metric."""
|
||||
summaries = self.get_daily_summaries(start_date, end_date)
|
||||
|
||||
series = []
|
||||
for summary in summaries:
|
||||
value = summary.get(metric)
|
||||
if value is not None:
|
||||
series.append({
|
||||
"date": summary["date"],
|
||||
"value": value,
|
||||
})
|
||||
|
||||
# Apply aggregation if needed
|
||||
if granularity == "week":
|
||||
series = self._aggregate_by_week(series, aggregation)
|
||||
elif granularity == "month":
|
||||
series = self._aggregate_by_month(series, aggregation)
|
||||
|
||||
return series
|
||||
|
||||
def _aggregate_by_week(
|
||||
self,
|
||||
series: list[dict],
|
||||
aggregation: str,
|
||||
) -> list[dict]:
|
||||
"""Aggregate daily series by week."""
|
||||
weeks = {}
|
||||
|
||||
for item in series:
|
||||
date = datetime.strptime(item["date"], "%Y-%m-%d")
|
||||
# Get week start (Monday)
|
||||
week_start = date - timedelta(days=date.weekday())
|
||||
week_key = week_start.strftime("%Y-%m-%d")
|
||||
|
||||
if week_key not in weeks:
|
||||
weeks[week_key] = []
|
||||
weeks[week_key].append(item["value"])
|
||||
|
||||
result = []
|
||||
for week_key, values in sorted(weeks.items()):
|
||||
if aggregation == "sum":
|
||||
value = sum(values)
|
||||
elif aggregation == "avg":
|
||||
value = sum(values) / len(values)
|
||||
elif aggregation == "min":
|
||||
value = min(values)
|
||||
elif aggregation == "max":
|
||||
value = max(values)
|
||||
else:
|
||||
value = sum(values)
|
||||
|
||||
result.append({"date": week_key, "value": value})
|
||||
|
||||
return result
|
||||
|
||||
def _aggregate_by_month(
|
||||
self,
|
||||
series: list[dict],
|
||||
aggregation: str,
|
||||
) -> list[dict]:
|
||||
"""Aggregate daily series by month."""
|
||||
months = {}
|
||||
|
||||
for item in series:
|
||||
date = datetime.strptime(item["date"], "%Y-%m-%d")
|
||||
month_key = date.strftime("%Y-%m")
|
||||
|
||||
if month_key not in months:
|
||||
months[month_key] = []
|
||||
months[month_key].append(item["value"])
|
||||
|
||||
result = []
|
||||
for month_key, values in sorted(months.items()):
|
||||
if aggregation == "sum":
|
||||
value = sum(values)
|
||||
elif aggregation == "avg":
|
||||
value = sum(values) / len(values)
|
||||
elif aggregation == "min":
|
||||
value = min(values)
|
||||
elif aggregation == "max":
|
||||
value = max(values)
|
||||
else:
|
||||
value = sum(values)
|
||||
|
||||
result.append({"date": month_key + "-01", "value": value})
|
||||
|
||||
return result
|
||||
|
||||
def get_sleep_sessions(
|
||||
self,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
include_naps: bool = True,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get sleep sessions for date range."""
|
||||
records = self.db.query_sleep_sessions(self.user_id, start_date, end_date)
|
||||
|
||||
sessions = []
|
||||
for record in records:
|
||||
# Skip naps if not included
|
||||
if not include_naps and record.get("is_nap"):
|
||||
continue
|
||||
|
||||
session = {
|
||||
"sleep_id": record["sleep_id"],
|
||||
"start_at": record["start_at"],
|
||||
"end_at": record["end_at"],
|
||||
"duration_minutes": record["duration_minutes"],
|
||||
"time_asleep_minutes": record["time_asleep_minutes"],
|
||||
"time_awake_minutes": record["time_awake_minutes"],
|
||||
"sleep_score": record.get("sleep_score"),
|
||||
"is_nap": record.get("is_nap", False),
|
||||
}
|
||||
|
||||
# Parse stages if available
|
||||
if record.get("stages"):
|
||||
import json
|
||||
try:
|
||||
session["stages"] = json.loads(record["stages"])
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
sessions.append(session)
|
||||
|
||||
return sessions
|
||||
|
||||
def get_workouts(
|
||||
self,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
activity_types: list[str] | None = None,
|
||||
min_duration: int | None = None,
|
||||
min_distance_km: float | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get workouts for date range with optional filters."""
|
||||
records = self.db.query_workouts(self.user_id, start_date, end_date)
|
||||
|
||||
workouts = []
|
||||
for record in records:
|
||||
# Apply filters
|
||||
if activity_types:
|
||||
if record["activity_type"].lower() not in [t.lower() for t in activity_types]:
|
||||
continue
|
||||
|
||||
if min_duration and record.get("duration_minutes", 0) < min_duration:
|
||||
continue
|
||||
|
||||
if min_distance_km:
|
||||
distance_km = (record.get("distance_m") or 0) / 1000
|
||||
if distance_km < min_distance_km:
|
||||
continue
|
||||
|
||||
workouts.append({
|
||||
"workout_id": record["workout_id"],
|
||||
"activity_type": record["activity_type"],
|
||||
"start_at": record["start_at"],
|
||||
"end_at": record["end_at"],
|
||||
"duration_minutes": record["duration_minutes"],
|
||||
"distance_m": record.get("distance_m"),
|
||||
"calories_kcal": record.get("calories_kcal"),
|
||||
"avg_heart_rate_bpm": record.get("avg_heart_rate_bpm"),
|
||||
"max_heart_rate_bpm": record.get("max_heart_rate_bpm"),
|
||||
"avg_pace_sec_per_km": record.get("avg_pace_sec_per_km"),
|
||||
"max_pace_sec_per_km": record.get("max_pace_sec_per_km"),
|
||||
"total_steps": record.get("total_steps"),
|
||||
})
|
||||
|
||||
return workouts
|
||||
|
||||
def get_body_measurements(
|
||||
self,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
metrics: list[str] | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get body measurements for date range."""
|
||||
records = self.db.query_body_measurements(self.user_id, start_date, end_date)
|
||||
|
||||
measurements = []
|
||||
for record in records:
|
||||
measurement = {
|
||||
"timestamp": record["timestamp"],
|
||||
"weight_kg": record["weight_kg"],
|
||||
}
|
||||
|
||||
# Add optional metrics
|
||||
optional_fields = [
|
||||
"bmi", "body_fat_pct", "muscle_mass_kg", "water_pct",
|
||||
"bone_mass_kg", "visceral_fat_score", "basal_metabolism_kcal",
|
||||
"metabolic_age",
|
||||
]
|
||||
|
||||
for field in optional_fields:
|
||||
if record.get(field) is not None:
|
||||
measurement[field] = record[field]
|
||||
|
||||
# Filter metrics if specified
|
||||
if metrics:
|
||||
filtered = {"timestamp": measurement["timestamp"]}
|
||||
for metric in metrics:
|
||||
if metric in measurement:
|
||||
filtered[metric] = measurement[metric]
|
||||
measurement = filtered
|
||||
|
||||
measurements.append(measurement)
|
||||
|
||||
return measurements
|
||||
|
||||
def get_heart_rate_samples(
|
||||
self,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
sample_type: str | None = None,
|
||||
limit: int | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
records = self.db.query_heart_rate_samples(self.user_id, start_date, end_date)
|
||||
|
||||
samples = []
|
||||
for record in records:
|
||||
if sample_type and record.get("sample_type") != sample_type:
|
||||
continue
|
||||
samples.append(
|
||||
{
|
||||
"timestamp": record["timestamp"],
|
||||
"bpm": record["bpm"],
|
||||
"sample_type": record.get("sample_type", "passive"),
|
||||
}
|
||||
)
|
||||
|
||||
if limit is not None:
|
||||
return samples[:limit]
|
||||
return samples
|
||||
|
||||
def get_data_coverage(self, data_types: list[str] | None = None) -> list[dict[str, Any]]:
|
||||
"""Get data coverage information."""
|
||||
coverage = self.db.get_data_coverage(self.user_id)
|
||||
|
||||
if data_types:
|
||||
coverage = [c for c in coverage if c["data_type"] in data_types]
|
||||
|
||||
return coverage
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Sync service for importing data from adapters to database."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from mi_fitness_mcp.adapters.base import DataAdapter
|
||||
from mi_fitness_mcp.storage import Database
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SyncService:
|
||||
"""Service for synchronizing data from adapters to local database."""
|
||||
|
||||
def __init__(self, adapter: DataAdapter, db: Database):
|
||||
"""Initialize sync service.
|
||||
|
||||
Args:
|
||||
adapter: Data source adapter
|
||||
db: Database instance
|
||||
"""
|
||||
self.adapter = adapter
|
||||
self.db = db
|
||||
|
||||
async def _iterate_records(self, records: Any) -> AsyncIterator[Any]:
|
||||
if hasattr(records, "__aiter__"):
|
||||
async for record in records:
|
||||
yield record
|
||||
return
|
||||
|
||||
for record in records:
|
||||
yield record
|
||||
|
||||
async def sync_data_type(
|
||||
self,
|
||||
data_type: str,
|
||||
start_date: str | None = None,
|
||||
end_date: str | None = None,
|
||||
force_full: bool = False,
|
||||
) -> dict:
|
||||
"""Synchronize a specific data type.
|
||||
|
||||
Args:
|
||||
data_type: Type of data to sync (daily_activity, sleep, workouts, body_measurements)
|
||||
start_date: Start date (YYYY-MM-DD), defaults to 30 days ago
|
||||
end_date: End date (YYYY-MM-DD), defaults to today
|
||||
force_full: Force full sync ignoring last sync state
|
||||
|
||||
Returns:
|
||||
Dict with sync statistics
|
||||
"""
|
||||
if not self.adapter.is_connected():
|
||||
raise RuntimeError("Adapter not connected")
|
||||
|
||||
# Get last sync state for incremental sync
|
||||
last_record_ts = None
|
||||
if not force_full:
|
||||
state = self.db.get_sync_state(data_type)
|
||||
if state and state.get("last_record_timestamp"):
|
||||
last_record_ts = datetime.fromisoformat(state["last_record_timestamp"])
|
||||
|
||||
if not end_date:
|
||||
end_date = datetime.now().strftime("%Y-%m-%d")
|
||||
if not start_date:
|
||||
if last_record_ts:
|
||||
start_date = last_record_ts.strftime("%Y-%m-%d")
|
||||
elif self.adapter.__class__.__name__ == "MiFitnessCloudAdapter":
|
||||
start_date = "2020-01-01"
|
||||
else:
|
||||
start = datetime.strptime(end_date, "%Y-%m-%d") - timedelta(days=30)
|
||||
start_date = start.strftime("%Y-%m-%d")
|
||||
|
||||
added = 0
|
||||
updated = 0
|
||||
skipped = 0
|
||||
last_ts = None
|
||||
|
||||
# Sync based on data type
|
||||
if data_type == "daily_activity":
|
||||
records = self.adapter.iter_daily_activity(start_date, end_date)
|
||||
async for activity in self._iterate_records(records):
|
||||
if self.db.insert_daily_activity(activity):
|
||||
added += 1
|
||||
else:
|
||||
updated += 1
|
||||
if last_ts is None or (activity.collected_at and activity.collected_at > last_ts):
|
||||
last_ts = activity.collected_at
|
||||
|
||||
elif data_type == "sleep":
|
||||
records = self.adapter.iter_sleep_sessions(start_date, end_date)
|
||||
async for sleep in self._iterate_records(records):
|
||||
if self.db.insert_sleep_session(sleep):
|
||||
added += 1
|
||||
else:
|
||||
updated += 1
|
||||
if last_ts is None or sleep.start_at > last_ts:
|
||||
last_ts = sleep.start_at
|
||||
|
||||
elif data_type == "workouts":
|
||||
records = self.adapter.iter_workouts(start_date, end_date)
|
||||
async for workout in self._iterate_records(records):
|
||||
if self.db.insert_workout(workout):
|
||||
added += 1
|
||||
else:
|
||||
updated += 1
|
||||
if last_ts is None or workout.start_at > last_ts:
|
||||
last_ts = workout.start_at
|
||||
|
||||
elif data_type == "body_measurements":
|
||||
records = self.adapter.iter_body_measurements(start_date, end_date)
|
||||
async for measurement in self._iterate_records(records):
|
||||
if self.db.insert_body_measurement(measurement):
|
||||
added += 1
|
||||
else:
|
||||
updated += 1
|
||||
if last_ts is None or measurement.timestamp > last_ts:
|
||||
last_ts = measurement.timestamp
|
||||
|
||||
elif data_type == "heart_rate":
|
||||
records = self.adapter.iter_heart_rate(start_date, end_date)
|
||||
async for sample in self._iterate_records(records):
|
||||
if self.db.insert_heart_rate_sample(sample):
|
||||
added += 1
|
||||
else:
|
||||
updated += 1
|
||||
if last_ts is None or sample.timestamp > last_ts:
|
||||
last_ts = sample.timestamp
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown data type: {data_type}")
|
||||
|
||||
# Update sync state
|
||||
if last_ts:
|
||||
self.db.update_sync_state(data_type, last_ts)
|
||||
|
||||
logger.info(
|
||||
f"Synced {data_type}: {added} added, {updated} updated, "
|
||||
f"range {start_date} to {end_date}"
|
||||
)
|
||||
|
||||
return {
|
||||
"data_type": data_type,
|
||||
"added": added,
|
||||
"updated": updated,
|
||||
"skipped": skipped,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
}
|
||||
|
||||
def sync_data_type_sync(
|
||||
self,
|
||||
data_type: str,
|
||||
start_date: str | None = None,
|
||||
end_date: str | None = None,
|
||||
force_full: bool = False,
|
||||
) -> dict:
|
||||
"""Synchronous wrapper for sync_data_type.
|
||||
|
||||
Use this when calling from synchronous code.
|
||||
"""
|
||||
return asyncio.run(self.sync_data_type(data_type, start_date, end_date, force_full))
|
||||
Reference in new issue
Block a user