Getting Started¶
This guide walks through the most common workflows: connecting to the API, uploading assets, organising with categories, searching, and handling errors.
Prerequisites¶
Install pycomad:
You need a running Comad DAM server and credentials for the ullav-user-management auth service.
Connecting and authenticating¶
from pycomad import ComadClient
client = ComadClient(
api_url="http://localhost:8080",
auth_url="http://localhost:8081",
)
info = client.login(email="you@example.com", password="secret")
print(info.username, info.roles)
If your DAM server and auth service share a hostname (e.g. behind a reverse proxy), you can omit auth_url:
client = ComadClient("https://dam.example.com")
client.login(email="you@example.com", password="secret")
Uploading an asset¶
assets.upload() creates the asset record and streams the file in a single call:
asset = client.assets.upload(
"photo.jpg",
creator="alice",
is_private=False,
description="Summer 2024 shoot",
keywords="summer, outdoor, portrait",
)
print(f"Uploaded: {asset.name} ({asset.size:,} bytes) id={asset.id}")
For larger pipelines you can separate record creation from file upload:
record = client.assets.create("Report", "application/pdf")
client.assets.upload_file(record.id, "report.pdf")
Downloading and thumbnails¶
# Full file as bytes
raw = client.assets.download(asset.id)
with open("copy.jpg", "wb") as f:
f.write(raw)
# PNG thumbnail (server-generated)
thumb = client.assets.thumbnail(asset.id)
with open("thumb.png", "wb") as f:
f.write(thumb)
Organising with categories¶
Categories form a tree. Create a root category, then add child categories:
root = client.categories.create("Architecture", access_level="Global")
child = client.categories.create(
"Interiors",
access_level="Group",
parent_id=root.id,
)
# Link an asset to a category
client.assets.add_category(asset.id, root.id)
# Retrieve the asset with its categories
detail = client.assets.get(asset.id)
for cat in detail.categories:
print(cat.name)
Searching assets¶
Full-text and field-filtered search:
results = client.search.search(q="portrait", creator="alice")
for r in results:
print(r.asset.name, r.asset.size)
if r.metadata and r.metadata.exif:
print(" Camera:", r.metadata.exif.get("camera_make"))
Geographic proximity search:
nearby = client.search.nearby(lat=53.3498, lon=-6.2603, radius_km=10)
for r in nearby:
print(r.asset.name)
Metadata (EXIF / IPTC / XMP)¶
The server extracts metadata when you upload a file. You can also trigger a refresh:
meta = client.metadata.get(asset.id)
if meta.exif:
print("Camera:", meta.exif.get("camera_make"), meta.exif.get("camera_model"))
print("Taken:", meta.exif.get("datetime"))
if meta.iptc:
print("Creator:", meta.iptc.get("creator"))
print("Caption:", meta.iptc.get("caption"))
# Re-extract from the stored file
refreshed = client.metadata.refresh(asset.id)
Checking usage quotas¶
usage = client.assets.usage()
used_mb = usage.used_bytes // 1024 ** 2
limit_mb = (usage.storage_limit_bytes or 0) // 1024 ** 2
print(f"{used_mb} MB / {limit_mb or '∞'} MB used")
print(f"{usage.asset_count} assets, {usage.category_count} categories")
Custom field schemas¶
Teams can define typed custom fields that appear on every asset:
schema = client.custom_field_schemas.create(
team_id="my-team",
key="project_code",
name="Project Code",
field_type="String",
required=False,
)
# Upload an asset with a custom field value
asset = client.assets.upload(
"brief.pdf",
custom_fields={"project_code": "PRJ-42"},
)
ZIP batch import¶
Upload a ZIP archive and the server mirrors the directory tree as a category hierarchy:
result = client.zip.upload("photos.zip", creator="alice", is_private=False)
print(f"Created {len(result.categories)} categories")
print(f"Uploaded {len(result.assets)} assets")
for err in result.errors:
print("Warning:", err)
Error handling reference¶
| Exception | Raised when |
|---|---|
ComadAuthError |
login() not called; 401 or 403 response |
ComadNotFoundError |
404 — resource does not exist |
ComadValidationError |
400 — invalid input |
ComadServerError |
5xx — server-side fault |
ComadError |
Base class; catches all of the above |