Create Bambuddy assignments from InvenTree spools

This commit is contained in:
2026-04-15 20:38:26 +03:00
parent f861db762b
commit 130308a8d9
6 changed files with 279 additions and 2 deletions

View File

@@ -47,9 +47,14 @@ FILAMENT_PRINTER_LOCATION_MAP=B1:93,B2:94,B3:95,B4:96
# Bambuddy spool field that stores the InvenTree StockItem Batch Code. # Bambuddy spool field that stores the InvenTree StockItem Batch Code.
FILAMENT_BATCH_SOURCE=tag_uid FILAMENT_BATCH_SOURCE=tag_uid
FILAMENT_SYNC_SPOOLS=true FILAMENT_SYNC_SPOOLS=true
FILAMENT_SYNC_ASSIGNMENTS=true
FILAMENT_SYNC_LOCATIONS=true FILAMENT_SYNC_LOCATIONS=true
FILAMENT_SYNC_USAGE=true FILAMENT_SYNC_USAGE=true
FILAMENT_RETURN_UNASSIGNED_TO_STORAGE=false FILAMENT_RETURN_UNASSIGNED_TO_STORAGE=false
# Optional. If empty, printer IDs are auto-detected from Bambuddy printer names.
FILAMENT_PRINTER_ID_MAP=
FILAMENT_ASSIGNMENT_DEFAULT_AMS_ID=0
FILAMENT_ASSIGNMENT_START_TRAY_ID=0
FILAMENT_USAGE_LIMIT=200 FILAMENT_USAGE_LIMIT=200
FILAMENT_USAGE_SUCCESS_STATUSES=success,completed,complete,done FILAMENT_USAGE_SUCCESS_STATUSES=success,completed,complete,done
FILAMENT_DEFAULT_MATERIAL=PLA FILAMENT_DEFAULT_MATERIAL=PLA

View File

@@ -99,9 +99,10 @@ Recommended InvenTree structure for the current setup:
The service deliberately starts with `FILAMENT_DRY_RUN=true`. In dry-run mode it reads both systems and reports what it would create, move, or subtract, but it does not write filament changes. Switch to `FILAMENT_DRY_RUN=false` only after `/filament/status` and `/sync/filament?dry_run=true` show the expected mapping. The service deliberately starts with `FILAMENT_DRY_RUN=true`. In dry-run mode it reads both systems and reports what it would create, move, or subtract, but it does not write filament changes. Switch to `FILAMENT_DRY_RUN=false` only after `/filament/status` and `/sync/filament?dry_run=true` show the expected mapping.
Filament sync has three independent parts: Filament sync has four independent parts:
- spool catalog sync: InvenTree stock items create/update Bambuddy spools; - spool catalog sync: InvenTree stock items create/update Bambuddy spools;
- assignment sync: InvenTree printer locations create Bambuddy spool assignments;
- location sync: Bambuddy assignments move InvenTree stock to printer locations; returning unassigned loaded spools to storage is optional; - location sync: Bambuddy assignments move InvenTree stock to printer locations; returning unassigned loaded spools to storage is optional;
- usage sync: Bambuddy usage history subtracts grams from the matching InvenTree stock item. - usage sync: Bambuddy usage history subtracts grams from the matching InvenTree stock item.
@@ -233,9 +234,13 @@ FILAMENT_LOADED_LOCATION_ID=72
FILAMENT_PRINTER_LOCATION_MAP=B1:93,B2:94,B3:95,B4:96 FILAMENT_PRINTER_LOCATION_MAP=B1:93,B2:94,B3:95,B4:96
FILAMENT_BATCH_SOURCE=tag_uid FILAMENT_BATCH_SOURCE=tag_uid
FILAMENT_SYNC_SPOOLS=true FILAMENT_SYNC_SPOOLS=true
FILAMENT_SYNC_ASSIGNMENTS=true
FILAMENT_SYNC_LOCATIONS=true FILAMENT_SYNC_LOCATIONS=true
FILAMENT_SYNC_USAGE=true FILAMENT_SYNC_USAGE=true
FILAMENT_RETURN_UNASSIGNED_TO_STORAGE=false FILAMENT_RETURN_UNASSIGNED_TO_STORAGE=false
FILAMENT_PRINTER_ID_MAP=
FILAMENT_ASSIGNMENT_DEFAULT_AMS_ID=0
FILAMENT_ASSIGNMENT_START_TRAY_ID=0
FILAMENT_USAGE_LIMIT=200 FILAMENT_USAGE_LIMIT=200
FILAMENT_USAGE_SUCCESS_STATUSES=success,completed,complete,done FILAMENT_USAGE_SUCCESS_STATUSES=success,completed,complete,done
FILAMENT_DEFAULT_MATERIAL=PLA FILAMENT_DEFAULT_MATERIAL=PLA
@@ -306,6 +311,9 @@ Do not commit `.env`. It contains API tokens and is ignored by git.
`FILAMENT_SYNC_SPOOLS` `FILAMENT_SYNC_SPOOLS`
: Creates/updates Bambuddy spool records from InvenTree stock. : Creates/updates Bambuddy spool records from InvenTree stock.
`FILAMENT_SYNC_ASSIGNMENTS`
: Creates Bambuddy assignments from InvenTree stock items currently stored in printer locations.
`FILAMENT_SYNC_LOCATIONS` `FILAMENT_SYNC_LOCATIONS`
: Moves InvenTree stock items between storage and printer locations from Bambuddy assignments. : Moves InvenTree stock items between storage and printer locations from Bambuddy assignments.
@@ -315,6 +323,15 @@ Do not commit `.env`. It contains API tokens and is ignored by git.
`FILAMENT_RETURN_UNASSIGNED_TO_STORAGE` `FILAMENT_RETURN_UNASSIGNED_TO_STORAGE`
: When `true`, known Bambuddy spools that are no longer assigned are moved from printer locations back to storage. Keep this `false` until Bambuddy assignments are reliable. : When `true`, known Bambuddy spools that are no longer assigned are moved from printer locations back to storage. Keep this `false` until Bambuddy assignments are reliable.
`FILAMENT_PRINTER_ID_MAP`
: Optional explicit printer-name to Bambuddy printer-ID map, for example `B1:5,B2:2,B3:3,B4:4`. If empty, printer IDs are auto-detected from Bambuddy printer names.
`FILAMENT_ASSIGNMENT_DEFAULT_AMS_ID`
: AMS ID used when creating Bambuddy assignments from InvenTree printer locations. Default is `0`.
`FILAMENT_ASSIGNMENT_START_TRAY_ID`
: First tray ID used for each printer when assigning multiple InvenTree-loaded spools. Default is `0`.
## InvenTree IDs ## InvenTree IDs
Use numeric IDs for target category and stock location. You can find them from the InvenTree UI URL or API. Use numeric IDs for target category and stock location. You can find them from the InvenTree UI URL or API.
@@ -389,6 +406,7 @@ To also run filament tracking automatically:
FILAMENT_TRACKING_ENABLED=true FILAMENT_TRACKING_ENABLED=true
FILAMENT_DRY_RUN=true FILAMENT_DRY_RUN=true
FILAMENT_SYNC_SPOOLS=true FILAMENT_SYNC_SPOOLS=true
FILAMENT_SYNC_ASSIGNMENTS=true
FILAMENT_SYNC_LOCATIONS=true FILAMENT_SYNC_LOCATIONS=true
FILAMENT_SYNC_USAGE=true FILAMENT_SYNC_USAGE=true
FILAMENT_RETURN_UNASSIGNED_TO_STORAGE=false FILAMENT_RETURN_UNASSIGNED_TO_STORAGE=false
@@ -452,6 +470,7 @@ Run individual filament steps:
```powershell ```powershell
curl.exe -X POST -H "X-Service-Token: change-me" http://localhost:8088/sync/filament/spools curl.exe -X POST -H "X-Service-Token: change-me" http://localhost:8088/sync/filament/spools
curl.exe -X POST -H "X-Service-Token: change-me" http://localhost:8088/sync/filament/assignments
curl.exe -X POST -H "X-Service-Token: change-me" http://localhost:8088/sync/filament/locations curl.exe -X POST -H "X-Service-Token: change-me" http://localhost:8088/sync/filament/locations
curl.exe -X POST -H "X-Service-Token: change-me" http://localhost:8088/sync/filament/usage curl.exe -X POST -H "X-Service-Token: change-me" http://localhost:8088/sync/filament/usage
``` ```
@@ -467,6 +486,7 @@ POST /sync/archive/{archive_id}
POST /sync/backfill POST /sync/backfill
POST /sync/filament POST /sync/filament
POST /sync/filament/spools POST /sync/filament/spools
POST /sync/filament/assignments
POST /sync/filament/locations POST /sync/filament/locations
POST /sync/filament/usage POST /sync/filament/usage
POST /webhooks/bambuddy POST /webhooks/bambuddy

View File

@@ -70,6 +70,10 @@ class BambuddyClient:
data = await self._request("PATCH", f"/inventory/spools/{spool_id}", json=payload) data = await self._request("PATCH", f"/inventory/spools/{spool_id}", json=payload)
return data if isinstance(data, dict) else {} return data if isinstance(data, dict) else {}
async def list_printers(self) -> list[dict[str, Any]]:
data = await self._request("GET", "/printers/")
return data if isinstance(data, list) else []
async def list_assignments(self, *, printer_id: int | None = None) -> list[dict[str, Any]]: async def list_assignments(self, *, printer_id: int | None = None) -> list[dict[str, Any]]:
params: dict[str, Any] = {} params: dict[str, Any] = {}
if printer_id is not None: if printer_id is not None:
@@ -77,6 +81,16 @@ class BambuddyClient:
data = await self._request("GET", "/inventory/assignments", params=params) data = await self._request("GET", "/inventory/assignments", params=params)
return data if isinstance(data, list) else [] return data if isinstance(data, list) else []
async def assign_spool(self, *, spool_id: int, printer_id: int, ams_id: int, tray_id: int) -> dict[str, Any]:
payload = {
"spool_id": spool_id,
"printer_id": printer_id,
"ams_id": ams_id,
"tray_id": tray_id,
}
data = await self._request("POST", "/inventory/assignments", json=payload)
return data if isinstance(data, dict) else {}
async def list_usage(self, *, limit: int, printer_id: int | None = None) -> list[dict[str, Any]]: async def list_usage(self, *, limit: int, printer_id: int | None = None) -> list[dict[str, Any]]:
params: dict[str, Any] = {"limit": limit} params: dict[str, Any] = {"limit": limit}
if printer_id is not None: if printer_id is not None:

View File

@@ -44,9 +44,13 @@ class Settings(BaseSettings):
filament_printer_location_map: str = "" filament_printer_location_map: str = ""
filament_batch_source: str = "tag_uid" filament_batch_source: str = "tag_uid"
filament_sync_spools: bool = True filament_sync_spools: bool = True
filament_sync_assignments: bool = True
filament_sync_locations: bool = True filament_sync_locations: bool = True
filament_sync_usage: bool = True filament_sync_usage: bool = True
filament_return_unassigned_to_storage: bool = False filament_return_unassigned_to_storage: bool = False
filament_printer_id_map: str = ""
filament_assignment_default_ams_id: Annotated[int, Field(ge=0)] = 0
filament_assignment_start_tray_id: Annotated[int, Field(ge=0)] = 0
filament_usage_limit: Annotated[int, Field(ge=1, le=1000)] = 200 filament_usage_limit: Annotated[int, Field(ge=1, le=1000)] = 200
filament_usage_success_statuses: str = "success,completed,complete,done" filament_usage_success_statuses: str = "success,completed,complete,done"
filament_default_material: str = "PLA" filament_default_material: str = "PLA"
@@ -88,6 +92,19 @@ class Settings(BaseSettings):
result[key] = int(value) result[key] = int(value)
return result return result
@property
def filament_printer_ids(self) -> dict[str, int]:
result: dict[str, int] = {}
for item in self.filament_printer_id_map.split(","):
if ":" not in item:
continue
key, value = item.split(":", 1)
key = key.strip().lower()
value = value.strip()
if key and value.isdigit():
result[key] = int(value)
return result
@property @property
def filament_loaded_location_ids(self) -> set[int]: def filament_loaded_location_ids(self) -> set[int]:
locations = set(self.filament_printer_locations.values()) locations = set(self.filament_printer_locations.values())

View File

@@ -27,11 +27,12 @@ class FilamentTrackingService:
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
async def status(self) -> dict[str, Any]: async def status(self) -> dict[str, Any]:
stock_items, spools, assignments, usage = await asyncio.gather( stock_items, spools, assignments, usage, printers = await asyncio.gather(
self._list_filament_stock(), self._list_filament_stock(),
self.bambuddy.list_spools(), self.bambuddy.list_spools(),
self.bambuddy.list_assignments(), self.bambuddy.list_assignments(),
self.bambuddy.list_usage(limit=self.settings.filament_usage_limit), self.bambuddy.list_usage(limit=self.settings.filament_usage_limit),
self.bambuddy.list_printers(),
) )
stock_by_batch = self._index_stock_by_batch(stock_items) stock_by_batch = self._index_stock_by_batch(stock_items)
@@ -65,8 +66,11 @@ class FilamentTrackingService:
"storage_location_id": self.settings.filament_storage_location_id, "storage_location_id": self.settings.filament_storage_location_id,
"loaded_location_id": self.settings.filament_loaded_location_id, "loaded_location_id": self.settings.filament_loaded_location_id,
"printer_locations": self.settings.filament_printer_locations, "printer_locations": self.settings.filament_printer_locations,
"printer_ids": self.settings.filament_printer_ids,
"batch_source": self.settings.filament_batch_source, "batch_source": self.settings.filament_batch_source,
"return_unassigned_to_storage": self.settings.filament_return_unassigned_to_storage, "return_unassigned_to_storage": self.settings.filament_return_unassigned_to_storage,
"assignment_default_ams_id": self.settings.filament_assignment_default_ams_id,
"assignment_start_tray_id": self.settings.filament_assignment_start_tray_id,
}, },
"inventree": { "inventree": {
"stock_items": len(stock_items), "stock_items": len(stock_items),
@@ -79,6 +83,7 @@ class FilamentTrackingService:
"spools_with_batch": len(spools_by_batch), "spools_with_batch": len(spools_by_batch),
"assignments": len(assignments), "assignments": len(assignments),
"usage_records": len(usage), "usage_records": len(usage),
"printers": len(printers),
}, },
"mapping": { "mapping": {
"matched_batches": len(matched_batches), "matched_batches": len(matched_batches),
@@ -91,6 +96,12 @@ class FilamentTrackingService:
"missing_in_bambuddy": missing_in_bambuddy[:50], "missing_in_bambuddy": missing_in_bambuddy[:50],
"missing_in_inventree": missing_in_inventree[:50], "missing_in_inventree": missing_in_inventree[:50],
"assignments": [self._assignment_summary(item, spools) for item in assignments[:50]], "assignments": [self._assignment_summary(item, spools) for item in assignments[:50]],
"inventree_loaded_assignments": self._planned_assignment_summaries(
stock_items=stock_items,
spools_by_batch=spools_by_batch,
assignments=assignments,
printers=printers,
)[:50],
"pending_usage": pending_usage[:50], "pending_usage": pending_usage[:50],
}, },
"database": self.database.counts(), "database": self.database.counts(),
@@ -107,6 +118,8 @@ class FilamentTrackingService:
if self.settings.filament_sync_spools: if self.settings.filament_sync_spools:
result["spools"] = await self.sync_spools_from_inventree(dry_run=dry_run) result["spools"] = await self.sync_spools_from_inventree(dry_run=dry_run)
if self.settings.filament_sync_assignments:
result["assignments"] = await self.sync_assignments_from_inventree(dry_run=dry_run)
if self.settings.filament_sync_usage: if self.settings.filament_sync_usage:
result["usage"] = await self.sync_usage_from_bambuddy(dry_run=dry_run) result["usage"] = await self.sync_usage_from_bambuddy(dry_run=dry_run)
if self.settings.filament_sync_locations: if self.settings.filament_sync_locations:
@@ -168,6 +181,99 @@ class FilamentTrackingService:
return result return result
async def sync_assignments_from_inventree(self, *, dry_run: bool | None = None) -> dict[str, Any]:
if not self.settings.filament_tracking_enabled:
return {"enabled": False, "status": "disabled"}
dry = self._dry_run(dry_run)
async with self._lock:
stock_items, spools, assignments, printers = await asyncio.gather(
self._list_filament_stock(),
self.bambuddy.list_spools(),
self.bambuddy.list_assignments(),
self.bambuddy.list_printers(),
)
spools_by_batch = self._index_spools_by_batch(spools)
assigned_by_spool_id = {
int(assignment["spool_id"]): assignment
for assignment in assignments
if assignment.get("spool_id") is not None
}
assigned_by_slot = {
self._assignment_slot_key(assignment): assignment
for assignment in assignments
if self._assignment_slot_key(assignment) is not None
}
printer_ids = self._printer_ids_by_key(printers)
planned = self._planned_assignments(
stock_items=stock_items,
spools_by_batch=spools_by_batch,
printer_ids=printer_ids,
)
result: dict[str, Any] = {
"dry_run": dry,
"seen": len(planned),
"created": 0,
"unchanged": 0,
"skipped": 0,
"failed": 0,
"would_create": [],
"missing_printers": self._missing_printer_keys(printer_ids),
}
for item in planned:
spool_id = int(item["spool_id"])
slot_key = (int(item["printer_id"]), int(item["ams_id"]), int(item["tray_id"]))
existing_for_spool = assigned_by_spool_id.get(spool_id)
existing_for_slot = assigned_by_slot.get(slot_key)
if existing_for_spool:
if self._assignment_slot_key(existing_for_spool) == slot_key:
result["unchanged"] += 1
else:
result["skipped"] += 1
result.setdefault("skipped_items", []).append(
{**item, "reason": "spool_already_assigned_to_another_slot"}
)
continue
if existing_for_slot:
result["skipped"] += 1
result.setdefault("skipped_items", []).append(
{
**item,
"reason": "target_slot_already_occupied",
"current_spool_id": existing_for_slot.get("spool_id"),
}
)
continue
if dry:
result["would_create"].append(item)
continue
try:
created = await self.bambuddy.assign_spool(
spool_id=spool_id,
printer_id=int(item["printer_id"]),
ams_id=int(item["ams_id"]),
tray_id=int(item["tray_id"]),
)
result["created"] += 1
assigned_by_spool_id[spool_id] = created or {
"spool_id": spool_id,
"printer_id": item["printer_id"],
"ams_id": item["ams_id"],
"tray_id": item["tray_id"],
}
assigned_by_slot[slot_key] = assigned_by_spool_id[spool_id]
except Exception as exc:
logger.exception("Failed to assign Bambuddy spool %s to printer %s", spool_id, item["printer_key"])
result["failed"] += 1
result.setdefault("errors", []).append({**item, "error": str(exc)})
return result
async def sync_locations_from_assignments(self, *, dry_run: bool | None = None) -> dict[str, Any]: async def sync_locations_from_assignments(self, *, dry_run: bool | None = None) -> dict[str, Any]:
if not self.settings.filament_tracking_enabled: if not self.settings.filament_tracking_enabled:
return {"enabled": False, "status": "disabled"} return {"enabled": False, "status": "disabled"}
@@ -486,6 +592,100 @@ class FilamentTrackingService:
by_pk[int(item.get("pk") or item.get("id"))] = item by_pk[int(item.get("pk") or item.get("id"))] = item
return list(by_pk.values()) return list(by_pk.values())
def _planned_assignment_summaries(
self,
*,
stock_items: list[dict[str, Any]],
spools_by_batch: dict[str, dict[str, Any]],
assignments: list[dict[str, Any]],
printers: list[dict[str, Any]],
) -> list[dict[str, Any]]:
printer_ids = self._printer_ids_by_key(printers)
existing_spool_ids = {
int(assignment["spool_id"])
for assignment in assignments
if assignment.get("spool_id") is not None
}
planned = self._planned_assignments(
stock_items=stock_items,
spools_by_batch=spools_by_batch,
printer_ids=printer_ids,
)
for item in planned:
item["already_assigned"] = int(item["spool_id"]) in existing_spool_ids
return planned
def _planned_assignments(
self,
*,
stock_items: list[dict[str, Any]],
spools_by_batch: dict[str, dict[str, Any]],
printer_ids: dict[str, int],
) -> list[dict[str, Any]]:
location_to_printer = {
location_id: printer_key
for printer_key, location_id in self.settings.filament_printer_locations.items()
}
by_printer: dict[str, list[dict[str, Any]]] = {}
for stock_item in stock_items:
location_id = self._stock_location(stock_item)
printer_key = location_to_printer.get(location_id)
if not printer_key:
continue
by_printer.setdefault(printer_key, []).append(stock_item)
planned: list[dict[str, Any]] = []
for printer_key, items in sorted(by_printer.items()):
printer_id = printer_ids.get(printer_key)
if printer_id is None:
continue
sorted_items = sorted(items, key=lambda item: int(item.get("pk") or item.get("id") or 0))
for index, stock_item in enumerate(sorted_items):
batch = self._stock_batch(stock_item)
spool = spools_by_batch.get(batch or "")
if not batch or not spool or spool.get("id") is None:
continue
planned.append(
{
"batch": batch,
"spool_id": int(spool["id"]),
"stock_item_id": int(stock_item.get("pk") or stock_item.get("id")),
"printer_key": printer_key,
"printer_id": printer_id,
"ams_id": self.settings.filament_assignment_default_ams_id,
"tray_id": self.settings.filament_assignment_start_tray_id + index,
"location_id": self._stock_location(stock_item),
"quantity": self._stock_quantity(stock_item),
}
)
return planned
def _printer_ids_by_key(self, printers: list[dict[str, Any]]) -> dict[str, int]:
result = dict(self.settings.filament_printer_ids)
for printer_key in self.settings.filament_printer_locations:
if printer_key in result:
continue
matched = self._find_printer_for_key(printer_key, printers)
if matched and matched.get("id") is not None:
result[printer_key] = int(matched["id"])
return result
def _missing_printer_keys(self, printer_ids: dict[str, int]) -> list[str]:
return sorted(key for key in self.settings.filament_printer_locations if key not in printer_ids)
@staticmethod
def _find_printer_for_key(printer_key: str, printers: list[dict[str, Any]]) -> dict[str, Any] | None:
normalized_key = printer_key.strip().lower()
for printer in printers:
name = str(printer.get("name") or "").strip().lower()
if name == normalized_key or name.startswith(normalized_key) or normalized_key.startswith(name):
return printer
return None
def _target_location_for_assignment(self, assignment: dict[str, Any]) -> int | None: def _target_location_for_assignment(self, assignment: dict[str, Any]) -> int | None:
printer_locations = self.settings.filament_printer_locations printer_locations = self.settings.filament_printer_locations
printer_name = str(assignment.get("printer_name") or "").strip().lower() printer_name = str(assignment.get("printer_name") or "").strip().lower()
@@ -509,6 +709,15 @@ class FilamentTrackingService:
spool = next((item for item in spools if item.get("id") == spool_id), None) spool = next((item for item in spools if item.get("id") == spool_id), None)
return self._batch_from_spool(spool) if spool else None return self._batch_from_spool(spool) if spool else None
@staticmethod
def _assignment_slot_key(assignment: dict[str, Any]) -> tuple[int, int, int] | None:
printer_id = assignment.get("printer_id")
ams_id = assignment.get("ams_id")
tray_id = assignment.get("tray_id")
if printer_id is None or ams_id is None or tray_id is None:
return None
return int(printer_id), int(ams_id), int(tray_id)
def _spool_payload_for_stock(self, stock_item: dict[str, Any]) -> dict[str, Any]: def _spool_payload_for_stock(self, stock_item: dict[str, Any]) -> dict[str, Any]:
part = stock_item.get("part_detail") or {} part = stock_item.get("part_detail") or {}
part_name = str(part.get("full_name") or part.get("name") or f"InvenTree stock {stock_item.get('pk')}") part_name = str(part.get("full_name") or part.get("name") or f"InvenTree stock {stock_item.get('pk')}")

View File

@@ -206,6 +206,18 @@ async def sync_filament_spools(
raise HTTPException(status_code=502, detail=str(exc)) from exc raise HTTPException(status_code=502, detail=str(exc)) from exc
@app.post("/sync/filament/assignments", dependencies=[Depends(require_service_token)])
async def sync_filament_assignments(
request: Request,
dry_run: bool | None = Query(default=None),
) -> dict[str, Any]:
filament_service: FilamentTrackingService = request.app.state.filament_service
try:
return await filament_service.sync_assignments_from_inventree(dry_run=dry_run)
except ExternalApiError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
@app.post("/sync/filament/locations", dependencies=[Depends(require_service_token)]) @app.post("/sync/filament/locations", dependencies=[Depends(require_service_token)])
async def sync_filament_locations( async def sync_filament_locations(
request: Request, request: Request,