pycomad 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pycomad/__init__.py ADDED
@@ -0,0 +1,51 @@
1
+ """pycomad — Python client library for the Comad DAM (Digital Asset Management) API."""
2
+
3
+ from .client import ComadClient
4
+ from .exceptions import (
5
+ ComadAuthError,
6
+ ComadError,
7
+ ComadNotFoundError,
8
+ ComadServerError,
9
+ ComadValidationError,
10
+ )
11
+ from .models import (
12
+ AccessLevel,
13
+ Asset,
14
+ AssetMetadata,
15
+ AssetMetadataSummary,
16
+ AssetSearchResult,
17
+ AssetWithCategories,
18
+ Category,
19
+ CategoryWithChildren,
20
+ CustomFieldSchema,
21
+ FieldType,
22
+ LoginInfo,
23
+ UsageSummary,
24
+ ZipUploadResult,
25
+ )
26
+
27
+ __all__ = [
28
+ # Client
29
+ "ComadClient",
30
+ # Exceptions
31
+ "ComadError",
32
+ "ComadAuthError",
33
+ "ComadNotFoundError",
34
+ "ComadValidationError",
35
+ "ComadServerError",
36
+ # Enumerations
37
+ "AccessLevel",
38
+ "FieldType",
39
+ # Models
40
+ "LoginInfo",
41
+ "Asset",
42
+ "AssetWithCategories",
43
+ "AssetMetadata",
44
+ "AssetMetadataSummary",
45
+ "AssetSearchResult",
46
+ "Category",
47
+ "CategoryWithChildren",
48
+ "CustomFieldSchema",
49
+ "UsageSummary",
50
+ "ZipUploadResult",
51
+ ]
pycomad/_http.py ADDED
@@ -0,0 +1,133 @@
1
+ """Internal HTTP session: request dispatch and error mapping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from typing import Any, Dict, Optional
7
+
8
+ import requests
9
+
10
+ from .exceptions import (
11
+ ComadAuthError,
12
+ ComadError,
13
+ ComadNotFoundError,
14
+ ComadServerError,
15
+ ComadValidationError,
16
+ )
17
+
18
+
19
+ def _compact(d: Dict[str, Any]) -> Dict[str, Any]:
20
+ """Return a copy of *d* with all ``None``-valued keys removed."""
21
+ return {k: v for k, v in d.items() if v is not None}
22
+
23
+
24
+ def _str_id(value: Any) -> Optional[str]:
25
+ """Coerce a ``uuid.UUID`` or string identifier to ``str``, pass ``None`` through."""
26
+ if value is None:
27
+ return None
28
+ return str(value) if isinstance(value, uuid.UUID) else value
29
+
30
+
31
+ class _HttpSession:
32
+ """Thin wrapper around :class:`requests.Session` that handles auth headers and
33
+ maps non-2xx responses to typed exceptions.
34
+
35
+ Content-Type is NOT set as a session default so that multipart and binary
36
+ requests are unaffected. JSON methods pass ``json=`` to requests, which
37
+ sets the header automatically per-call.
38
+ """
39
+
40
+ def __init__(self, api_url: str, auth_url: str) -> None:
41
+ self._api_url = api_url.rstrip("/")
42
+ self._auth_url = auth_url.rstrip("/")
43
+ self._session = requests.Session()
44
+ self._token: Optional[str] = None
45
+
46
+ def set_token(self, token: str) -> None:
47
+ """Store the JWT and attach it to all subsequent requests."""
48
+ self._token = token
49
+ self._session.headers["Authorization"] = f"Bearer {token}"
50
+
51
+ def _require_auth(self) -> None:
52
+ if self._token is None:
53
+ raise ComadAuthError("Not authenticated — call ComadClient.login() first.")
54
+
55
+ def _raise_for_status(self, response: requests.Response) -> None:
56
+ if response.ok:
57
+ return
58
+ try:
59
+ body = response.json()
60
+ message = body.get("error") or body.get("message") or response.text
61
+ except Exception:
62
+ message = response.text
63
+ if response.status_code == 401:
64
+ raise ComadAuthError(message)
65
+ if response.status_code == 403:
66
+ raise ComadAuthError(f"Forbidden: {message}")
67
+ if response.status_code == 404:
68
+ raise ComadNotFoundError(message)
69
+ if response.status_code == 400:
70
+ raise ComadValidationError(message)
71
+ if response.status_code >= 500:
72
+ raise ComadServerError(f"Server error {response.status_code}: {message}")
73
+ raise ComadError(f"HTTP {response.status_code}: {message}")
74
+
75
+ # ── auth service ──────────────────────────────────────────────────────────
76
+
77
+ def post_auth(self, path: str, json: Any) -> Any:
78
+ """POST to the authentication service (no JWT header required)."""
79
+ response = self._session.post(f"{self._auth_url}{path}", json=json)
80
+ self._raise_for_status(response)
81
+ return response.json()
82
+
83
+ # ── DAM API ───────────────────────────────────────────────────────────────
84
+
85
+ def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Any:
86
+ self._require_auth()
87
+ response = self._session.get(f"{self._api_url}{path}", params=params)
88
+ self._raise_for_status(response)
89
+ return response.json()
90
+
91
+ def get_bytes(self, path: str) -> bytes:
92
+ """GET a binary response (download, thumbnail)."""
93
+ self._require_auth()
94
+ response = self._session.get(f"{self._api_url}{path}")
95
+ self._raise_for_status(response)
96
+ return response.content
97
+
98
+ def post(self, path: str, json: Any = None) -> Any:
99
+ self._require_auth()
100
+ response = self._session.post(f"{self._api_url}{path}", json=json)
101
+ self._raise_for_status(response)
102
+ return response.json() if response.status_code != 204 else None
103
+
104
+ def post_multipart(
105
+ self,
106
+ path: str,
107
+ files: Dict[str, Any],
108
+ data: Optional[Dict[str, str]] = None,
109
+ ) -> Any:
110
+ """POST multipart/form-data (file uploads).
111
+
112
+ Does not set Content-Type — requests computes the boundary automatically
113
+ when ``files`` is passed.
114
+ """
115
+ self._require_auth()
116
+ headers = {"Authorization": f"Bearer {self._token}"}
117
+ response = requests.post(
118
+ f"{self._api_url}{path}", files=files, data=data, headers=headers
119
+ )
120
+ self._raise_for_status(response)
121
+ return response.json() if response.status_code != 204 else None
122
+
123
+ def put(self, path: str, json: Any = None) -> Any:
124
+ self._require_auth()
125
+ response = self._session.put(f"{self._api_url}{path}", json=json)
126
+ self._raise_for_status(response)
127
+ return response.json() if response.status_code != 204 else None
128
+
129
+ def delete(self, path: str) -> Any:
130
+ self._require_auth()
131
+ response = self._session.delete(f"{self._api_url}{path}")
132
+ self._raise_for_status(response)
133
+ return response.json() if response.status_code != 204 else None