feat: MCP server, Bambu Cloud task sync & display name fix (#7)

* added mcp initial trail files

* timestamp use your local django timezone

* added bambu cloud task sync with correct endpoint other than py cloud api

* back fill and relink print name using cloud if there is

* use correct bump-version
This commit is contained in:
RunLit
2026-03-29 23:15:59 +11:00
committed by GitHub
parent 9a91b14593
commit fa90ef11b6
20 changed files with 1761 additions and 12 deletions

121
bambu_run/bambu_cloud.py Normal file
View File

@@ -0,0 +1,121 @@
"""
Thin wrapper around the Bambu Cloud HTTP API using verified endpoints only.
Uses BambuClient as the transport (auth headers, base URL) but bypasses
the package's named methods, which contain guessed/unverified endpoints.
All functions take a BambuClient instance as first argument.
"""
import logging
from datetime import timezone as dt_timezone
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Verified HTTP wrappers
# ---------------------------------------------------------------------------
def get_tasks(client, limit=20, offset=0):
"""Fetch recent cloud tasks. Returns the raw response dict."""
return client.get('v1/user-service/my/tasks', params={'limit': limit, 'offset': offset})
def get_profile(client):
"""Fetch the authenticated user's profile."""
return client.get('v1/user-service/my/profile')
# ---------------------------------------------------------------------------
# Upsert helpers
# ---------------------------------------------------------------------------
def _parse_cloud_dt(value):
"""Parse an ISO-8601 string like '2026-03-28T12:38:29Z' to aware datetime."""
if not value:
return None
from django.utils.dateparse import parse_datetime
from django.utils import timezone
dt = parse_datetime(value)
if dt and dt.tzinfo is None:
dt = dt.replace(tzinfo=dt_timezone.utc)
return dt
def upsert_cloud_task(task_dict):
"""
Parse one task dict from the cloud API and upsert into BambuCloudTask.
Returns the (BambuCloudTask instance, created bool) tuple.
"""
from .models import BambuCloudTask
task_id = task_dict.get('id')
if not task_id:
raise ValueError("task_dict has no 'id' field")
defaults = {
'design_id': task_dict.get('designId') or None,
'design_title': task_dict.get('designTitle') or '',
'plate_title': task_dict.get('title') or '',
'model_id': task_dict.get('modelId') or '',
'profile_id': task_dict.get('profileId') or None,
'plate_index': task_dict.get('plateIndex'),
'device_serial': task_dict.get('deviceId') or '',
'cover_url': task_dict.get('cover') or '',
'weight_grams': task_dict.get('weight'),
'length_mm': task_dict.get('length'),
'cost_time_seconds': task_dict.get('costTime'),
'cloud_status': task_dict.get('status'),
'bed_type': task_dict.get('bedType') or '',
'use_ams': bool(task_dict.get('useAms', True)),
'print_mode': task_dict.get('mode') or '',
'ams_detail_mapping': task_dict.get('amsDetailMapping') or [],
'cloud_start_time': _parse_cloud_dt(task_dict.get('startTime')),
'cloud_end_time': _parse_cloud_dt(task_dict.get('endTime')),
'raw_data': task_dict,
}
return BambuCloudTask.objects.update_or_create(task_id=task_id, defaults=defaults)
def fetch_and_upsert_task(client, print_job):
"""
Called by bambu_collector at print finalization.
Fetches recent tasks from cloud, finds the one matching print_job.cloud_task_id_raw,
upserts BambuCloudTask, and wires up the FK on print_job.
Non-fatal: all errors are logged as warnings only.
"""
if not print_job.cloud_task_id_raw:
logger.debug(f"Job #{print_job.id} has no cloud_task_id_raw — skipping cloud sync")
return
try:
response = get_tasks(client, limit=20)
hits = response.get('hits', response.get('tasks', []))
except Exception as e:
logger.warning(f"Cloud tasks fetch failed for job #{print_job.id}: {e}")
return
target = next((t for t in hits if t.get('id') == print_job.cloud_task_id_raw), None)
if not target:
logger.warning(
f"Job #{print_job.id}: cloud task {print_job.cloud_task_id_raw} "
f"not found in last {len(hits)} tasks from API"
)
return
try:
cloud_task, created = upsert_cloud_task(target)
print_job.cloud_task = cloud_task
print_job.save(update_fields=['cloud_task'])
action = 'created' if created else 'updated'
logger.info(
f"Job #{print_job.id}: cloud task {print_job.cloud_task_id_raw} {action} "
f"— design_title={cloud_task.design_title!r}"
)
except Exception as e:
logger.warning(f"Cloud task upsert failed for job #{print_job.id}: {e}")