initial commit after automated creating

This commit is contained in:
claudi 2026-03-03 12:55:22 +01:00
commit 1cf124a5a3
48 changed files with 12041 additions and 0 deletions

14
.env.example Normal file
View file

@ -0,0 +1,14 @@
# Copy this file to .env and fill in your values.
# All variables are prefixed with AGRAVITY_.
# Required: your x-functions-key API key
AGRAVITY_API_KEY=your-api-key-here
# Optional: override the base API URL
# AGRAVITY_BASE_URL=https://devagravitypublic.azurewebsites.net/api
# Optional: HTTP request timeout in seconds
# AGRAVITY_TIMEOUT=60.0
# Optional: set to false to disable SSL certificate verification (not recommended)
# AGRAVITY_VERIFY_SSL=true

37
.gitignore vendored Normal file
View file

@ -0,0 +1,37 @@
# Python
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.egg
*.egg-info/
dist/
build/
.eggs/
# Virtual environments
.venv/
venv/
env/
# Type checking
.mypy_cache/
.dmypy.json
dmypy.json
# Linting
.ruff_cache/
# Environment files
.env
# pytest
.pytest_cache/
.coverage
htmlcov/
# VS Code
.vscode/
# macOS
.DS_Store

141
README.md Normal file
View file

@ -0,0 +1,141 @@
# agravity-client
A fully **Pythonic, Pydantic v2driven, async** REST client for the
[Agravity DAM API](https://agravity.io) (v10.3.0).
Built on [`httpx`](https://www.python-httpx.org/) and
[Pydantic](https://docs.pydantic.dev/), this library gives you:
- **Typed return values** every endpoint returns a validated Pydantic model.
- **Async-first** all network calls are `async def` using `httpx.AsyncClient`.
- **Context-manager support** clean connection lifecycle.
- **Auto-configuration** reads `AGRAVITY_*` env vars or a `.env` file via
`pydantic-settings`.
## Installation
```bash
pip install agravity-client
```
_Or in development mode:_
```bash
pip install -e ".[dev]"
```
## Quick start
```python
import asyncio
from agravity_client import AgravityClient, AgravityConfig
async def main():
config = AgravityConfig(api_key="YOUR_API_KEY") # or set AGRAVITY_API_KEY env var
async with AgravityClient(config) as client:
# ------------------------------------------------------------------
# Version & capabilities
# ------------------------------------------------------------------
version = await client.general.get_version()
print(version.version)
# ------------------------------------------------------------------
# List assets in a collection
# ------------------------------------------------------------------
page = await client.assets.list_assets(
collection_id="your-collection-id",
limit=25,
)
for asset in page.assets or []:
print(asset.id, asset.name)
# ------------------------------------------------------------------
# Upload an asset
# ------------------------------------------------------------------
with open("photo.jpg", "rb") as fh:
new_asset = await client.assets.upload_asset(
fh.read(),
"photo.jpg",
name="My photo",
collection_id="your-collection-id",
)
print("Created:", new_asset.id)
# ------------------------------------------------------------------
# Search
# ------------------------------------------------------------------
from agravity_client.models import AzSearchOptions
results = await client.search.search(
AzSearchOptions(searchterm="sunset", limit=10)
)
print(results.count, "results")
asyncio.run(main())
```
## Configuration
| Setting | Env variable | Default |
|---------|-------------|---------|
| `base_url` | `AGRAVITY_BASE_URL` | `https://devagravitypublic.azurewebsites.net/api` |
| `api_key` | `AGRAVITY_API_KEY` | _(empty)_ |
| `timeout` | `AGRAVITY_TIMEOUT` | `60.0` |
| `verify_ssl` | `AGRAVITY_VERIFY_SSL` | `true` |
Copy `.env.example` to `.env` and fill in your values.
## API modules
| Attribute | Endpoints covered |
|-----------|-------------------|
| `client.assets` | `/assets`, `/assetsupload`, `/assetsbulkupdate`, all sub-resources |
| `client.collections` | `/collections` CRUD, ancestors, descendants, preview, bynames |
| `client.collection_types` | `/collectiontypes` |
| `client.relations` | `/relations`, `/assetrelationtypes` |
| `client.search` | `/search`, `/search/facette`, `/search/adminstatus`, `/savedsearch` |
| `client.sharing` | `/sharing`, `/sharing/quickshares` |
| `client.portals` | `/portals` |
| `client.workspaces` | `/workspaces` |
| `client.auth` | `/auth/containerwrite`, `/auth/inbox`, `/auth/users` |
| `client.download_formats` | `/downloadformats` |
| `client.static_lists` | `/staticdefinedlists` |
| `client.translations` | `/translations` |
| `client.publishing` | `/publish` |
| `client.secure_upload` | `/secureupload` |
| `client.helper` | `/helper/*` |
| `client.webappdata` | `/webappdata` |
| `client.general` | `/version`, `/deleted`, `/durable`, `/negotiate`, `/public`, `/config` |
| `client.ai` | `/ai/reverseassetsearch` |
## Development
```bash
# Create and activate a virtual environment
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Unix
# Install in editable mode with all dev dependencies
pip install -e ".[dev]"
# Lint & format
ruff check .
ruff format .
# Type-check
mypy agravity_client
# Run tests
pytest
```
## Licence
MIT

View file

@ -0,0 +1,120 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
// Python environment
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/Scripts/python.exe",
"python.terminal.activateEnvironment": true,
// Editor
"editor.formatOnSave": true,
"editor.rulers": [100],
"editor.tabSize": 4,
// Ruff (linter + formatter)
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.ruff": "explicit",
"source.organizeImports.ruff": "explicit"
}
},
// Pylance / Pyright
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true,
"python.analysis.indexing": true,
// Test discovery
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"python.testing.pytestArgs": ["tests"],
// File associations
"files.associations": {
"*.env.example": "dotenv"
},
// Exclude noise from explorer
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true,
"**/.mypy_cache": true,
"**/.ruff_cache": true,
"**/*.egg-info": true,
".venv": false
}
},
"extensions": {
"recommendations": [
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff",
"ms-python.mypy-type-checker",
"tamasfe.even-better-toml",
"mikestead.dotenv"
]
},
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"justMyCode": true
}
]
},
"tasks": {
"version": "2.0.0",
"tasks": [
{
"label": "Install (editable)",
"type": "shell",
"command": "pip install -e \".[dev]\"",
"group": "build",
"presentation": { "reveal": "always" },
"problemMatcher": []
},
{
"label": "Ruff: lint",
"type": "shell",
"command": "ruff check .",
"group": "test",
"presentation": { "reveal": "always" },
"problemMatcher": []
},
{
"label": "Ruff: format",
"type": "shell",
"command": "ruff format .",
"group": "build",
"presentation": { "reveal": "always" },
"problemMatcher": []
},
{
"label": "mypy: type-check",
"type": "shell",
"command": "mypy agravity_client",
"group": "test",
"presentation": { "reveal": "always" },
"problemMatcher": ["$mypy"]
},
{
"label": "pytest",
"type": "shell",
"command": "pytest -v",
"group": { "kind": "test", "isDefault": true },
"presentation": { "reveal": "always" },
"problemMatcher": []
}
]
}
}

View file

@ -0,0 +1,29 @@
"""Agravity DAM Python client public API surface."""
from __future__ import annotations
from agravity_client.client import AgravityClient
from agravity_client.config import AgravityConfig
from agravity_client.exceptions import (
AgravityAPIError,
AgravityAuthenticationError,
AgravityConnectionError,
AgravityError,
AgravityNotFoundError,
AgravityTimeoutError,
AgravityValidationError,
)
from agravity_client.models import * # noqa: F403 re-export all model symbols
__version__ = "0.1.0"
__all__ = [
"AgravityClient",
"AgravityConfig",
# Exceptions
"AgravityError",
"AgravityAPIError",
"AgravityAuthenticationError",
"AgravityConnectionError",
"AgravityNotFoundError",
"AgravityTimeoutError",
"AgravityValidationError",
]

View file

@ -0,0 +1,42 @@
"""Agravity API client sub-modules."""
from __future__ import annotations
from agravity_client.api.ai import AiApi
from agravity_client.api.assets import AssetsApi
from agravity_client.api.auth import AuthApi
from agravity_client.api.collection_types import CollectionTypesApi
from agravity_client.api.collections import CollectionsApi
from agravity_client.api.download_formats import DownloadFormatsApi
from agravity_client.api.general import GeneralApi
from agravity_client.api.helper import HelperApi
from agravity_client.api.portals import PortalsApi
from agravity_client.api.publishing import PublishingApi
from agravity_client.api.relations import RelationsApi
from agravity_client.api.search import SearchApi
from agravity_client.api.secure_upload import SecureUploadApi
from agravity_client.api.sharing import SharingApi
from agravity_client.api.static_lists import StaticListsApi
from agravity_client.api.translations import TranslationsApi
from agravity_client.api.webappdata import WebAppDataApi
from agravity_client.api.workspaces import WorkspacesApi
__all__ = [
"AiApi",
"AssetsApi",
"AuthApi",
"CollectionTypesApi",
"CollectionsApi",
"DownloadFormatsApi",
"GeneralApi",
"HelperApi",
"PortalsApi",
"PublishingApi",
"RelationsApi",
"SearchApi",
"SecureUploadApi",
"SharingApi",
"StaticListsApi",
"TranslationsApi",
"WebAppDataApi",
"WorkspacesApi",
]

37
agravity_client/api/ai.py Normal file
View file

@ -0,0 +1,37 @@
"""AI Operations API module (reverse asset search)."""
from __future__ import annotations
from typing import Optional
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.asset import Asset
class AiApi(AgravityBaseApi):
"""Covers all /ai endpoints."""
async def reverse_asset_search(
self,
image_file: bytes,
filename: str,
*,
limit: Optional[int] = None,
content_type: str = "application/octet-stream",
) -> list[Asset]:
"""POST /ai/reverseassetsearch find similar assets by image.
Args:
image_file: Raw image bytes to use as the search query.
filename: Original filename for the image.
limit: Maximum number of results to return.
content_type: MIME type of the uploaded image.
Returns:
A list of matching :class:`~agravity_client.models.asset.Asset` objects.
"""
files = {"image_file": (filename, image_file, content_type)}
data = {}
if limit is not None:
data["limit"] = str(limit)
resp = await self._post("/ai/reverseassetsearch", data=data, files=files)
return [Asset.model_validate(item) for item in resp.json()]

View file

@ -0,0 +1,420 @@
"""Asset Management and Operations API module."""
from __future__ import annotations
from pathlib import Path
from typing import Any, Optional
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.asset import (
Asset,
AssetAvailability,
AssetBlob,
AssetBulkUpdate,
AssetIdFormat,
AssetPageResult,
)
from agravity_client.models.collection import Collection
from agravity_client.models.common import AgravityInfoResponse
from agravity_client.models.download import DynamicImageOperation
from agravity_client.models.publish import PublishEntity, PublishedAsset
from agravity_client.models.relation import AssetRelation
from agravity_client.models.versioning import VersionedAsset, VersionEntity
class AssetsApi(AgravityBaseApi):
"""Covers all /assets endpoints."""
# ------------------------------------------------------------------
# Asset CRUD
# ------------------------------------------------------------------
async def list_assets(
self,
*,
collection_id: Optional[str] = None,
collection_type_id: Optional[str] = None,
fields: Optional[str] = None,
expose: Optional[bool] = None,
continuation_token: Optional[str] = None,
limit: Optional[int] = None,
orderby: Optional[str] = None,
filter: Optional[str] = None,
items: Optional[bool] = None,
translations: Optional[bool] = None,
accept_language: Optional[str] = None,
) -> AssetPageResult:
"""GET /assets list assets (optionally filtered/paginated)."""
params: dict[str, Any] = {
"collectionid": collection_id,
"collectiontypeid": collection_type_id,
"fields": fields,
"expose": expose,
"continuation_token": continuation_token,
"limit": limit,
"orderby": orderby,
"filter": filter,
"items": items,
"translations": translations,
}
headers = {"Accept-Language": accept_language} if accept_language else None
resp = await self._get("/assets", params=params, extra_headers=headers)
return AssetPageResult.model_validate(resp.json())
async def create_asset(self, payload: dict[str, Any]) -> Asset:
"""POST /assets create a new asset."""
resp = await self._post("/assets", json=payload)
return Asset.model_validate(resp.json())
async def get_asset(
self,
asset_id: str,
*,
fields: Optional[str] = None,
items: Optional[bool] = None,
translations: Optional[bool] = None,
accept_language: Optional[str] = None,
) -> Asset:
"""GET /assets/{id}."""
params: dict[str, Any] = {
"fields": fields,
"items": items,
"translations": translations,
}
headers = {"Accept-Language": accept_language} if accept_language else None
resp = await self._get(f"/assets/{asset_id}", params=params, extra_headers=headers)
return Asset.model_validate(resp.json())
async def update_asset(self, asset_id: str, payload: dict[str, Any]) -> Asset:
"""POST /assets/{id} update asset metadata."""
resp = await self._post(f"/assets/{asset_id}", json=payload)
return Asset.model_validate(resp.json())
async def delete_asset(self, asset_id: str) -> None:
"""DELETE /assets/{id}."""
await self._delete(f"/assets/{asset_id}")
# ------------------------------------------------------------------
# Upload
# ------------------------------------------------------------------
async def upload_asset(
self,
file: bytes,
filename: str,
*,
name: Optional[str] = None,
collection_id: Optional[str] = None,
preview_of: Optional[str] = None,
content_type: str = "application/octet-stream",
) -> Asset:
"""POST /assetsupload upload a new asset file (multipart)."""
files: dict[str, Any] = {
"file": (filename, file, content_type),
}
data: dict[str, Any] = {}
if name:
data["name"] = name
if collection_id:
data["collectionid"] = collection_id
if filename:
data["filename"] = filename
if preview_of:
data["previewof"] = preview_of
resp = await self._post("/assetsupload", data=data, files=files)
return Asset.model_validate(resp.json())
async def upload_asset_from_path(
self,
path: Path | str,
*,
name: Optional[str] = None,
collection_id: Optional[str] = None,
preview_of: Optional[str] = None,
) -> Asset:
"""Convenience wrapper: read a local file and call :meth:`upload_asset`."""
path = Path(path)
return await self.upload_asset(
path.read_bytes(),
path.name,
name=name,
collection_id=collection_id,
preview_of=preview_of,
)
# ------------------------------------------------------------------
# Bulk update
# ------------------------------------------------------------------
async def bulk_update_assets(self, payload: AssetBulkUpdate) -> AgravityInfoResponse:
"""POST /assetsbulkupdate."""
resp = await self._post("/assetsbulkupdate", json=payload.model_dump(by_alias=True, exclude_none=True))
return AgravityInfoResponse.model_validate(resp.json())
async def bulk_upsert_assets(self, payload: AssetBulkUpdate) -> AgravityInfoResponse:
"""PUT /assetsbulkupdate."""
resp = await self._put("/assetsbulkupdate", json=payload.model_dump(by_alias=True, exclude_none=True))
return AgravityInfoResponse.model_validate(resp.json())
async def bulk_delete_assets(self, asset_ids: list[str]) -> AgravityInfoResponse:
"""DELETE /assetsbulkupdate."""
resp = await self._delete("/assetsbulkupdate", params={"ids": ",".join(asset_ids)})
return AgravityInfoResponse.model_validate(resp.json())
# ------------------------------------------------------------------
# Blobs / binary operations
# ------------------------------------------------------------------
async def get_asset_blob(
self,
asset_id: str,
c: str,
*,
portal_id: Optional[str] = None,
key: Optional[str] = None,
) -> AssetBlob:
"""GET /assets/{id}/blobs get a specific blob by format key."""
resp = await self._get(
f"/assets/{asset_id}/blobs",
params={"c": c, "portal_id": portal_id, "key": key},
)
return AssetBlob.model_validate(resp.json())
async def get_shared_asset_blob(
self,
asset_id: str,
share_id: str,
format: str,
*,
password: Optional[str] = None,
) -> AssetBlob:
"""GET /assets/{id}/blob retrieve a blob via share token."""
headers = {"ay-password": password} if password else None
resp = await self._get(
f"/assets/{asset_id}/blob",
params={"share_id": share_id, "format": format},
extra_headers=headers,
)
return AssetBlob.model_validate(resp.json())
async def download_asset(
self,
asset_id: str,
*,
format: Optional[str] = None,
c: Optional[str] = None,
) -> AssetBlob:
"""GET /assets/{id}/download resolve download URL for an asset."""
resp = await self._get(
f"/assets/{asset_id}/download",
params={"format": format, "c": c},
)
return AssetBlob.model_validate(resp.json())
async def resize_asset(
self,
asset_id: str,
*,
width: Optional[int] = None,
height: Optional[int] = None,
mode: Optional[str] = None,
format: Optional[str] = None,
bgcolor: Optional[str] = None,
dpi: Optional[int] = None,
depth: Optional[int] = None,
) -> bytes:
"""GET /assets/{id}/resize return resized image bytes."""
resp = await self._get(
f"/assets/{asset_id}/resize",
params={
"width": width,
"height": height,
"mode": mode,
"format": format,
"bgcolor": bgcolor,
"dpi": dpi,
"depth": depth,
},
)
return resp.content
async def imageedit_asset(
self,
asset_id: str,
*,
width: Optional[int] = None,
height: Optional[int] = None,
mode: Optional[str] = None,
target: Optional[str] = None,
bgcolor: Optional[str] = None,
dpi: Optional[int] = None,
depth: Optional[int] = None,
quality: Optional[int] = None,
colorspace: Optional[str] = None,
crop_x: Optional[int] = None,
crop_y: Optional[int] = None,
crop_width: Optional[int] = None,
crop_height: Optional[int] = None,
filter: Optional[str] = None,
original: Optional[bool] = None,
) -> bytes:
"""GET /assets/{id}/imageedit apply image transformations."""
resp = await self._get(
f"/assets/{asset_id}/imageedit",
params={
"width": width, "height": height, "mode": mode, "target": target,
"bgcolor": bgcolor, "dpi": dpi, "depth": depth, "quality": quality,
"colorspace": colorspace, "crop_x": crop_x, "crop_y": crop_y,
"crop_width": crop_width, "crop_height": crop_height,
"filter": filter, "original": original,
},
)
return resp.content
async def imageedit_asset_operations(
self,
asset_id: str,
operations: list[DynamicImageOperation],
) -> bytes:
"""POST /assets/{id}/imageedit apply a sequence of image operations."""
payload = [op.model_dump(by_alias=True, exclude_none=True) for op in operations]
resp = await self._post(f"/assets/{asset_id}/imageedit", json=payload)
return resp.content
async def imageedit_asset_by_format(
self,
asset_id: str,
download_format_id: str,
) -> bytes:
"""GET /assets/{id}/imageedit/{download_format_id}."""
resp = await self._get(f"/assets/{asset_id}/imageedit/{download_format_id}")
return resp.content
# ------------------------------------------------------------------
# Collections membership
# ------------------------------------------------------------------
async def get_asset_collections(self, asset_id: str) -> list[Collection]:
"""GET /assets/{id}/collections."""
resp = await self._get(f"/assets/{asset_id}/collections")
return [Collection.model_validate(item) for item in resp.json()]
async def add_asset_to_collection(
self,
asset_id: str,
*,
collection_id: Optional[str] = None,
) -> None:
"""POST /assets/{id}/tocollection."""
await self._post(
f"/assets/{asset_id}/tocollection",
params={"collectionid": collection_id},
)
# ------------------------------------------------------------------
# Availability
# ------------------------------------------------------------------
async def update_asset_availability(
self, asset_id: str, availability: AssetAvailability
) -> AssetAvailability:
"""PUT /assets/{id}/availability."""
resp = await self._put(
f"/assets/{asset_id}/availability",
json=availability.model_dump(by_alias=True, exclude_none=True),
)
return AssetAvailability.model_validate(resp.json())
# ------------------------------------------------------------------
# Relations
# ------------------------------------------------------------------
async def get_asset_relations(self, asset_id: str) -> list[AssetRelation]:
"""GET /assets/{id}/relations."""
resp = await self._get(f"/assets/{asset_id}/relations")
return [AssetRelation.model_validate(item) for item in resp.json()]
# ------------------------------------------------------------------
# Publishing (per-asset)
# ------------------------------------------------------------------
async def get_asset_publish(self, asset_id: str) -> PublishEntity:
"""GET /assets/{id}/publish."""
resp = await self._get(f"/assets/{asset_id}/publish")
return PublishEntity.model_validate(resp.json())
async def create_asset_publish(
self,
asset_id: str,
payload: Optional[dict[str, Any]] = None,
) -> PublishedAsset:
"""POST /assets/{id}/publish."""
resp = await self._post(f"/assets/{asset_id}/publish", json=payload)
return PublishedAsset.model_validate(resp.json())
async def get_asset_published_by_id(
self, asset_id: str, pub_id: str
) -> PublishedAsset:
"""GET /assets/{id}/publish/{pid}."""
resp = await self._get(f"/assets/{asset_id}/publish/{pub_id}")
return PublishedAsset.model_validate(resp.json())
# ------------------------------------------------------------------
# Versioning (per-asset)
# ------------------------------------------------------------------
async def get_asset_versions(self, asset_id: str) -> VersionEntity:
"""GET /assets/{id}/versions."""
resp = await self._get(f"/assets/{asset_id}/versions")
return VersionEntity.model_validate(resp.json())
async def create_asset_version(
self,
asset_id: str,
payload: Optional[dict[str, Any]] = None,
) -> VersionedAsset:
"""POST /assets/{id}/versions create a new version snapshot."""
resp = await self._post(f"/assets/{asset_id}/versions", json=payload)
return VersionedAsset.model_validate(resp.json())
async def upload_asset_version(
self,
asset_id: str,
file: bytes,
filename: str,
*,
content_type: str = "application/octet-stream",
) -> VersionedAsset:
"""POST /assets/{id}/versionsupload upload a new version file."""
files = {"file": (filename, file, content_type)}
resp = await self._post(f"/assets/{asset_id}/versionsupload", files=files)
return VersionedAsset.model_validate(resp.json())
async def restore_asset_version(
self, asset_id: str, version_nr: int
) -> dict[str, Any]:
"""POST /assets/{id}/versions/{vNr}/restore."""
resp = await self._post(f"/assets/{asset_id}/versions/{version_nr}/restore")
return resp.json()
async def delete_asset_version(self, asset_id: str, version_nr: int) -> None:
"""DELETE /assets/{id}/versions/{vNr}."""
await self._delete(f"/assets/{asset_id}/versions/{version_nr}")
async def update_asset_version(
self,
asset_id: str,
version_nr: int,
payload: Optional[dict[str, Any]] = None,
) -> VersionedAsset:
"""POST /assets/{id}/versions/{vNr} update version metadata."""
resp = await self._post(
f"/assets/{asset_id}/versions/{version_nr}", json=payload
)
return VersionedAsset.model_validate(resp.json())
async def get_asset_version_blob(
self, asset_id: str, version_nr: int
) -> AssetBlob:
"""GET /assets/{id}/versions/{vNr}/blobs."""
resp = await self._get(f"/assets/{asset_id}/versions/{version_nr}/blobs")
return AssetBlob.model_validate(resp.json())

View file

@ -0,0 +1,35 @@
"""Auth API module (SAS tokens, users)."""
from __future__ import annotations
from typing import Optional
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.auth import AgravityUser, SasToken
class AuthApi(AgravityBaseApi):
"""Covers all /auth endpoints."""
async def get_container_write_sas_token(
self,
container: str,
blob: str,
*,
permission: Optional[str] = None,
) -> SasToken:
"""GET /auth/containerwrite."""
resp = await self._get(
"/auth/containerwrite",
params={"container": container, "blob": blob, "permission": permission},
)
return SasToken.model_validate(resp.json())
async def get_inbox_sas_token(self) -> SasToken:
"""GET /auth/inbox (deprecated)."""
resp = await self._get("/auth/inbox")
return SasToken.model_validate(resp.json())
async def get_users(self) -> list[AgravityUser]:
"""GET /auth/users."""
resp = await self._get("/auth/users")
return [AgravityUser.model_validate(item) for item in resp.json()]

160
agravity_client/api/base.py Normal file
View file

@ -0,0 +1,160 @@
"""Base HTTP client shared by all Agravity API modules."""
from __future__ import annotations
from typing import Any, Optional
import httpx
from agravity_client.config import AgravityConfig
from agravity_client.exceptions import (
AgravityAPIError,
AgravityAuthenticationError,
AgravityConnectionError,
AgravityNotFoundError,
AgravityTimeoutError,
AgravityValidationError,
)
from agravity_client.models.common import AgravityErrorResponse
class AgravityBaseApi:
"""Low-level async HTTP wrapper around ``httpx.AsyncClient``.
Every high-level API module subclasses this and calls the
``_get`` / ``_post`` / ``_put`` / ``_delete`` / ``_patch`` helpers,
which centralise auth header injection and error mapping.
"""
def __init__(self, http: httpx.AsyncClient, config: AgravityConfig) -> None:
self._http = http
self._config = config
# ------------------------------------------------------------------
# Private helpers
# ------------------------------------------------------------------
def _auth_headers(self) -> dict[str, str]:
"""Return the authentication headers for every request."""
if self._config.api_key:
return {"x-functions-key": self._config.api_key}
return {}
def _full_url(self, path: str) -> str:
"""Combine base URL with a relative path."""
return f"{self._config.base_url}/{path.lstrip('/')}"
@staticmethod
def _raise_for_status(response: httpx.Response) -> None:
"""Map HTTP error codes to typed exceptions."""
if response.is_success:
return
error_response: Optional[AgravityErrorResponse] = None
raw_body: Optional[str] = None
try:
data = response.json()
error_response = AgravityErrorResponse.model_validate(data)
except Exception:
raw_body = response.text
status = response.status_code
if status == 401:
raise AgravityAuthenticationError(status, error_response, raw_body)
if status == 404:
raise AgravityNotFoundError(status, error_response, raw_body)
if status in (400, 422):
raise AgravityValidationError(status, error_response, raw_body)
raise AgravityAPIError(status, error_response, raw_body)
# ------------------------------------------------------------------
# HTTP verbs
# ------------------------------------------------------------------
async def _request(
self,
method: str,
path: str,
*,
params: Optional[dict[str, Any]] = None,
json: Optional[Any] = None,
data: Optional[dict[str, Any]] = None,
files: Optional[dict[str, Any]] = None,
content: Optional[bytes] = None,
extra_headers: Optional[dict[str, str]] = None,
) -> httpx.Response:
"""Dispatch a request and raise on error."""
headers = {**self._auth_headers(), **(extra_headers or {})}
# Remove None-valued query params
if params:
params = {k: v for k, v in params.items() if v is not None}
try:
response = await self._http.request(
method,
self._full_url(path),
params=params or None,
json=json,
data=data,
files=files,
content=content,
headers=headers,
timeout=self._config.timeout,
)
except httpx.TimeoutException as exc:
raise AgravityTimeoutError(str(exc)) from exc
except httpx.TransportError as exc:
raise AgravityConnectionError(str(exc)) from exc
self._raise_for_status(response)
return response
async def _get(
self,
path: str,
params: Optional[dict[str, Any]] = None,
extra_headers: Optional[dict[str, str]] = None,
) -> httpx.Response:
return await self._request("GET", path, params=params, extra_headers=extra_headers)
async def _post(
self,
path: str,
json: Optional[Any] = None,
data: Optional[dict[str, Any]] = None,
files: Optional[dict[str, Any]] = None,
content: Optional[bytes] = None,
params: Optional[dict[str, Any]] = None,
extra_headers: Optional[dict[str, str]] = None,
) -> httpx.Response:
return await self._request(
"POST", path,
json=json, data=data, files=files, content=content,
params=params, extra_headers=extra_headers,
)
async def _put(
self,
path: str,
json: Optional[Any] = None,
params: Optional[dict[str, Any]] = None,
extra_headers: Optional[dict[str, str]] = None,
) -> httpx.Response:
return await self._request("PUT", path, json=json, params=params, extra_headers=extra_headers)
async def _patch(
self,
path: str,
json: Optional[Any] = None,
params: Optional[dict[str, Any]] = None,
extra_headers: Optional[dict[str, str]] = None,
) -> httpx.Response:
return await self._request("PATCH", path, json=json, params=params, extra_headers=extra_headers)
async def _delete(
self,
path: str,
params: Optional[dict[str, Any]] = None,
extra_headers: Optional[dict[str, str]] = None,
) -> httpx.Response:
return await self._request("DELETE", path, params=params, extra_headers=extra_headers)

View file

@ -0,0 +1,24 @@
"""Collection Type Management API module."""
from __future__ import annotations
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.collection import CollectionType, CollTypeItem
class CollectionTypesApi(AgravityBaseApi):
"""Covers all /collectiontypes endpoints."""
async def list_collection_types(self) -> list[CollectionType]:
"""GET /collectiontypes."""
resp = await self._get("/collectiontypes")
return [CollectionType.model_validate(item) for item in resp.json()]
async def get_collection_type(self, type_id: str) -> CollectionType:
"""GET /collectiontypes/{id}."""
resp = await self._get(f"/collectiontypes/{type_id}")
return CollectionType.model_validate(resp.json())
async def get_collection_type_items(self, type_id: str) -> list[CollTypeItem]:
"""GET /collectiontypes/{id}/items."""
resp = await self._get(f"/collectiontypes/{type_id}/items")
return [CollTypeItem.model_validate(item) for item in resp.json()]

View file

@ -0,0 +1,106 @@
"""Collection Management API module."""
from __future__ import annotations
from typing import Any, Optional
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.collection import (
Collection,
EntityListResult,
EntityNamesRequest,
MoveCollectionBody,
)
class CollectionsApi(AgravityBaseApi):
"""Covers all /collections endpoints."""
async def list_collections(
self,
*,
accept_language: Optional[str] = None,
) -> list[Collection]:
"""GET /collections."""
headers = {"Accept-Language": accept_language} if accept_language else None
resp = await self._get("/collections", extra_headers=headers)
return [Collection.model_validate(item) for item in resp.json()]
async def create_collection(
self, payload: dict[str, Any]
) -> Collection:
"""POST /collections."""
resp = await self._post("/collections", json=payload)
return Collection.model_validate(resp.json())
async def get_collection(
self,
collection_id: str,
*,
accept_language: Optional[str] = None,
) -> Collection:
"""GET /collections/{id}."""
headers = {"Accept-Language": accept_language} if accept_language else None
resp = await self._get(f"/collections/{collection_id}", extra_headers=headers)
return Collection.model_validate(resp.json())
async def update_collection(
self, collection_id: str, payload: dict[str, Any]
) -> Collection:
"""POST /collections/{id}."""
resp = await self._post(f"/collections/{collection_id}", json=payload)
return Collection.model_validate(resp.json())
async def delete_collection(self, collection_id: str) -> None:
"""DELETE /collections/{id}."""
await self._delete(f"/collections/{collection_id}")
async def get_collection_ancestors(
self,
collection_id: str,
*,
accept_language: Optional[str] = None,
) -> list[Collection]:
"""GET /collections/{id}/ancestors."""
headers = {"Accept-Language": accept_language} if accept_language else None
resp = await self._get(f"/collections/{collection_id}/ancestors", extra_headers=headers)
return [Collection.model_validate(item) for item in resp.json()]
async def get_collection_descendants(
self,
collection_id: str,
*,
accept_language: Optional[str] = None,
) -> list[Collection]:
"""GET /collections/{id}/descendants."""
headers = {"Accept-Language": accept_language} if accept_language else None
resp = await self._get(f"/collections/{collection_id}/descendants", extra_headers=headers)
return [Collection.model_validate(item) for item in resp.json()]
async def get_collection_preview(
self,
collection_id: str,
) -> bytes:
"""GET /collections/{id}/preview returns image bytes."""
resp = await self._get(f"/collections/{collection_id}/preview")
return resp.content
async def get_collections_by_names(
self,
request: EntityNamesRequest,
) -> EntityListResult:
"""POST /collections/bynames."""
resp = await self._post(
"/collections/bynames",
json=request.model_dump(by_alias=True, exclude_none=True),
)
return EntityListResult.model_validate(resp.json())
async def move_collection(
self,
payload: MoveCollectionBody,
) -> None:
"""POST /movecolltocoll move a collection to another parent."""
await self._post(
"/movecolltocoll",
json=payload.model_dump(by_alias=True, exclude_none=True),
)

View file

@ -0,0 +1,21 @@
"""Download Format Management API module."""
from __future__ import annotations
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.download import DownloadFormat
class DownloadFormatsApi(AgravityBaseApi):
"""Covers all /downloadformats endpoints."""
async def list_download_formats(self) -> list[DownloadFormat]:
"""GET /downloadformats."""
resp = await self._get("/downloadformats")
return [DownloadFormat.model_validate(item) for item in resp.json()]
async def list_download_formats_from_shared(
self, share_id: str
) -> list[DownloadFormat]:
"""GET /downloadformats/{shareId} formats available for a share token."""
resp = await self._get(f"/downloadformats/{share_id}")
return [DownloadFormat.model_validate(item) for item in resp.json()]

View file

@ -0,0 +1,50 @@
"""General / utility endpoints (version, deleted, durable, signalR, public)."""
from __future__ import annotations
from typing import Any, Optional
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.auth import AgravityVersion
from agravity_client.models.common import DeletedEntities, SignalRConnectionInfo
class GeneralApi(AgravityBaseApi):
"""Covers miscellaneous top-level endpoints."""
async def get_version(self) -> AgravityVersion:
"""GET /version API version and capabilities."""
resp = await self._get("/version")
return AgravityVersion.model_validate(resp.json())
async def get_deleted_entities(self) -> list[DeletedEntities]:
"""GET /deleted list recently deleted entities."""
resp = await self._get("/deleted")
return [DeletedEntities.model_validate(item) for item in resp.json()]
async def get_durable_status(
self,
instance_id: Optional[str] = None,
) -> dict[str, Any]:
"""GET /durable check status of a durable function instance."""
resp = await self._get("/durable", params={"instanceId": instance_id})
return resp.json()
async def get_signalr_connection_info(self) -> SignalRConnectionInfo:
"""GET /negotiate retrieve SignalR connection details."""
resp = await self._get("/negotiate")
return SignalRConnectionInfo.model_validate(resp.json())
async def get_public_asset(self, asset_id: str) -> dict[str, Any]:
"""GET /public/{id} publicly accessible asset metadata."""
resp = await self._get(f"/public/{asset_id}")
return resp.json()
async def view_public_asset(self, asset_id: str) -> bytes:
"""GET /public/{id}/view publicly accessible asset binary."""
resp = await self._get(f"/public/{asset_id}/view")
return resp.content
async def get_frontend_config(self) -> list[dict[str, Any]]:
"""GET /config retrieve frontend configuration entries."""
resp = await self._get("/config")
return resp.json()

View file

@ -0,0 +1,38 @@
"""Helper Tools API module (searchable/filterable item metadata)."""
from __future__ import annotations
from typing import Any, Optional
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.search import SearchableItem
class HelperApi(AgravityBaseApi):
"""Covers all /helper endpoints."""
async def get_user_defined_lists(
self,
*,
collection_type_id: Optional[str] = None,
) -> list[dict[str, Any]]:
"""GET /helper/userdefinedlists."""
resp = await self._get(
"/helper/userdefinedlists",
params={"collectiontypeid": collection_type_id},
)
return resp.json()
async def get_searchable_item_names(self) -> list[str]:
"""GET /helper/searchableitemnames list names of searchable fields."""
resp = await self._get("/helper/searchableitemnames")
return resp.json()
async def get_searchable_items(self) -> list[SearchableItem]:
"""GET /helper/searchableitems full searchable field definitions."""
resp = await self._get("/helper/searchableitems")
return [SearchableItem.model_validate(item) for item in resp.json()]
async def get_filterable_items(self) -> list[SearchableItem]:
"""GET /helper/filterableitems fields that can be used as filters."""
resp = await self._get("/helper/filterableitems")
return [SearchableItem.model_validate(item) for item in resp.json()]

View file

@ -0,0 +1,73 @@
"""Portal Management API module."""
from __future__ import annotations
from typing import Any, Optional
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.download import DownloadZipRequest, DownloadZipStatus
from agravity_client.models.portal import Portal, PortalConfiguration
class PortalsApi(AgravityBaseApi):
"""Covers all /portals endpoints."""
async def list_portals(self) -> list[Portal]:
"""GET /portals."""
resp = await self._get("/portals")
return [Portal.model_validate(item) for item in resp.json()]
async def get_portal(self, portal_id: str) -> Portal:
"""GET /portals/{id}."""
resp = await self._get(f"/portals/{portal_id}")
return Portal.model_validate(resp.json())
async def get_portal_config(self, portal_id: str) -> PortalConfiguration:
"""GET /portals/{id}/config."""
resp = await self._get(f"/portals/{portal_id}/config")
return PortalConfiguration.model_validate(resp.json())
async def enhance_portal_token(
self,
portal_id: str,
payload: Optional[dict[str, Any]] = None,
) -> dict[str, Any]:
"""POST /portals/{id}/enhancetoken augment the auth token claims."""
resp = await self._post(
f"/portals/{portal_id}/enhancetoken",
json=payload,
)
return resp.json()
async def save_portal_user_attributes(
self,
portal_id: str,
payload: dict[str, Any],
) -> dict[str, Any]:
"""POST /portals/{id}/saveuserattributes."""
resp = await self._post(
f"/portals/{portal_id}/saveuserattributes",
json=payload,
)
return resp.json()
async def get_portal_zip_status(self, portal_id: str) -> DownloadZipStatus:
"""GET /portals/{id}/zip."""
resp = await self._get(f"/portals/{portal_id}/zip")
return DownloadZipStatus.model_validate(resp.json())
async def create_portal_zip(
self,
portal_id: str,
payload: DownloadZipRequest,
) -> DownloadZipStatus:
"""POST /portals/{id}/zip."""
resp = await self._post(
f"/portals/{portal_id}/zip",
json=payload.model_dump(by_alias=True, exclude_none=True),
)
return DownloadZipStatus.model_validate(resp.json())
async def get_portal_asset_ids(self, portal_id: str) -> list[str]:
"""GET /portals/{id}/assetids."""
resp = await self._get(f"/portals/{portal_id}/assetids")
return resp.json()

View file

@ -0,0 +1,14 @@
"""Publishing API module (global published assets list)."""
from __future__ import annotations
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.publish import PublishEntity
class PublishingApi(AgravityBaseApi):
"""Covers the /publish endpoint."""
async def list_published(self) -> list[PublishEntity]:
"""GET /publish list all published entities across assets."""
resp = await self._get("/publish")
return [PublishEntity.model_validate(item) for item in resp.json()]

View file

@ -0,0 +1,57 @@
"""Asset Relation and Asset Relation Type API modules."""
from __future__ import annotations
from typing import Any, Optional
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.relation import AssetRelation, AssetRelationType
class RelationsApi(AgravityBaseApi):
"""Covers /relations and /assetrelationtypes endpoints."""
# ------------------------------------------------------------------
# Asset Relations (CRUD)
# ------------------------------------------------------------------
async def list_relations(self) -> list[AssetRelation]:
"""GET /relations."""
resp = await self._get("/relations")
return [AssetRelation.model_validate(item) for item in resp.json()]
async def create_relation(self, payload: dict[str, Any]) -> AssetRelation:
"""POST /relations."""
resp = await self._post("/relations", json=payload)
return AssetRelation.model_validate(resp.json())
async def get_relation(self, relation_id: str) -> AssetRelation:
"""GET /relations/{id}."""
resp = await self._get(f"/relations/{relation_id}")
return AssetRelation.model_validate(resp.json())
async def update_relation(
self,
relation_id: str,
payload: dict[str, Any],
) -> AssetRelation:
"""POST /relations/{id}."""
resp = await self._post(f"/relations/{relation_id}", json=payload)
return AssetRelation.model_validate(resp.json())
async def delete_relation(self, relation_id: str) -> None:
"""DELETE /relations/{id}."""
await self._delete(f"/relations/{relation_id}")
# ------------------------------------------------------------------
# Asset Relation Types (read-only)
# ------------------------------------------------------------------
async def list_relation_types(self) -> list[AssetRelationType]:
"""GET /assetrelationtypes."""
resp = await self._get("/assetrelationtypes")
return [AssetRelationType.model_validate(item) for item in resp.json()]
async def get_relation_type(self, type_id: str) -> AssetRelationType:
"""GET /assetrelationtypes/{id}."""
resp = await self._get(f"/assetrelationtypes/{type_id}")
return AssetRelationType.model_validate(resp.json())

View file

@ -0,0 +1,42 @@
"""Search Management API module."""
from __future__ import annotations
from typing import Optional
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.search import (
AzSearchOptions,
SavedSearch,
SearchAdminStatus,
SearchResult,
)
class SearchApi(AgravityBaseApi):
"""Covers /search and /savedsearch endpoints."""
async def search(self, options: AzSearchOptions) -> SearchResult:
"""POST /search perform a full-text/faceted search."""
resp = await self._post(
"/search",
json=options.model_dump(by_alias=True, exclude_none=True),
)
return SearchResult.model_validate(resp.json())
async def search_facette(self, options: AzSearchOptions) -> SearchResult:
"""POST /search/facette faceted search."""
resp = await self._post(
"/search/facette",
json=options.model_dump(by_alias=True, exclude_none=True),
)
return SearchResult.model_validate(resp.json())
async def get_search_admin_status(self) -> SearchAdminStatus:
"""GET /search/adminstatus."""
resp = await self._get("/search/adminstatus")
return SearchAdminStatus.model_validate(resp.json())
async def list_saved_searches(self) -> list[SavedSearch]:
"""GET /savedsearch."""
resp = await self._get("/savedsearch")
return [SavedSearch.model_validate(item) for item in resp.json()]

View file

@ -0,0 +1,28 @@
"""Secure Upload API module."""
from __future__ import annotations
from typing import Optional
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.secure_upload import SecureUploadEntity
class SecureUploadApi(AgravityBaseApi):
"""Covers all /secureupload endpoints."""
async def check_secure_upload(
self,
*,
token: Optional[str] = None,
) -> dict:
"""GET /secureupload check status / retrieve upload config."""
resp = await self._get("/secureupload", params={"token": token})
return resp.json()
async def create_secure_upload(
self,
payload: Optional[dict] = None,
) -> SecureUploadEntity:
"""POST /secureupload create a secure upload entity."""
resp = await self._post("/secureupload", json=payload)
return SecureUploadEntity.model_validate(resp.json())

View file

@ -0,0 +1,79 @@
"""Sharing Management API module."""
from __future__ import annotations
from typing import Optional
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.download import DownloadZipRequest, DownloadZipStatus
from agravity_client.models.sharing import QuickShareFull, SharedCollectionFull
class SharingApi(AgravityBaseApi):
"""Covers all /sharing endpoints."""
# ------------------------------------------------------------------
# Shared collections
# ------------------------------------------------------------------
async def list_shared_collections(self) -> list[SharedCollectionFull]:
"""GET /sharing."""
resp = await self._get("/sharing")
return [SharedCollectionFull.model_validate(item) for item in resp.json()]
async def get_shared_collection(
self,
token: str,
*,
password: Optional[str] = None,
) -> SharedCollectionFull:
"""GET /sharing/{token}."""
headers = {"ay-password": password} if password else None
resp = await self._get(f"/sharing/{token}", extra_headers=headers)
return SharedCollectionFull.model_validate(resp.json())
async def get_shared_collection_zip_status(
self,
token: str,
*,
password: Optional[str] = None,
) -> DownloadZipStatus:
"""GET /sharing/{token}/zip."""
headers = {"ay-password": password} if password else None
resp = await self._get(f"/sharing/{token}/zip", extra_headers=headers)
return DownloadZipStatus.model_validate(resp.json())
async def create_shared_collection_zip(
self,
token: str,
payload: DownloadZipRequest,
*,
password: Optional[str] = None,
) -> DownloadZipStatus:
"""POST /sharing/{token}/zip."""
headers = {"ay-password": password} if password else None
resp = await self._post(
f"/sharing/{token}/zip",
json=payload.model_dump(by_alias=True, exclude_none=True),
extra_headers=headers,
)
return DownloadZipStatus.model_validate(resp.json())
# ------------------------------------------------------------------
# Quick shares
# ------------------------------------------------------------------
async def list_quickshares(self) -> list[QuickShareFull]:
"""GET /sharing/quickshares."""
resp = await self._get("/sharing/quickshares")
return [QuickShareFull.model_validate(item) for item in resp.json()]
async def get_quickshare(
self,
token: str,
*,
password: Optional[str] = None,
) -> QuickShareFull:
"""GET /sharing/quickshares/{token}."""
headers = {"ay-password": password} if password else None
resp = await self._get(f"/sharing/quickshares/{token}", extra_headers=headers)
return QuickShareFull.model_validate(resp.json())

View file

@ -0,0 +1,30 @@
"""Static Defined Lists API module."""
from __future__ import annotations
from typing import Any
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.static_lists import StaticDefinedList
class StaticListsApi(AgravityBaseApi):
"""Covers all /staticdefinedlists endpoints."""
async def list_static_defined_lists(self) -> list[StaticDefinedList]:
"""GET /staticdefinedlists."""
resp = await self._get("/staticdefinedlists")
return [StaticDefinedList.model_validate(item) for item in resp.json()]
async def get_static_defined_list(self, list_id: str) -> StaticDefinedList:
"""GET /staticdefinedlists/{id}."""
resp = await self._get(f"/staticdefinedlists/{list_id}")
return StaticDefinedList.model_validate(resp.json())
async def update_static_defined_list(
self,
list_id: str,
payload: dict[str, Any],
) -> StaticDefinedList:
"""PUT /staticdefinedlists/{id}."""
resp = await self._put(f"/staticdefinedlists/{list_id}", json=payload)
return StaticDefinedList.model_validate(resp.json())

View file

@ -0,0 +1,43 @@
"""Translation Management API module."""
from __future__ import annotations
from typing import Any
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.common import DictionaryObject
class TranslationsApi(AgravityBaseApi):
"""Covers all /translations endpoints."""
async def get_translation(self, entity_id: str) -> DictionaryObject:
"""GET /translations/{id} retrieve all translations for an entity."""
resp = await self._get(f"/translations/{entity_id}")
return DictionaryObject.model_validate(resp.json())
async def update_translation_property(
self,
entity_id: str,
property_name: str,
payload: dict[str, Any],
) -> DictionaryObject:
"""PUT /translations/{id}/{property}."""
resp = await self._put(
f"/translations/{entity_id}/{property_name}",
json=payload,
)
return DictionaryObject.model_validate(resp.json())
async def update_translation_custom_field(
self,
entity_id: str,
property_name: str,
custom_field: str,
payload: dict[str, Any],
) -> DictionaryObject:
"""PUT /translations/{id}/{property}/{custom_field}."""
resp = await self._put(
f"/translations/{entity_id}/{property_name}/{custom_field}",
json=payload,
)
return DictionaryObject.model_validate(resp.json())

View file

@ -0,0 +1,45 @@
"""Web App Data API module."""
from __future__ import annotations
from typing import Optional, Union
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.search import AllWebAppData, GroupAllAppData
class WebAppDataApi(AgravityBaseApi):
"""Covers all /webappdata endpoints."""
async def get_web_app_data(
self,
*,
collection_type_id: Optional[str] = None,
accept_language: Optional[str] = None,
) -> Union[AllWebAppData, GroupAllAppData]:
"""GET /webappdata retrieve structured data for web app initialization.
If *collection_type_id* is supplied the API may return a
:class:`GroupAllAppData` instead of :class:`AllWebAppData`.
"""
params = {"collectiontypeid": collection_type_id}
headers = {"Accept-Language": accept_language} if accept_language else None
resp = await self._get("/webappdata", params=params, extra_headers=headers)
data = resp.json()
# Heuristic: GroupAllAppData has a "collection_type" key
if "collection_type" in data or "collectionType" in data:
return GroupAllAppData.model_validate(data)
return AllWebAppData.model_validate(data)
async def get_web_app_data_by_collection_type(
self,
collection_type_id: str,
*,
accept_language: Optional[str] = None,
) -> GroupAllAppData:
"""GET /webappdata/{collectionTypeId}."""
headers = {"Accept-Language": accept_language} if accept_language else None
resp = await self._get(
f"/webappdata/{collection_type_id}",
extra_headers=headers,
)
return GroupAllAppData.model_validate(resp.json())

View file

@ -0,0 +1,19 @@
"""Workspaces API module."""
from __future__ import annotations
from agravity_client.api.base import AgravityBaseApi
from agravity_client.models.workspace import Workspace
class WorkspacesApi(AgravityBaseApi):
"""Covers all /workspaces endpoints."""
async def list_workspaces(self) -> list[Workspace]:
"""GET /workspaces."""
resp = await self._get("/workspaces")
return [Workspace.model_validate(item) for item in resp.json()]
async def get_workspace(self, workspace_id: str) -> Workspace:
"""GET /workspaces/{id}."""
resp = await self._get(f"/workspaces/{workspace_id}")
return Workspace.model_validate(resp.json())

114
agravity_client/client.py Normal file
View file

@ -0,0 +1,114 @@
"""Top-level AgravityClient the single entry point for users."""
from __future__ import annotations
from types import TracebackType
from typing import Optional, Type
import httpx
from agravity_client.api.ai import AiApi
from agravity_client.api.assets import AssetsApi
from agravity_client.api.auth import AuthApi
from agravity_client.api.collection_types import CollectionTypesApi
from agravity_client.api.collections import CollectionsApi
from agravity_client.api.download_formats import DownloadFormatsApi
from agravity_client.api.general import GeneralApi
from agravity_client.api.helper import HelperApi
from agravity_client.api.portals import PortalsApi
from agravity_client.api.publishing import PublishingApi
from agravity_client.api.relations import RelationsApi
from agravity_client.api.search import SearchApi
from agravity_client.api.secure_upload import SecureUploadApi
from agravity_client.api.sharing import SharingApi
from agravity_client.api.static_lists import StaticListsApi
from agravity_client.api.translations import TranslationsApi
from agravity_client.api.webappdata import WebAppDataApi
from agravity_client.api.workspaces import WorkspacesApi
from agravity_client.config import AgravityConfig
class AgravityClient:
"""Fully async Agravity DAM API client.
Usage example::
import asyncio
from agravity_client import AgravityClient, AgravityConfig
async def main():
config = AgravityConfig(api_key="your-key")
async with AgravityClient(config) as client:
version = await client.general.get_version()
print(version)
asyncio.run(main())
The client can also be used without the context-manager idiom just
remember to call :meth:`aclose` when you are done.
"""
def __init__(
self,
config: Optional[AgravityConfig] = None,
*,
http_client: Optional[httpx.AsyncClient] = None,
) -> None:
self._config = config or AgravityConfig()
self._http = http_client or httpx.AsyncClient(
verify=self._config.verify_ssl,
timeout=self._config.timeout,
)
self._init_api_modules()
# ------------------------------------------------------------------
# Context-manager support
# ------------------------------------------------------------------
async def __aenter__(self) -> "AgravityClient":
return self
async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
await self.aclose()
async def aclose(self) -> None:
"""Close the underlying HTTP transport."""
await self._http.aclose()
# ------------------------------------------------------------------
# API module initialisation
# ------------------------------------------------------------------
def _init_api_modules(self) -> None:
args = (self._http, self._config)
self.ai = AiApi(*args)
self.assets = AssetsApi(*args)
self.auth = AuthApi(*args)
self.collection_types = CollectionTypesApi(*args)
self.collections = CollectionsApi(*args)
self.download_formats = DownloadFormatsApi(*args)
self.general = GeneralApi(*args)
self.helper = HelperApi(*args)
self.portals = PortalsApi(*args)
self.publishing = PublishingApi(*args)
self.relations = RelationsApi(*args)
self.search = SearchApi(*args)
self.secure_upload = SecureUploadApi(*args)
self.sharing = SharingApi(*args)
self.static_lists = StaticListsApi(*args)
self.translations = TranslationsApi(*args)
self.webappdata = WebAppDataApi(*args)
self.workspaces = WorkspacesApi(*args)
# ------------------------------------------------------------------
# Convenience read-only properties
# ------------------------------------------------------------------
@property
def config(self) -> AgravityConfig:
"""The current client configuration."""
return self._config

48
agravity_client/config.py Normal file
View file

@ -0,0 +1,48 @@
"""Agravity client configuration using Pydantic Settings."""
from __future__ import annotations
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class AgravityConfig(BaseSettings):
"""Configuration for the Agravity API client.
Values can be provided via environment variables or directly in code.
Environment variable names:
AGRAVITY_BASE_URL
AGRAVITY_API_KEY
AGRAVITY_TIMEOUT
AGRAVITY_VERIFY_SSL
"""
model_config = SettingsConfigDict(
env_prefix="AGRAVITY_",
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
base_url: str = Field(
default="https://devagravitypublic.azurewebsites.net/api",
description="The base URL for the Agravity API (without trailing slash).",
)
api_key: str = Field(
default="",
description="The API key (x-functions-key) used for authentication.",
)
timeout: float = Field(
default=60.0,
description="Default HTTP request timeout in seconds.",
)
verify_ssl: bool = Field(
default=True,
description="Whether to verify SSL certificates.",
)
@field_validator("base_url")
@classmethod
def strip_trailing_slash(cls, v: str) -> str:
return v.rstrip("/")

View file

@ -0,0 +1,59 @@
"""Custom exceptions for the Agravity API client."""
from __future__ import annotations
from typing import Optional
from agravity_client.models.common import AgravityErrorResponse
class AgravityError(Exception):
"""Base exception for all Agravity client errors."""
class AgravityAPIError(AgravityError):
"""Raised when the API returns a non-2xx response.
Attributes:
status_code: The HTTP status code returned by the server.
error_response: The parsed AgravityErrorResponse body, if available.
raw_body: The raw response text, if parsing failed.
"""
def __init__(
self,
status_code: int,
error_response: Optional[AgravityErrorResponse] = None,
raw_body: Optional[str] = None,
) -> None:
self.status_code = status_code
self.error_response = error_response
self.raw_body = raw_body
if error_response and error_response.error_message:
msg = f"HTTP {status_code}: {error_response.error_message}"
elif raw_body:
msg = f"HTTP {status_code}: {raw_body[:200]}"
else:
msg = f"HTTP {status_code}"
super().__init__(msg)
class AgravityAuthenticationError(AgravityAPIError):
"""Raised when the API returns a 401 Unauthorized response."""
class AgravityNotFoundError(AgravityAPIError):
"""Raised when the API returns a 404 Not Found response."""
class AgravityValidationError(AgravityAPIError):
"""Raised when the API returns a 400 Bad Request / 422 response."""
class AgravityTimeoutError(AgravityError):
"""Raised when an HTTP request times out."""
class AgravityConnectionError(AgravityError):
"""Raised when a network-level connection error occurs."""

View file

@ -0,0 +1,138 @@
"""Agravity DAM Pydantic models (re-exported from sub-modules)."""
from agravity_client.models.asset import (
Asset,
AssetAvailability,
AssetBlob,
AssetBlobOrientation,
AssetBlobType,
AssetBulkUpdate,
AssetCheckout,
AssetIconRule,
AssetIdFormat,
AssetPageResult,
AssetRole,
)
from agravity_client.models.auth import (
AgravityUser,
AgravityUserOnlineStatus,
AgravityVersion,
SasToken,
)
from agravity_client.models.collection import (
CollTypeItem,
Collection,
CollectionType,
CollectionUDL,
CollectionUDLListEntity,
CollectionUDLReference,
EntityListResult,
EntityNamesRequest,
MoveCollectionBody,
PermissionEntity,
PermissionRole,
)
from agravity_client.models.common import (
AgravityErrorResponse,
AgravityInfoResponse,
DataResult,
DeletedEntities,
DictionaryObject,
EntityId,
EntityIdName,
FrontendAppConfig,
SignalRConnectionInfo,
WhereParam,
WhereParamOperator,
WhereParamValueType,
)
from agravity_client.models.download import (
DistZipResponse,
DownloadFormat,
DownloadZipRequest,
DownloadZipStatus,
DynamicImageOperation,
SharedAllowedFormat,
ZipType,
)
from agravity_client.models.portal import (
Portal,
PortalAuthentication,
PortalAuthMethod,
PortalConfiguration,
PortalFields,
PortalLinks,
PortalTheme,
PortalUserContext,
)
from agravity_client.models.publish import PublishedAsset, PublishEntity
from agravity_client.models.relation import AssetRelation, AssetRelationType, RelatedAsset
from agravity_client.models.search import (
AllWebAppData,
AzSearchOptions,
GroupAllAppData,
SavedSearch,
SearchableItem,
SearchAdminDataSourceStatus,
SearchAdminIndexerLastRun,
SearchAdminIndexerStatus,
SearchAdminIndexStatus,
SearchAdminSkillStatus,
SearchAdminStatistics,
SearchAdminStatus,
SearchFacet,
SearchFacetEntity,
SearchResult,
)
from agravity_client.models.secure_upload import SecureUploadEntity
from agravity_client.models.sharing import (
QuickShareFull,
SharedAsset,
SharedCollectionFull,
)
from agravity_client.models.static_lists import StaticDefinedList
from agravity_client.models.versioning import VersionedAsset, VersionEntity
from agravity_client.models.workspace import Workspace
__all__ = [
# asset
"Asset", "AssetAvailability", "AssetBlob", "AssetBlobOrientation",
"AssetBlobType", "AssetBulkUpdate", "AssetCheckout", "AssetIconRule",
"AssetIdFormat", "AssetPageResult", "AssetRole",
# auth
"AgravityUser", "AgravityUserOnlineStatus", "AgravityVersion", "SasToken",
# collection
"CollTypeItem", "Collection", "CollectionType", "CollectionUDL",
"CollectionUDLListEntity", "CollectionUDLReference", "EntityListResult",
"EntityNamesRequest", "MoveCollectionBody", "PermissionEntity", "PermissionRole",
# common
"AgravityErrorResponse", "AgravityInfoResponse", "DataResult", "DeletedEntities",
"DictionaryObject", "EntityId", "EntityIdName", "FrontendAppConfig",
"SignalRConnectionInfo", "WhereParam", "WhereParamOperator", "WhereParamValueType",
# download
"DistZipResponse", "DownloadFormat", "DownloadZipRequest", "DownloadZipStatus",
"DynamicImageOperation", "SharedAllowedFormat", "ZipType",
# portal
"Portal", "PortalAuthentication", "PortalAuthMethod", "PortalConfiguration",
"PortalFields", "PortalLinks", "PortalTheme", "PortalUserContext",
# publish
"PublishedAsset", "PublishEntity",
# relation
"AssetRelation", "AssetRelationType", "RelatedAsset",
# search
"AllWebAppData", "AzSearchOptions", "GroupAllAppData", "SavedSearch",
"SearchableItem", "SearchAdminDataSourceStatus", "SearchAdminIndexerLastRun",
"SearchAdminIndexerStatus", "SearchAdminIndexStatus", "SearchAdminSkillStatus",
"SearchAdminStatistics", "SearchAdminStatus", "SearchFacet", "SearchFacetEntity",
"SearchResult",
# secure upload
"SecureUploadEntity",
# sharing
"QuickShareFull", "SharedAsset", "SharedCollectionFull",
# static lists
"StaticDefinedList",
# versioning
"VersionedAsset", "VersionEntity",
# workspace
"Workspace",
]

View file

@ -0,0 +1,208 @@
"""Asset-related Pydantic models."""
from __future__ import annotations
from enum import Enum
from typing import Any, Optional
from pydantic import ConfigDict, Field
from agravity_client.models.common import DictionaryObject, WhereParam, _Base
# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class AssetBlobType(str, Enum):
unknown = "UNKNOWN"
image = "IMAGE"
video = "VIDEO"
audio = "AUDIO"
document = "DOCUMENT"
text = "TEXT"
other = "OTHER"
class AssetBlobOrientation(str, Enum):
portrait = "PORTRAIT"
landscape = "LANDSCAPE"
square = "SQUARE"
class AssetRole(str, Enum):
none = "NONE"
viewer = "VIEWER"
editor = "EDITOR"
# ---------------------------------------------------------------------------
# AssetBlob
# ---------------------------------------------------------------------------
class AssetBlob(_Base):
blob_type: Optional[AssetBlobType] = AssetBlobType.unknown
name: Optional[str] = None
container: Optional[str] = None
size: Optional[int] = None
extension: Optional[str] = None
content_type: Optional[str] = None
md5: Optional[str] = None
add_data: Optional[dict[str, Any]] = None
# image metadata
width: Optional[int] = None
height: Optional[int] = None
maxwidthheight: Optional[int] = None
quality: Optional[float] = None
orientation: Optional[AssetBlobOrientation] = AssetBlobOrientation.portrait
colorspace: Optional[str] = None
profile: Optional[str] = None
transparency: Optional[bool] = None
mode: Optional[str] = None
target: Optional[str] = None
filter: Optional[str] = None
dpi_x: Optional[float] = None
dpi_y: Optional[float] = None
perhash: Optional[str] = None
dominantcolor: Optional[str] = None
depth: Optional[int] = None
animated: Optional[bool] = None
# video metadata
duration: Optional[int] = None
videocodec: Optional[str] = None
videobitrate: Optional[int] = None
fps: Optional[float] = None
colormode: Optional[str] = None
# audio metadata
audiocodec: Optional[str] = None
audiosamplerate: Optional[str] = None
audiochanneloutput: Optional[str] = None
audiobitrate: Optional[int] = None
# document metadata
author: Optional[str] = None
title: Optional[str] = None
language: Optional[str] = None
wordcount: Optional[int] = None
pages: Optional[int] = None
encoding_name: Optional[str] = None
encoding_code: Optional[str] = None
# URL / download info
url: Optional[str] = None
size_readable: Optional[str] = None
downloadable: Optional[bool] = None
expires: Optional[str] = Field(None, description="ISO 8601 date-time string")
uploaded_date: Optional[str] = Field(None, description="ISO 8601 date-time string")
uploaded_by: Optional[str] = None
# ---------------------------------------------------------------------------
# AssetCheckout
# ---------------------------------------------------------------------------
class AssetCheckout(_Base):
user_id: Optional[str] = None
checkout_date: Optional[str] = Field(None, description="ISO 8601 date-time string")
# ---------------------------------------------------------------------------
# AssetIconRule
# ---------------------------------------------------------------------------
class AssetIconRule(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
type: Optional[str] = None
path: Optional[str] = None
color: Optional[str] = None
value: Optional[str] = None
icon: Optional[str] = None
operator: Optional[str] = None
translations: Optional[dict[str, DictionaryObject]] = None
name: Optional[str] = None
description: Optional[str] = None
add_properties: Optional[dict[str, Any]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
# ---------------------------------------------------------------------------
# Asset (core entity)
# ---------------------------------------------------------------------------
class Asset(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
name: Optional[str] = None
asset_type: Optional[str] = None
duplicates: Optional[list[str]] = None
keywords: Optional[list[str]] = None
orig_blob: Optional[AssetBlob] = None
blobs: Optional[list[AssetBlob]] = None
collections: Optional[list[str]] = None
failed_reason: Optional[str] = None
quality_gate: Optional[list[str]] = None
region_of_origin: Optional[str] = None
availability: Optional[str] = None
available_from: Optional[str] = Field(None, description="ISO 8601 date-time string")
available_to: Optional[str] = Field(None, description="ISO 8601 date-time string")
checkout: Optional[AssetCheckout] = None
fs_synced: Optional[bool] = None
custom: Optional[dict[str, Any]] = None
items: Optional[list[Any]] = None # List[CollTypeItem] to avoid circular import
translations: Optional[dict[str, DictionaryObject]] = None
role: Optional[AssetRole] = AssetRole.none
description: Optional[str] = None
add_properties: Optional[dict[str, Any]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
# ---------------------------------------------------------------------------
# AssetAvailability
# ---------------------------------------------------------------------------
class AssetAvailability(_Base):
availability: Optional[str] = None
available_from: Optional[str] = Field(None, description="ISO 8601 date-time string")
available_to: Optional[str] = Field(None, description="ISO 8601 date-time string")
# ---------------------------------------------------------------------------
# AssetBulkUpdate
# ---------------------------------------------------------------------------
class AssetBulkUpdate(_Base):
collection_id: Optional[str] = None
ref_asset: Optional[Asset] = None
asset_ids: Optional[list[str]] = None
# ---------------------------------------------------------------------------
# AssetPageResult
# ---------------------------------------------------------------------------
class AssetPageResult(_Base):
page: Optional[list[Asset]] = None
page_size: Optional[int] = None
size: Optional[int] = None
continuation_token: Optional[str] = None
filter: Optional[list[WhereParam]] = None
# ---------------------------------------------------------------------------
# AssetIdFormat (used in QuickShareFull)
# ---------------------------------------------------------------------------
class AssetIdFormat(_Base):
id: Optional[str] = None
name: Optional[str] = None
format: Optional[str] = None

View file

@ -0,0 +1,58 @@
"""Auth-related Pydantic models."""
from __future__ import annotations
from typing import Optional
from pydantic import ConfigDict, Field
from agravity_client.models.common import _Base
class AgravityUserOnlineStatus(_Base):
status: Optional[str] = None
last_connection: Optional[str] = Field(None, description="ISO 8601 date-time string")
class AgravityUser(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
name: Optional[str] = None
email: Optional[str] = None
impersonation: Optional[str] = None
apikey: Optional[str] = None
online_status: Optional[AgravityUserOnlineStatus] = None
roles: Optional[list[str]] = None
groups: Optional[list[str]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
class AgravityVersion(_Base):
name: Optional[str] = None
company: Optional[str] = None
customer: Optional[str] = None
contact: Optional[str] = None
updated: Optional[str] = Field(None, description="ISO 8601 date-time string")
client_id: Optional[str] = None
tenant_id: Optional[str] = None
subscription_id: Optional[str] = None
base: Optional[str] = None
version: Optional[str] = None
enabled_features: Optional[list[str]] = None
region: Optional[str] = None
class SasToken(_Base):
token: Optional[str] = None
container: Optional[str] = None
blob: Optional[str] = None
url: Optional[str] = None
fulltoken: Optional[str] = None
expires: Optional[str] = Field(None, description="ISO 8601 date-time string")

View file

@ -0,0 +1,166 @@
"""Collection-related Pydantic models."""
from __future__ import annotations
from enum import Enum
from typing import Any, Optional
from pydantic import ConfigDict, Field
from agravity_client.models.common import DictionaryObject, _Base
# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class PermissionRole(str, Enum):
none = "NONE"
viewer = "VIEWER"
editor = "EDITOR"
# ---------------------------------------------------------------------------
# PermissionEntity
# ---------------------------------------------------------------------------
class PermissionEntity(_Base):
id: Optional[str] = None
role: Optional[PermissionRole] = PermissionRole.none
# ---------------------------------------------------------------------------
# CollTypeItem
# ---------------------------------------------------------------------------
class CollTypeItem(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
name: Optional[str] = None
item_type: Optional[str] = None
format: Optional[str] = None
label: Optional[str] = None
default_value: Optional[Any] = None
mandatory: Optional[bool] = None
searchable: Optional[bool] = None
onlyasset: Optional[bool] = None
multi: Optional[bool] = None
md5: Optional[str] = None
group: Optional[str] = None
order: Optional[int] = None
translations: Optional[dict[str, DictionaryObject]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
# ---------------------------------------------------------------------------
# Collection
# ---------------------------------------------------------------------------
class Collection(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
parent: Optional[str] = None
path: Optional[str] = None
level: Optional[int] = None
custom: Optional[dict[str, Any]] = None
items: Optional[list[CollTypeItem]] = None
translations: Optional[dict[str, DictionaryObject]] = None
role: Optional[PermissionRole] = PermissionRole.none
name: Optional[str] = None
description: Optional[str] = None
add_properties: Optional[dict[str, Any]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
# ---------------------------------------------------------------------------
# CollectionType
# ---------------------------------------------------------------------------
class CollectionType(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
name: Optional[str] = None
items: Optional[list[CollTypeItem]] = None
translations: Optional[dict[str, DictionaryObject]] = None
order: Optional[int] = None
permissions: Optional[list[PermissionEntity]] = None
permissionless: Optional[bool] = None
role: Optional[PermissionRole] = PermissionRole.none
description: Optional[str] = None
add_properties: Optional[dict[str, Any]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
# ---------------------------------------------------------------------------
# CollectionUDL (user-defined list)
# ---------------------------------------------------------------------------
class CollectionUDL(_Base):
id: Optional[str] = None
pk: Optional[str] = None
name: Optional[str] = None
entity_type: Optional[str] = None
status: Optional[str] = None
translations: Optional[dict[str, DictionaryObject]] = None
children: Optional[list[Any]] = None # List[EntityIdName]
class CollectionUDLReference(_Base):
parent: Optional[str] = None
coll_types: Optional[list[str]] = None
permissions: Optional[list[PermissionEntity]] = None
class CollectionUDLListEntity(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
udl_refs: Optional[list[CollectionUDLReference]] = None
udl_entries: Optional[list[CollectionUDL]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
# ---------------------------------------------------------------------------
# EntityNamesRequest / EntityListResult
# ---------------------------------------------------------------------------
class EntityNamesRequest(_Base):
names: Optional[list[str]] = None
filter: Optional[str] = None
class EntityListResult(_Base):
entities: Optional[list[Any]] = None # List[EntityIdName]
notfounds: Optional[list[str]] = None
# ---------------------------------------------------------------------------
# MoveCollectionBody
# ---------------------------------------------------------------------------
class MoveCollectionBody(_Base):
from_collection_id: Optional[str] = None
to_collection_id: Optional[str] = None
operation: Optional[str] = None

View file

@ -0,0 +1,131 @@
"""Common / shared Pydantic models for the Agravity API."""
from __future__ import annotations
from enum import Enum
from typing import Any, Optional
from pydantic import BaseModel, ConfigDict, Field
# ---------------------------------------------------------------------------
# Shared Config mixin: allow extra fields (the API may return undocumented ones)
# ---------------------------------------------------------------------------
class _Base(BaseModel):
model_config = ConfigDict(extra="allow", populate_by_name=True)
# ---------------------------------------------------------------------------
# Error / Info responses
# ---------------------------------------------------------------------------
class AgravityErrorResponse(_Base):
error_id: Optional[str] = None
error_message: Optional[str] = None
exception: Optional[str] = None
class AgravityInfoResponse(_Base):
info_id: Optional[str] = None
info_message: Optional[str] = None
info_object: Optional[Any] = None
# ---------------------------------------------------------------------------
# Dictionary helper
# ---------------------------------------------------------------------------
class DictionaryObject(_Base):
"""A freeform dictionary of key -> any value (used for translations etc.)"""
model_config = ConfigDict(extra="allow")
def to_dict(self) -> dict[str, Any]:
return dict(self.model_extra or {})
# ---------------------------------------------------------------------------
# EntityId / EntityIdName
# ---------------------------------------------------------------------------
class EntityId(_Base):
id: Optional[str] = None
pk: Optional[str] = None
class EntityIdName(_Base):
id: Optional[str] = None
pk: Optional[str] = None
name: Optional[str] = None
entity_type: Optional[str] = None
status: Optional[str] = None
translations: Optional[dict[str, DictionaryObject]] = None
# ---------------------------------------------------------------------------
# Frontend config
# ---------------------------------------------------------------------------
class FrontendAppConfig(_Base):
key: Optional[str] = None
value: Optional[str] = None
description: Optional[str] = None
contentType: Optional[str] = None
sinceApiVersion: Optional[str] = None
# ---------------------------------------------------------------------------
# SignalR
# ---------------------------------------------------------------------------
class SignalRConnectionInfo(_Base):
url: Optional[str] = None
accessToken: Optional[str] = None
# ---------------------------------------------------------------------------
# Deleted Entities
# ---------------------------------------------------------------------------
class DeletedEntities(_Base):
id: Optional[str] = None
name: Optional[str] = None
deleted: Optional[str] = Field(None, description="ISO 8601 date-time string")
entity_type: Optional[str] = None
# ---------------------------------------------------------------------------
# DataResult (search sub-model)
# ---------------------------------------------------------------------------
class DataResult(_Base):
asset: Optional[list[Any]] = None # List[Asset] forward ref resolved later
sum_asset_results: Optional[int] = None
collection: Optional[list[Any]] = None # List[Collection]
sum_collection_results: Optional[int] = None
# ---------------------------------------------------------------------------
# WhereParam (filter parameter in AssetPageResult)
# ---------------------------------------------------------------------------
class WhereParamOperator(str, Enum):
equals = "Equals"
not_equals = "NotEquals"
greater_than = "GreaterThan"
less_than = "LessThan"
greater_than_or_equal = "GreaterThanOrEqual"
less_than_or_equal = "LessThanOrEqual"
contains = "Contains"
starts_with = "StartsWith"
array_contains = "ArrayContains"
array_contains_partial = "ArrayContainsPartial"
is_defined = "IsDefined"
is_not_defined = "IsNotDefined"
is_empty = "IsEmpty"
is_not_empty = "IsNotEmpty"
class WhereParamValueType(str, Enum):
string = "String"
bool = "Bool"
number = "Number"
class WhereParam(_Base):
operator: Optional[WhereParamOperator] = WhereParamOperator.equals
field: Optional[str] = None
value: Optional[Any] = None
notPrefix: Optional[bool] = None
valueType: Optional[WhereParamValueType] = WhereParamValueType.string

View file

@ -0,0 +1,104 @@
"""Download-format and ZIP-related Pydantic models."""
from __future__ import annotations
from enum import Enum
from typing import Any, Optional
from pydantic import ConfigDict, Field
from agravity_client.models.common import DictionaryObject, _Base
from agravity_client.models.collection import PermissionEntity
# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class ZipType(str, Enum):
download = "DOWNLOAD"
shared = "SHARED"
quickshare = "QUICKSHARE"
portal = "PORTAL"
# ---------------------------------------------------------------------------
# DynamicImageOperation
# ---------------------------------------------------------------------------
class DynamicImageOperation(_Base):
operation: Optional[str] = None
params: Optional[list[Any]] = None
# ---------------------------------------------------------------------------
# SharedAllowedFormat
# ---------------------------------------------------------------------------
class SharedAllowedFormat(_Base):
asset_type: Optional[str] = None
format: Optional[str] = None
# ---------------------------------------------------------------------------
# DownloadFormat
# ---------------------------------------------------------------------------
class DownloadFormat(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
operations: Optional[list[DynamicImageOperation]] = None
extension: Optional[str] = None
asset_type: Optional[str] = None
origin: Optional[str] = None
fallback_thumb: Optional[bool] = None
target_filename: Optional[str] = None
translations: Optional[dict[str, DictionaryObject]] = None
permissions: Optional[list[PermissionEntity]] = None
name: Optional[str] = None
description: Optional[str] = None
add_properties: Optional[dict[str, Any]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
# ---------------------------------------------------------------------------
# DownloadZipRequest
# ---------------------------------------------------------------------------
class DownloadZipRequest(_Base):
id: Optional[str] = None
zip_type: Optional[ZipType] = ZipType.download
asset_ids: Optional[list[str]] = None
allowed_formats: Optional[list[SharedAllowedFormat]] = None
zipname: Optional[str] = None
email_to: Optional[list[str]] = None
message: Optional[str] = None
valid_until: Optional[str] = Field(None, description="ISO 8601 date-time string")
# ---------------------------------------------------------------------------
# DownloadZipStatus
# ---------------------------------------------------------------------------
class DownloadZipStatus(_Base):
id: Optional[str] = None
user: Optional[str] = None
percent: Optional[float] = None
part: Optional[float] = None
count: Optional[int] = None
message: Optional[str] = None
status: Optional[str] = None
zip_type: Optional[ZipType] = ZipType.download
zipname: Optional[str] = None
size: Optional[str] = None
url: Optional[str] = None
# ---------------------------------------------------------------------------
# DistZipResponse (used in GroupAllAppData)
# ---------------------------------------------------------------------------
class DistZipResponse(_Base):
url: Optional[str] = None
modified_date: Optional[str] = Field(None, description="ISO 8601 date-time string")
size: Optional[int] = None

View file

@ -0,0 +1,175 @@
"""Portal-related Pydantic models."""
from __future__ import annotations
from enum import Enum
from typing import Any, Optional
from pydantic import ConfigDict, Field
from agravity_client.models.collection import CollTypeItem, PermissionEntity
from agravity_client.models.common import DictionaryObject, FrontendAppConfig, _Base
from agravity_client.models.download import SharedAllowedFormat
from agravity_client.models.asset import AssetIconRule
from agravity_client.models.static_lists import StaticDefinedList
from agravity_client.models.collection import CollectionUDL
# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class PortalAuthMethod(str, Enum):
undefined = "UNDEFINED"
none = "NONE"
password = "PASSWORD"
eeid = "EEID"
auth0 = "AUTH0"
# ---------------------------------------------------------------------------
# PortalAuthentication
# ---------------------------------------------------------------------------
class PortalAuthentication(_Base):
method: Optional[PortalAuthMethod] = PortalAuthMethod.undefined
issuer: Optional[str] = None
client_id: Optional[str] = None
tenant_id: Optional[str] = None
password: Optional[str] = None
# ---------------------------------------------------------------------------
# PortalUserContext
# ---------------------------------------------------------------------------
class PortalUserContext(_Base):
key: Optional[str] = None
mandatory: Optional[bool] = None
mapping: Optional[dict[str, str]] = None
options: Optional[list[str]] = None
# ---------------------------------------------------------------------------
# PortalFields
# ---------------------------------------------------------------------------
class PortalFields(_Base):
name: Optional[str] = None
detail_order: Optional[int] = None
facet_order: Optional[int] = None
labels: Optional[dict[str, str]] = None
user_context: Optional[PortalUserContext] = None
format: Optional[str] = None
# ---------------------------------------------------------------------------
# PortalLinks
# ---------------------------------------------------------------------------
class PortalLinks(_Base):
conditions: Optional[str] = None
privacy: Optional[str] = None
impressum: Optional[str] = None
# ---------------------------------------------------------------------------
# PortalTheme
# ---------------------------------------------------------------------------
class PortalTheme(_Base):
logo_url: Optional[str] = None
topbar_color: Optional[str] = None
background_url: Optional[str] = None
fav_icon: Optional[str] = None
icon_empty: Optional[str] = None
icon_active: Optional[str] = None
colors: Optional[dict[str, Any]] = None
# ---------------------------------------------------------------------------
# Portal (base entity)
# ---------------------------------------------------------------------------
class _PortalBase(_Base):
"""Shared fields between Portal and PortalConfiguration."""
id: Optional[str] = None
entity_type: Optional[str] = None
authentication: Optional[PortalAuthentication] = None
languages: Optional[str] = None
fields: Optional[list[PortalFields]] = None
filter: Optional[str] = None
limit_ids: Optional[list[str]] = None
allowed_formats: Optional[list[SharedAllowedFormat]] = None
asset_icon_rules: Optional[list[AssetIconRule]] = None
allowed_origins: Optional[list[str]] = None
links: Optional[PortalLinks] = None
theme: Optional[PortalTheme] = None
name: Optional[str] = None
description: Optional[str] = None
add_properties: Optional[dict[str, Any]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
class Portal(_PortalBase):
pass
# ---------------------------------------------------------------------------
# PortalConfiguration (extends Portal with full config data)
# ---------------------------------------------------------------------------
class PortalConfiguration(_PortalBase):
download_formats: Optional[list[Any]] = None # List[DownloadFormat]
sdls: Optional[list[StaticDefinedList]] = None
udls: Optional[list[CollectionUDL]] = None
items: Optional[list[CollTypeItem]] = None
configs: Optional[list[FrontendAppConfig]] = None
# ---------------------------------------------------------------------------
# Custom claims provider response models (token issuance / attribute collection)
# ---------------------------------------------------------------------------
class CustomClaimsProviderClaims(_Base):
userContext: Optional[list[str]] = None
role: Optional[str] = None
class CustomClaimsProviderActionTokenIssuanceStart(_Base):
claims: Optional[CustomClaimsProviderClaims] = None
odata_type: Optional[str] = Field(None, alias="@odata.type")
model_config = ConfigDict(extra="allow", populate_by_name=True)
class CustomClaimsProviderActionAttributeCollectionSubmit(_Base):
odata_type: Optional[str] = Field(None, alias="@odata.type")
model_config = ConfigDict(extra="allow", populate_by_name=True)
class CustomClaimsProviderDataTokenIssuanceStart(_Base):
actions: Optional[list[CustomClaimsProviderActionTokenIssuanceStart]] = None
odata_type: Optional[str] = Field(None, alias="@odata.type")
model_config = ConfigDict(extra="allow", populate_by_name=True)
class CustomClaimsProviderDataAttributeCollectionSubmit(_Base):
actions: Optional[list[CustomClaimsProviderActionAttributeCollectionSubmit]] = None
odata_type: Optional[str] = Field(None, alias="@odata.type")
model_config = ConfigDict(extra="allow", populate_by_name=True)
class CustomClaimsProviderResponseContentTokenIssuanceStart(_Base):
data: Optional[CustomClaimsProviderDataTokenIssuanceStart] = None
class CustomClaimsProviderResponseContentAttributeCollectionSubmit(_Base):
data: Optional[CustomClaimsProviderDataAttributeCollectionSubmit] = None
# Convenience alias used by the API client
from typing import Union as _Union # noqa: E402
CustomClaimsProviderResponse = _Union[
CustomClaimsProviderResponseContentTokenIssuanceStart,
CustomClaimsProviderResponseContentAttributeCollectionSubmit,
]

View file

@ -0,0 +1,39 @@
"""Publish-related Pydantic models."""
from __future__ import annotations
from typing import Any, Optional
from pydantic import ConfigDict, Field
from agravity_client.models.common import _Base
class PublishedAsset(_Base):
id: Optional[str] = None
name: Optional[str] = None
target: Optional[str] = None
description: Optional[str] = None
usecases: Optional[list[str]] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
url: Optional[str] = None
cdn: Optional[str] = None
status_table_id: Optional[str] = None
format: Optional[str] = None
properties: Optional[dict[str, Any]] = None
class PublishEntity(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
published: Optional[list[PublishedAsset]] = None
region_of_origin: Optional[str] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)

View file

@ -0,0 +1,51 @@
"""Asset relation Pydantic models."""
from __future__ import annotations
from typing import Any, Optional
from pydantic import ConfigDict, Field
from agravity_client.models.collection import PermissionEntity
from agravity_client.models.common import DictionaryObject, _Base
class RelatedAsset(_Base):
id: Optional[str] = None
parent: Optional[bool] = None
class AssetRelation(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
assets: Optional[list[RelatedAsset]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
class AssetRelationType(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
hierarchical: Optional[bool] = None
sequential: Optional[bool] = None
unique_per_asset: Optional[bool] = None
translations: Optional[dict[str, DictionaryObject]] = None
permissions: Optional[list[PermissionEntity]] = None
name: Optional[str] = None
description: Optional[str] = None
add_properties: Optional[dict[str, Any]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)

View file

@ -0,0 +1,172 @@
"""Search-related Pydantic models (includes web-app data and saved searches)."""
from __future__ import annotations
from typing import Any, Optional
from pydantic import ConfigDict, Field
from agravity_client.models.asset import Asset
from agravity_client.models.collection import Collection, CollectionType
from agravity_client.models.common import DictionaryObject, _Base
from agravity_client.models.download import DistZipResponse
from agravity_client.models.publish import PublishedAsset
# ---------------------------------------------------------------------------
# AzSearchOptions
# ---------------------------------------------------------------------------
class AzSearchOptions(_Base):
searchString: Optional[str] = None
limit: Optional[int] = None
skip: Optional[int] = None
collectiontypeid: Optional[str] = None
collectionid: Optional[str] = None
filter: Optional[str] = None
orderby: Optional[str] = None
mode: Optional[str] = None
broadness: Optional[int] = None
rel_id: Optional[str] = None
ids: Optional[str] = None
portal_id: Optional[str] = None
scopefilter: Optional[str] = None
# ---------------------------------------------------------------------------
# SearchFacet
# ---------------------------------------------------------------------------
class SearchFacetEntity(_Base):
count: Optional[int] = None
value: Optional[str] = None
name: Optional[str] = None
class SearchFacet(_Base):
name: Optional[str] = None
entities: Optional[list[SearchFacetEntity]] = None
# ---------------------------------------------------------------------------
# DataResult → SearchResult
# ---------------------------------------------------------------------------
class _DataResult(_Base):
asset: Optional[list[Asset]] = None
sum_asset_results: Optional[int] = None
collection: Optional[list[Collection]] = None
sum_collection_results: Optional[int] = None
class SearchResult(_Base):
data_result: Optional[_DataResult] = None
options: Optional[AzSearchOptions] = None
facets: Optional[list[SearchFacet]] = None
count: Optional[int] = None
# ---------------------------------------------------------------------------
# SearchableItem
# ---------------------------------------------------------------------------
class SearchableItem(_Base):
name: Optional[str] = None
is_key: Optional[bool] = None
filterable: Optional[bool] = None
hidden: Optional[bool] = None
searchable: Optional[bool] = None
facetable: Optional[bool] = None
sortable: Optional[bool] = None
is_collection: Optional[bool] = None
searchtype: Optional[str] = None
fields: Optional[list["SearchableItem"]] = None
# ---------------------------------------------------------------------------
# SearchAdmin* models
# ---------------------------------------------------------------------------
class SearchAdminStatistics(_Base):
documentcount: Optional[int] = None
storagesizebytes: Optional[int] = None
class SearchAdminIndexStatus(_Base):
name: Optional[str] = None
status: Optional[str] = None
statistics: Optional[SearchAdminStatistics] = None
class SearchAdminIndexerLastRun(_Base):
status: Optional[str] = None
starttime: Optional[str] = None
endtime: Optional[str] = None
itemcount: Optional[int] = None
faileditemcount: Optional[int] = None
class SearchAdminIndexerStatus(_Base):
name: Optional[str] = None
status: Optional[str] = None
error: Optional[str] = None
lastrun: Optional[SearchAdminIndexerLastRun] = None
history: Optional[list[SearchAdminIndexerLastRun]] = None
class SearchAdminDataSourceStatus(_Base):
name: Optional[str] = None
status: Optional[str] = None
class SearchAdminSkillStatus(_Base):
name: Optional[str] = None
skills: Optional[list[str]] = None
class SearchAdminStatus(_Base):
index: Optional[SearchAdminIndexStatus] = None
indexer: Optional[SearchAdminIndexerStatus] = None
datasource: Optional[SearchAdminDataSourceStatus] = None
skillsets: Optional[list[SearchAdminSkillStatus]] = None
# ---------------------------------------------------------------------------
# SavedSearch
# ---------------------------------------------------------------------------
class SavedSearch(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
searchstring: Optional[str] = None
external: Optional[bool] = None
translations: Optional[dict[str, DictionaryObject]] = None
name: Optional[str] = None
description: Optional[str] = None
add_properties: Optional[dict[str, Any]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
# ---------------------------------------------------------------------------
# Web App Data
# ---------------------------------------------------------------------------
class AllWebAppData(_Base):
root_collection: Optional[Collection] = None
subcollections: Optional[list[Collection]] = None
assets: Optional[list[Asset]] = None
pub_assets: Optional[list[PublishedAsset]] = None
created_date: Optional[str] = None
class KeyValuePairObject(_Base):
model_config = ConfigDict(extra="allow")
class GroupAllAppData(_Base):
collection_type: Optional[CollectionType] = None
collections: Optional[list[Collection]] = None
assets: Optional[list[Asset]] = None
created_date: Optional[str] = None
add_info: Optional[list[KeyValuePairObject]] = None
dist: Optional[DistZipResponse] = None

View file

@ -0,0 +1,35 @@
"""Secure Upload Pydantic model."""
from __future__ import annotations
from typing import Optional
from pydantic import ConfigDict, Field
from agravity_client.models.common import _Base
class CreateSftpUserResult(_Base):
url: Optional[str] = None
password: Optional[str] = None
class SecureUploadEntity(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
collection_id: Optional[str] = None
url: Optional[str] = None
valid_until: Optional[str] = Field(None, description="ISO 8601 date-time string")
password: Optional[str] = None
asset_tags: Optional[list[str]] = None
message: Optional[str] = None
sftp_connection: Optional[CreateSftpUserResult] = None
check_name_for_version: Optional[bool] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)

View file

@ -0,0 +1,71 @@
"""Sharing-related Pydantic models (shared collections and quick shares)."""
from __future__ import annotations
from typing import Any, Optional
from pydantic import ConfigDict, Field
from agravity_client.models.asset import AssetBlob, AssetIdFormat
from agravity_client.models.common import EntityId, _Base
from agravity_client.models.download import SharedAllowedFormat
class SharedAsset(_Base):
id: Optional[str] = None
name: Optional[str] = None
description: Optional[str] = None
created_date: Optional[str] = None
modified_date: Optional[str] = None
asset_type: Optional[str] = None
orig_blob: Optional[AssetBlob] = None
blobs: Optional[list[AssetBlob]] = None
class SharedCollectionFull(_Base):
page: Optional[list[SharedAsset]] = None
page_size: Optional[int] = None
size: Optional[int] = None
continuation_token: Optional[str] = None
id: Optional[str] = None
entity_type: Optional[str] = None
collection_id: Optional[str] = None
url: Optional[str] = None
valid_until: Optional[str] = Field(None, description="ISO 8601 date-time string")
valid_for: Optional[str] = None
message: Optional[str] = None
global_: Optional[bool] = Field(None, alias="global")
allowed_formats: Optional[list[SharedAllowedFormat]] = None
password: Optional[str] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)
class QuickShareFull(_Base):
page: Optional[list[SharedAsset]] = None
page_size: Optional[int] = None
size: Optional[int] = None
continuation_token: Optional[str] = None
id: Optional[str] = None
entity_type: Optional[str] = None
name: Optional[str] = None
assets: Optional[list[AssetIdFormat]] = None
users: Optional[list[EntityId]] = None
expires: Optional[str] = Field(None, description="ISO 8601 date-time string")
url: Optional[str] = None
zip_url: Optional[str] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)

View file

@ -0,0 +1,27 @@
"""Static Defined List Pydantic model."""
from __future__ import annotations
from typing import Any, Optional
from pydantic import ConfigDict, Field
from agravity_client.models.common import DictionaryObject, _Base
class StaticDefinedList(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
translations: Optional[dict[str, DictionaryObject]] = None
values: Optional[list[str]] = None
name: Optional[str] = None
description: Optional[str] = None
add_properties: Optional[dict[str, Any]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)

View file

@ -0,0 +1,36 @@
"""Asset versioning Pydantic models."""
from __future__ import annotations
from typing import Optional
from pydantic import ConfigDict, Field
from agravity_client.models.asset import AssetBlob
from agravity_client.models.common import _Base
class VersionedAsset(_Base):
version_nr: Optional[int] = None
until_date: Optional[str] = Field(None, description="ISO 8601 date-time string")
version_info: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
blob_data: Optional[AssetBlob] = None
blob_uploaded: Optional[str] = Field(None, description="ISO 8601 date-time string")
mime_type: Optional[str] = None
class VersionEntity(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
versions: Optional[list[VersionedAsset]] = None
region_of_origin: Optional[str] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)

View file

@ -0,0 +1,30 @@
"""Workspace Pydantic model."""
from __future__ import annotations
from typing import Any, Optional
from pydantic import ConfigDict, Field
from agravity_client.models.collection import CollectionType, PermissionEntity
from agravity_client.models.common import DictionaryObject, _Base
class Workspace(_Base):
id: Optional[str] = None
entity_type: Optional[str] = None
name: Optional[str] = None
collection_types: Optional[list[CollectionType]] = None
translations: Optional[dict[str, DictionaryObject]] = None
order: Optional[int] = None
permissions: Optional[list[PermissionEntity]] = None
description: Optional[str] = None
add_properties: Optional[dict[str, Any]] = None
status: Optional[str] = None
created_date: Optional[str] = None
created_by: Optional[str] = None
modified_date: Optional[str] = None
modified_by: Optional[str] = None
pk: Optional[str] = None
etag: Optional[str] = Field(None, alias="_etag")
model_config = ConfigDict(extra="allow", populate_by_name=True)

70
pyproject.toml Normal file
View file

@ -0,0 +1,70 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "agravity-client"
version = "0.1.0"
description = "Pythonic, Pydantic-driven async REST client for the Agravity DAM API (v10.3.0)."
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
keywords = ["agravity", "dam", "rest", "api", "client", "pydantic", "httpx"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
]
dependencies = [
"httpx>=0.27",
"pydantic>=2.6",
"pydantic-settings>=2.2",
]
[project.optional-dependencies]
dev = [
"pytest>=8.1",
"pytest-asyncio>=0.23",
"pytest-httpx>=0.30",
"ruff>=0.4",
"mypy>=1.10",
]
[tool.hatch.build.targets.wheel]
packages = ["agravity_client"]
# ---------------------------------------------------------------------------
# Ruff
# ---------------------------------------------------------------------------
[tool.ruff]
target-version = "py310"
line-length = 100
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "ANN", "B", "C4", "T20"]
ignore = [
"ANN101", # missing type annotation for self
"ANN102", # missing type annotation for cls
"ANN401", # Dynamically typed expressions (Any) are disallowed
]
# ---------------------------------------------------------------------------
# mypy
# ---------------------------------------------------------------------------
[tool.mypy]
python_version = "3.10"
strict = true
ignore_missing_imports = true
# ---------------------------------------------------------------------------
# pytest
# ---------------------------------------------------------------------------
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

8501
swagger.json Normal file

File diff suppressed because one or more lines are too long

1
tests/__init__.py Normal file
View file

@ -0,0 +1 @@
# tests package

31
tests/conftest.py Normal file
View file

@ -0,0 +1,31 @@
"""Shared pytest fixtures for the agravity-client test suite."""
from __future__ import annotations
import pytest
import pytest_asyncio
import httpx
from agravity_client.client import AgravityClient
from agravity_client.config import AgravityConfig
@pytest.fixture
def config() -> AgravityConfig:
"""Return a test AgravityConfig with a dummy API key."""
return AgravityConfig(
api_key="test-api-key",
base_url="https://api.example.local/api",
)
@pytest_asyncio.fixture
async def client(config: AgravityConfig) -> AgravityClient:
"""Return an AgravityClient backed by a real (but not-yet-connected) transport.
Tests that need to mock HTTP responses should use ``pytest-httpx``'s
``httpx_mock`` fixture and pass a transport via::
client._http = httpx.AsyncClient(transport=httpx_mock.get_async_handler())
"""
async with AgravityClient(config) as c:
yield c

72
tests/test_models.py Normal file
View file

@ -0,0 +1,72 @@
"""Basic smoke tests for Pydantic model validation."""
from __future__ import annotations
import pytest
from agravity_client.models import (
Asset,
AssetBlob,
AssetBlobType,
AssetPageResult,
Collection,
AgravityVersion,
SearchResult,
AzSearchOptions,
)
class TestAssetModel:
def test_minimal_asset(self):
asset = Asset.model_validate({"id": "abc123"})
assert asset.id == "abc123"
def test_asset_with_blob_type_enum(self):
asset = Asset.model_validate({
"id": "abc",
"asset_type": "IMAGE",
})
assert asset.asset_type == "IMAGE"
def test_asset_page_result(self):
result = AssetPageResult.model_validate({
"assets": [{"id": "a1"}, {"id": "a2"}],
"size": 2,
})
assert len(result.assets) == 2
assert result.assets[0].id == "a1"
def test_extra_fields_allowed(self):
"""Unknown API fields must not raise ValidationError."""
asset = Asset.model_validate({"id": "x", "future_field": "value"})
assert asset.id == "x"
class TestCollectionModel:
def test_minimal_collection(self):
coll = Collection.model_validate({"id": "c1", "name": "Root"})
assert coll.id == "c1"
assert coll.name == "Root"
class TestVersionModel:
def test_agravity_version(self):
v = AgravityVersion.model_validate({
"name": "Agravity",
"version": "10.3.0",
})
assert v.version == "10.3.0"
class TestSearchModels:
def test_search_options_defaults(self):
opts = AzSearchOptions()
assert opts.limit is None
def test_search_options_with_values(self):
opts = AzSearchOptions(searchterm="hello", limit=5)
dumped = opts.model_dump(exclude_none=True)
assert dumped["searchterm"] == "hello"
assert dumped["limit"] == 5
def test_search_result_minimal(self):
result = SearchResult.model_validate({})
assert result.count is None