sammler-sdk 1.0.0__tar.gz

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.
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: sammler-sdk
3
+ Version: 1.0.0
4
+ Summary: Python SDK for interacting with Sammler GraphQL API
5
+ Requires-Python: >=3.8
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx>=0.24.0
8
+ Requires-Dist: pydantic>=2.0
9
+
10
+ # Sammler Python SDK
11
+
12
+ Python client for interacting with the Sammler GraphQL API.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install .
18
+ ```
19
+
20
+ ## Code Generation
21
+
22
+ To regenerate the client using `ariadne-codegen`:
23
+
24
+ ```bash
25
+ # Set up a virtual environment
26
+ python3 -m venv .venv
27
+ source .venv/bin/activate
28
+
29
+ # Install tools
30
+ pip install ariadne-codegen[http]
31
+
32
+ # Generate client
33
+ ariadne-codegen
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ import asyncio
40
+ from sammler import SammlerClient
41
+
42
+ async def main():
43
+ async with SammlerClient(api_key="your_api_key") as client:
44
+ collections = await client.get_collections()
45
+ for col in collections:
46
+ print(col.name)
47
+
48
+ if __name__ == "__main__":
49
+ asyncio.run(main())
50
+ ```
@@ -0,0 +1,41 @@
1
+ # Sammler Python SDK
2
+
3
+ Python client for interacting with the Sammler GraphQL API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install .
9
+ ```
10
+
11
+ ## Code Generation
12
+
13
+ To regenerate the client using `ariadne-codegen`:
14
+
15
+ ```bash
16
+ # Set up a virtual environment
17
+ python3 -m venv .venv
18
+ source .venv/bin/activate
19
+
20
+ # Install tools
21
+ pip install ariadne-codegen[http]
22
+
23
+ # Generate client
24
+ ariadne-codegen
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ import asyncio
31
+ from sammler import SammlerClient
32
+
33
+ async def main():
34
+ async with SammlerClient(api_key="your_api_key") as client:
35
+ collections = await client.get_collections()
36
+ for col in collections:
37
+ print(col.name)
38
+
39
+ if __name__ == "__main__":
40
+ asyncio.run(main())
41
+ ```
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sammler-sdk"
7
+ version = "1.0.0"
8
+ description = "Python SDK for interacting with Sammler GraphQL API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ dependencies = [
12
+ "httpx>=0.24.0",
13
+ "pydantic>=2.0"
14
+ ]
15
+
16
+ [tool.ariadne-codegen]
17
+ remote_schema_url = "https://sammler-api-3f60d.web.app"
18
+ queries_path = "../graphql/operations.graphql"
19
+ target_package_name = "graphql"
20
+ target_package_path = "sammler"
21
+ client_type = "async"
@@ -0,0 +1,15 @@
1
+ from .client import SammlerClient
2
+ from .graphql.get_collections import GetCollectionsCollections
3
+ from .graphql.get_collection import GetCollectionCollection, GetCollectionCollectionItems
4
+ from .graphql.get_items import GetItemsItems
5
+ from .graphql.get_item import GetItemItem, GetItemItemCollection
6
+
7
+ __all__ = [
8
+ "SammlerClient",
9
+ "GetCollectionsCollections",
10
+ "GetCollectionCollection",
11
+ "GetCollectionCollectionItems",
12
+ "GetItemsItems",
13
+ "GetItemItem",
14
+ "GetItemItemCollection",
15
+ ]
@@ -0,0 +1,61 @@
1
+ import httpx
2
+ from typing import List, Optional, Any
3
+ from .graphql.client import Client
4
+ from .graphql.get_collections import GetCollectionsCollections
5
+ from .graphql.get_collection import GetCollectionCollection
6
+ from .graphql.get_items import GetItemsItems
7
+ from .graphql.get_item import GetItemItem
8
+
9
+ class SammlerClient:
10
+ def __init__(
11
+ self,
12
+ api_key: str,
13
+ endpoint: str = "https://sammler-api-3f60d.web.app",
14
+ headers: Optional[dict] = None,
15
+ ):
16
+ self.endpoint = endpoint
17
+ req_headers = {
18
+ "x-api-key": api_key,
19
+ **(headers or {})
20
+ }
21
+ self.http_client = httpx.AsyncClient(headers=req_headers)
22
+ self.client = Client(url=self.endpoint, http_client=self.http_client)
23
+
24
+ async def __aenter__(self):
25
+ return self
26
+
27
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
28
+ await self.close()
29
+
30
+ async def close(self):
31
+ await self.http_client.aclose()
32
+
33
+ async def get_collections(self) -> List[GetCollectionsCollections]:
34
+ """Retrieve all collections belonging to the authenticated developer profile."""
35
+ response = await self.client.get_collections()
36
+ return response.collections
37
+
38
+ async def get_collection(self, id: str) -> Optional[GetCollectionCollection]:
39
+ """Retrieve a single collection by its ID, including all items inside it."""
40
+ response = await self.client.get_collection(id=id)
41
+ return response.collection
42
+
43
+ async def get_items(self) -> List[GetItemsItems]:
44
+ """Retrieve all collection items belonging to the authenticated developer profile."""
45
+ response = await self.client.get_items()
46
+ return response.items
47
+
48
+ async def get_item(self, id: str) -> Optional[GetItemItem]:
49
+ """Retrieve a single collection item by its ID, including its associated collection."""
50
+ response = await self.client.get_item(id=id)
51
+ return response.item
52
+
53
+ @property
54
+ def raw_client(self) -> Client:
55
+ """Exposes the raw underlying generated SDK client methods."""
56
+ return self.client
57
+
58
+ @property
59
+ def httpx_client(self) -> httpx.AsyncClient:
60
+ """Exposes the underlying httpx.AsyncClient instance."""
61
+ return self.http_client
@@ -0,0 +1,42 @@
1
+ # Generated by ariadne-codegen
2
+
3
+ from .async_base_client import AsyncBaseClient
4
+ from .base_model import BaseModel, Upload
5
+ from .client import Client
6
+ from .exceptions import (
7
+ GraphQLClientError,
8
+ GraphQLClientGraphQLError,
9
+ GraphQLClientGraphQLMultiError,
10
+ GraphQLClientHttpError,
11
+ GraphQLClientInvalidResponseError,
12
+ )
13
+ from .get_collection import (
14
+ GetCollection,
15
+ GetCollectionCollection,
16
+ GetCollectionCollectionItems,
17
+ )
18
+ from .get_collections import GetCollections, GetCollectionsCollections
19
+ from .get_item import GetItem, GetItemItem, GetItemItemCollection
20
+ from .get_items import GetItems, GetItemsItems
21
+
22
+ __all__ = [
23
+ "AsyncBaseClient",
24
+ "BaseModel",
25
+ "Client",
26
+ "GetCollection",
27
+ "GetCollectionCollection",
28
+ "GetCollectionCollectionItems",
29
+ "GetCollections",
30
+ "GetCollectionsCollections",
31
+ "GetItem",
32
+ "GetItemItem",
33
+ "GetItemItemCollection",
34
+ "GetItems",
35
+ "GetItemsItems",
36
+ "GraphQLClientError",
37
+ "GraphQLClientGraphQLError",
38
+ "GraphQLClientGraphQLMultiError",
39
+ "GraphQLClientHttpError",
40
+ "GraphQLClientInvalidResponseError",
41
+ "Upload",
42
+ ]
@@ -0,0 +1,395 @@
1
+ # Generated by ariadne-codegen
2
+
3
+ import asyncio
4
+ import enum
5
+ import json
6
+ from collections.abc import AsyncIterator
7
+ from typing import IO, Any, Optional, TypeVar, cast
8
+ from uuid import uuid4
9
+
10
+ import httpx
11
+ from pydantic import BaseModel
12
+ from pydantic_core import to_jsonable_python
13
+
14
+ from .base_model import UNSET, Upload
15
+ from .exceptions import (
16
+ GraphQLClientError,
17
+ GraphQLClientGraphQLMultiError,
18
+ GraphQLClientHttpError,
19
+ GraphQLClientInvalidMessageFormat,
20
+ GraphQLClientInvalidResponseError,
21
+ )
22
+
23
+ try:
24
+ from websockets import ( # type: ignore[import-not-found,unused-ignore]
25
+ ClientConnection,
26
+ )
27
+ from websockets import ( # type: ignore[import-not-found,unused-ignore]
28
+ connect as ws_connect,
29
+ )
30
+ from websockets.typing import ( # type: ignore[import-not-found,unused-ignore]
31
+ Data,
32
+ Origin,
33
+ Subprotocol,
34
+ )
35
+ except ImportError:
36
+ from contextlib import asynccontextmanager
37
+
38
+ @asynccontextmanager # type: ignore
39
+ async def ws_connect(*args, **kwargs):
40
+ raise NotImplementedError("Subscriptions require 'websockets' package.")
41
+ yield
42
+
43
+ ClientConnection = Any # type: ignore[misc,assignment,unused-ignore]
44
+ Data = Any # type: ignore[misc,assignment,unused-ignore]
45
+ Origin = Any # type: ignore[misc,assignment,unused-ignore]
46
+
47
+ def Subprotocol(*args, **kwargs): # type: ignore # noqa: N802, N803
48
+ raise NotImplementedError("Subscriptions require 'websockets' package.")
49
+
50
+
51
+ Self = TypeVar("Self", bound="AsyncBaseClient")
52
+
53
+ GRAPHQL_TRANSPORT_WS = "graphql-transport-ws"
54
+
55
+
56
+ class GraphQLTransportWSMessageType(str, enum.Enum):
57
+ CONNECTION_INIT = "connection_init"
58
+ CONNECTION_ACK = "connection_ack"
59
+ PING = "ping"
60
+ PONG = "pong"
61
+ SUBSCRIBE = "subscribe"
62
+ NEXT = "next"
63
+ ERROR = "error"
64
+ COMPLETE = "complete"
65
+
66
+
67
+ class AsyncBaseClient:
68
+ def __init__(
69
+ self,
70
+ url: str = "",
71
+ headers: Optional[dict[str, str]] = None,
72
+ http_client: Optional[httpx.AsyncClient] = None,
73
+ ws_url: str = "",
74
+ ws_headers: Optional[dict[str, Any]] = None,
75
+ ws_origin: Optional[str] = None,
76
+ ws_connection_init_payload: Optional[dict[str, Any]] = None,
77
+ ) -> None:
78
+ self.url = url
79
+ self.headers = headers
80
+ self.http_client = (
81
+ http_client if http_client else httpx.AsyncClient(headers=headers)
82
+ )
83
+
84
+ self.ws_url = ws_url
85
+ self.ws_headers = ws_headers or {}
86
+ self.ws_origin = Origin(ws_origin) if ws_origin else None
87
+ self.ws_connection_init_payload = ws_connection_init_payload
88
+
89
+ async def __aenter__(self: Self) -> Self:
90
+ return self
91
+
92
+ async def __aexit__(
93
+ self,
94
+ exc_type: object,
95
+ exc_val: object,
96
+ exc_tb: object,
97
+ ) -> None:
98
+ await self.http_client.aclose()
99
+
100
+ async def execute(
101
+ self,
102
+ query: str,
103
+ operation_name: Optional[str] = None,
104
+ variables: Optional[dict[str, Any]] = None,
105
+ **kwargs: Any,
106
+ ) -> httpx.Response:
107
+ processed_variables, files, files_map = self._process_variables(variables)
108
+
109
+ if files and files_map:
110
+ return await self._execute_multipart(
111
+ query=query,
112
+ operation_name=operation_name,
113
+ variables=processed_variables,
114
+ files=files,
115
+ files_map=files_map,
116
+ **kwargs,
117
+ )
118
+
119
+ return await self._execute_json(
120
+ query=query,
121
+ operation_name=operation_name,
122
+ variables=processed_variables,
123
+ **kwargs,
124
+ )
125
+
126
+ def get_data(self, response: httpx.Response) -> dict[str, Any]:
127
+ if not response.is_success:
128
+ raise GraphQLClientHttpError(
129
+ status_code=response.status_code, response=response
130
+ )
131
+
132
+ try:
133
+ response_json = response.json()
134
+ except ValueError as exc:
135
+ raise GraphQLClientInvalidResponseError(response=response) from exc
136
+
137
+ if (not isinstance(response_json, dict)) or (
138
+ "data" not in response_json and "errors" not in response_json
139
+ ):
140
+ raise GraphQLClientInvalidResponseError(response=response)
141
+
142
+ data = response_json.get("data")
143
+ errors = response_json.get("errors")
144
+
145
+ if errors:
146
+ raise GraphQLClientGraphQLMultiError.from_errors_dicts(
147
+ errors_dicts=errors, data=data
148
+ )
149
+
150
+ return cast(dict[str, Any], data)
151
+
152
+ async def execute_ws(
153
+ self,
154
+ query: str,
155
+ operation_name: Optional[str] = None,
156
+ variables: Optional[dict[str, Any]] = None,
157
+ **kwargs: Any,
158
+ ) -> AsyncIterator[dict[str, Any]]:
159
+ headers = self.ws_headers.copy()
160
+ headers.update(kwargs.pop("additional_headers", {}))
161
+
162
+ merged_kwargs: dict[str, Any] = {"origin": self.ws_origin}
163
+ merged_kwargs.update(kwargs)
164
+ merged_kwargs["additional_headers"] = headers
165
+
166
+ operation_id = str(uuid4())
167
+ async with ws_connect(
168
+ self.ws_url,
169
+ subprotocols=[Subprotocol(GRAPHQL_TRANSPORT_WS)],
170
+ **merged_kwargs,
171
+ ) as websocket:
172
+ await self._send_connection_init(websocket)
173
+ # Wait for connection_ack; some servers (e.g. Hasura) send ping before
174
+ # connection_ack, so we loop and handle pings until we get ack.
175
+ try:
176
+ await asyncio.wait_for(
177
+ self._wait_for_connection_ack(websocket),
178
+ timeout=5.0,
179
+ )
180
+ except asyncio.TimeoutError as exc:
181
+ raise GraphQLClientError(
182
+ "Connection ack not received within 5 seconds"
183
+ ) from exc
184
+ await self._send_subscribe(
185
+ websocket,
186
+ operation_id=operation_id,
187
+ query=query,
188
+ operation_name=operation_name,
189
+ variables=variables,
190
+ )
191
+
192
+ async for message in websocket:
193
+ data = await self._handle_ws_message(message, websocket)
194
+ if data and "connection_ack" not in data:
195
+ yield data
196
+
197
+ def _process_variables(
198
+ self, variables: Optional[dict[str, Any]]
199
+ ) -> tuple[
200
+ dict[str, Any], dict[str, tuple[str, IO[bytes], str]], dict[str, list[str]]
201
+ ]:
202
+ if not variables:
203
+ return {}, {}, {}
204
+
205
+ serializable_variables = self._convert_dict_to_json_serializable(variables)
206
+ return self._get_files_from_variables(serializable_variables)
207
+
208
+ def _convert_dict_to_json_serializable(
209
+ self, dict_: dict[str, Any]
210
+ ) -> dict[str, Any]:
211
+ return {
212
+ key: self._convert_value(value)
213
+ for key, value in dict_.items()
214
+ if value is not UNSET
215
+ }
216
+
217
+ def _convert_value(self, value: Any) -> Any:
218
+ if isinstance(value, BaseModel):
219
+ return value.model_dump(by_alias=True, exclude_unset=True)
220
+ if isinstance(value, list):
221
+ return [self._convert_value(item) for item in value]
222
+ return value
223
+
224
+ def _get_files_from_variables(
225
+ self, variables: dict[str, Any]
226
+ ) -> tuple[
227
+ dict[str, Any], dict[str, tuple[str, IO[bytes], str]], dict[str, list[str]]
228
+ ]:
229
+ files_map: dict[str, list[str]] = {}
230
+ files_list: list[Upload] = []
231
+
232
+ def separate_files(path: str, obj: Any) -> Any:
233
+ if isinstance(obj, list):
234
+ nulled_list = []
235
+ for index, value in enumerate(obj):
236
+ value = separate_files(f"{path}.{index}", value)
237
+ nulled_list.append(value)
238
+ return nulled_list
239
+
240
+ if isinstance(obj, dict):
241
+ nulled_dict = {}
242
+ for key, value in obj.items():
243
+ value = separate_files(f"{path}.{key}", value)
244
+ nulled_dict[key] = value
245
+ return nulled_dict
246
+
247
+ if isinstance(obj, Upload):
248
+ if obj in files_list:
249
+ file_index = files_list.index(obj)
250
+ files_map[str(file_index)].append(path)
251
+ else:
252
+ file_index = len(files_list)
253
+ files_list.append(obj)
254
+ files_map[str(file_index)] = [path]
255
+ return None
256
+
257
+ return obj
258
+
259
+ nulled_variables = separate_files("variables", variables)
260
+ files: dict[str, tuple[str, IO[bytes], str]] = {
261
+ str(i): (file_.filename, cast(IO[bytes], file_.content), file_.content_type)
262
+ for i, file_ in enumerate(files_list)
263
+ }
264
+ return nulled_variables, files, files_map
265
+
266
+ async def _execute_multipart(
267
+ self,
268
+ query: str,
269
+ operation_name: Optional[str],
270
+ variables: dict[str, Any],
271
+ files: dict[str, tuple[str, IO[bytes], str]],
272
+ files_map: dict[str, list[str]],
273
+ **kwargs: Any,
274
+ ) -> httpx.Response:
275
+ data = {
276
+ "operations": json.dumps(
277
+ {
278
+ "query": query,
279
+ "operationName": operation_name,
280
+ "variables": variables,
281
+ },
282
+ default=to_jsonable_python,
283
+ ),
284
+ "map": json.dumps(files_map, default=to_jsonable_python),
285
+ }
286
+
287
+ return await self.http_client.post(
288
+ url=self.url, data=data, files=files, **kwargs
289
+ )
290
+
291
+ async def _execute_json(
292
+ self,
293
+ query: str,
294
+ operation_name: Optional[str],
295
+ variables: dict[str, Any],
296
+ **kwargs: Any,
297
+ ) -> httpx.Response:
298
+ headers: dict[str, str] = {"Content-type": "application/json"}
299
+ headers.update(kwargs.get("headers", {}))
300
+
301
+ merged_kwargs: dict[str, Any] = kwargs.copy()
302
+ merged_kwargs["headers"] = headers
303
+
304
+ return await self.http_client.post(
305
+ url=self.url,
306
+ content=json.dumps(
307
+ {
308
+ "query": query,
309
+ "operationName": operation_name,
310
+ "variables": variables,
311
+ },
312
+ default=to_jsonable_python,
313
+ ),
314
+ **merged_kwargs,
315
+ )
316
+
317
+ async def _send_connection_init(self, websocket: ClientConnection) -> None:
318
+ payload: dict[str, Any] = {
319
+ "type": GraphQLTransportWSMessageType.CONNECTION_INIT.value
320
+ }
321
+ if self.ws_connection_init_payload:
322
+ payload["payload"] = self.ws_connection_init_payload
323
+ await websocket.send(json.dumps(payload))
324
+
325
+ async def _wait_for_connection_ack(self, websocket: ClientConnection) -> None:
326
+ """Read messages until connection_ack; handle ping/pong in between."""
327
+ async for message in websocket:
328
+ data = await self._handle_ws_message(message, websocket)
329
+ if data is not None and "connection_ack" in data:
330
+ return
331
+
332
+ async def _send_subscribe(
333
+ self,
334
+ websocket: ClientConnection,
335
+ operation_id: str,
336
+ query: str,
337
+ operation_name: Optional[str] = None,
338
+ variables: Optional[dict[str, Any]] = None,
339
+ ) -> None:
340
+ payload_inner: dict[str, Any] = {
341
+ "query": query,
342
+ "operationName": operation_name,
343
+ }
344
+ if variables:
345
+ payload_inner["variables"] = self._convert_dict_to_json_serializable(
346
+ variables
347
+ )
348
+ payload: dict[str, Any] = {
349
+ "id": operation_id,
350
+ "type": GraphQLTransportWSMessageType.SUBSCRIBE.value,
351
+ "payload": payload_inner,
352
+ }
353
+ await websocket.send(json.dumps(payload))
354
+
355
+ async def _handle_ws_message(
356
+ self,
357
+ message: Data,
358
+ websocket: ClientConnection,
359
+ expected_type: Optional[GraphQLTransportWSMessageType] = None,
360
+ ) -> Optional[dict[str, Any]]:
361
+ try:
362
+ message_dict = json.loads(message)
363
+ except json.JSONDecodeError as exc:
364
+ raise GraphQLClientInvalidMessageFormat(message=message) from exc
365
+
366
+ type_ = message_dict.get("type")
367
+ payload = message_dict.get("payload", {})
368
+
369
+ if not type_ or type_ not in {t.value for t in GraphQLTransportWSMessageType}:
370
+ raise GraphQLClientInvalidMessageFormat(message=message)
371
+
372
+ if expected_type and expected_type != type_:
373
+ raise GraphQLClientInvalidMessageFormat(
374
+ f"Invalid message received. Expected: {expected_type.value}"
375
+ )
376
+
377
+ if type_ == GraphQLTransportWSMessageType.NEXT:
378
+ if "data" not in payload:
379
+ raise GraphQLClientInvalidMessageFormat(message=message)
380
+ return cast(dict[str, Any], payload["data"])
381
+
382
+ if type_ == GraphQLTransportWSMessageType.COMPLETE:
383
+ await websocket.close()
384
+ elif type_ == GraphQLTransportWSMessageType.PING:
385
+ await websocket.send(
386
+ json.dumps({"type": GraphQLTransportWSMessageType.PONG.value})
387
+ )
388
+ elif type_ == GraphQLTransportWSMessageType.ERROR:
389
+ raise GraphQLClientGraphQLMultiError.from_errors_dicts(
390
+ errors_dicts=payload, data=message_dict
391
+ )
392
+ elif type_ == GraphQLTransportWSMessageType.CONNECTION_ACK:
393
+ return {"connection_ack": True}
394
+
395
+ return None
@@ -0,0 +1,30 @@
1
+ # Generated by ariadne-codegen
2
+
3
+ from io import IOBase
4
+
5
+ from pydantic import BaseModel as PydanticBaseModel
6
+ from pydantic import ConfigDict
7
+
8
+
9
+ class UnsetType:
10
+ def __bool__(self) -> bool:
11
+ return False
12
+
13
+
14
+ UNSET = UnsetType()
15
+
16
+
17
+ class BaseModel(PydanticBaseModel):
18
+ model_config = ConfigDict(
19
+ populate_by_name=True,
20
+ validate_assignment=True,
21
+ arbitrary_types_allowed=True,
22
+ protected_namespaces=(),
23
+ )
24
+
25
+
26
+ class Upload:
27
+ def __init__(self, filename: str, content: IOBase, content_type: str):
28
+ self.filename = filename
29
+ self.content = content
30
+ self.content_type = content_type
@@ -0,0 +1,128 @@
1
+ # Generated by ariadne-codegen
2
+ # Source: ../graphql/operations.graphql
3
+
4
+ from typing import Any
5
+
6
+ from .async_base_client import AsyncBaseClient
7
+ from .get_collection import GetCollection
8
+ from .get_collections import GetCollections
9
+ from .get_item import GetItem
10
+ from .get_items import GetItems
11
+
12
+
13
+ def gql(q: str) -> str:
14
+ return q
15
+
16
+
17
+ class Client(AsyncBaseClient):
18
+ async def get_collections(self, **kwargs: Any) -> GetCollections:
19
+ query = gql("""
20
+ query GetCollections {
21
+ collections {
22
+ id
23
+ userId
24
+ name
25
+ description
26
+ type
27
+ createdAt
28
+ updatedAt
29
+ }
30
+ }
31
+ """)
32
+ variables: dict[str, object] = {}
33
+ response = await self.execute(
34
+ query=query, operation_name="GetCollections", variables=variables, **kwargs
35
+ )
36
+ data = self.get_data(response)
37
+ return GetCollections.model_validate(data)
38
+
39
+ async def get_collection(self, id: str, **kwargs: Any) -> GetCollection:
40
+ query = gql("""
41
+ query GetCollection($id: ID!) {
42
+ collection(id: $id) {
43
+ id
44
+ userId
45
+ name
46
+ description
47
+ type
48
+ createdAt
49
+ updatedAt
50
+ items {
51
+ id
52
+ collectionId
53
+ name
54
+ description
55
+ quantity
56
+ value
57
+ condition
58
+ notes
59
+ createdAt
60
+ updatedAt
61
+ photoRef
62
+ }
63
+ }
64
+ }
65
+ """)
66
+ variables: dict[str, object] = {"id": id}
67
+ response = await self.execute(
68
+ query=query, operation_name="GetCollection", variables=variables, **kwargs
69
+ )
70
+ data = self.get_data(response)
71
+ return GetCollection.model_validate(data)
72
+
73
+ async def get_items(self, **kwargs: Any) -> GetItems:
74
+ query = gql("""
75
+ query GetItems {
76
+ items {
77
+ id
78
+ userId
79
+ collectionId
80
+ name
81
+ description
82
+ quantity
83
+ value
84
+ condition
85
+ notes
86
+ createdAt
87
+ updatedAt
88
+ photoRef
89
+ }
90
+ }
91
+ """)
92
+ variables: dict[str, object] = {}
93
+ response = await self.execute(
94
+ query=query, operation_name="GetItems", variables=variables, **kwargs
95
+ )
96
+ data = self.get_data(response)
97
+ return GetItems.model_validate(data)
98
+
99
+ async def get_item(self, id: str, **kwargs: Any) -> GetItem:
100
+ query = gql("""
101
+ query GetItem($id: ID!) {
102
+ item(id: $id) {
103
+ id
104
+ userId
105
+ collectionId
106
+ name
107
+ description
108
+ quantity
109
+ value
110
+ condition
111
+ notes
112
+ createdAt
113
+ updatedAt
114
+ photoRef
115
+ collection {
116
+ id
117
+ name
118
+ type
119
+ }
120
+ }
121
+ }
122
+ """)
123
+ variables: dict[str, object] = {"id": id}
124
+ response = await self.execute(
125
+ query=query, operation_name="GetItem", variables=variables, **kwargs
126
+ )
127
+ data = self.get_data(response)
128
+ return GetItem.model_validate(data)
@@ -0,0 +1,3 @@
1
+ # Generated by ariadne-codegen
2
+ # Source: https://sammler-api-3f60d.web.app
3
+
@@ -0,0 +1,85 @@
1
+ # Generated by ariadne-codegen
2
+
3
+ from typing import Any, Optional, Union
4
+
5
+ import httpx
6
+
7
+
8
+ class GraphQLClientError(Exception):
9
+ """Base exception."""
10
+
11
+
12
+ class GraphQLClientHttpError(GraphQLClientError):
13
+ def __init__(self, status_code: int, response: httpx.Response) -> None:
14
+ self.status_code = status_code
15
+ self.response = response
16
+
17
+ def __str__(self) -> str:
18
+ return f"HTTP status code: {self.status_code}"
19
+
20
+
21
+ class GraphQLClientInvalidResponseError(GraphQLClientError):
22
+ def __init__(self, response: httpx.Response) -> None:
23
+ self.response = response
24
+
25
+ def __str__(self) -> str:
26
+ return "Invalid response format."
27
+
28
+
29
+ class GraphQLClientGraphQLError(GraphQLClientError):
30
+ def __init__(
31
+ self,
32
+ message: str,
33
+ locations: Optional[list[dict[str, int]]] = None,
34
+ path: Optional[list[str]] = None,
35
+ extensions: Optional[dict[str, object]] = None,
36
+ original: Optional[dict[str, object]] = None,
37
+ ):
38
+ self.message = message
39
+ self.locations = locations
40
+ self.path = path
41
+ self.extensions = extensions
42
+ self.original = original
43
+
44
+ def __str__(self) -> str:
45
+ return self.message
46
+
47
+ @classmethod
48
+ def from_dict(cls, error: dict[str, Any]) -> "GraphQLClientGraphQLError":
49
+ return cls(
50
+ message=error["message"],
51
+ locations=error.get("locations"),
52
+ path=error.get("path"),
53
+ extensions=error.get("extensions"),
54
+ original=error,
55
+ )
56
+
57
+
58
+ class GraphQLClientGraphQLMultiError(GraphQLClientError):
59
+ def __init__(
60
+ self,
61
+ errors: list[GraphQLClientGraphQLError],
62
+ data: Optional[dict[str, Any]] = None,
63
+ ):
64
+ self.errors = errors
65
+ self.data = data
66
+
67
+ def __str__(self) -> str:
68
+ return "; ".join(str(e) for e in self.errors)
69
+
70
+ @classmethod
71
+ def from_errors_dicts(
72
+ cls, errors_dicts: list[dict[str, Any]], data: Optional[dict[str, Any]] = None
73
+ ) -> "GraphQLClientGraphQLMultiError":
74
+ return cls(
75
+ errors=[GraphQLClientGraphQLError.from_dict(e) for e in errors_dicts],
76
+ data=data,
77
+ )
78
+
79
+
80
+ class GraphQLClientInvalidMessageFormat(GraphQLClientError): # noqa: N818
81
+ def __init__(self, message: Union[str, bytes]) -> None:
82
+ self.message = message
83
+
84
+ def __str__(self) -> str:
85
+ return "Invalid message format."
@@ -0,0 +1,41 @@
1
+ # Generated by ariadne-codegen
2
+ # Source: ../graphql/operations.graphql
3
+
4
+ from typing import Optional
5
+
6
+ from pydantic import Field
7
+
8
+ from .base_model import BaseModel
9
+
10
+
11
+ class GetCollection(BaseModel):
12
+ collection: Optional["GetCollectionCollection"]
13
+
14
+
15
+ class GetCollectionCollection(BaseModel):
16
+ id: str
17
+ user_id: str = Field(alias="userId")
18
+ name: str
19
+ description: Optional[str]
20
+ type_: str = Field(alias="type")
21
+ created_at: str = Field(alias="createdAt")
22
+ updated_at: str = Field(alias="updatedAt")
23
+ items: list["GetCollectionCollectionItems"]
24
+
25
+
26
+ class GetCollectionCollectionItems(BaseModel):
27
+ id: str
28
+ collection_id: str = Field(alias="collectionId")
29
+ name: str
30
+ description: Optional[str]
31
+ quantity: int
32
+ value: Optional[float]
33
+ condition: Optional[str]
34
+ notes: Optional[str]
35
+ created_at: str = Field(alias="createdAt")
36
+ updated_at: str = Field(alias="updatedAt")
37
+ photo_ref: Optional[str] = Field(alias="photoRef")
38
+
39
+
40
+ GetCollection.model_rebuild()
41
+ GetCollectionCollection.model_rebuild()
@@ -0,0 +1,25 @@
1
+ # Generated by ariadne-codegen
2
+ # Source: ../graphql/operations.graphql
3
+
4
+ from typing import Optional
5
+
6
+ from pydantic import Field
7
+
8
+ from .base_model import BaseModel
9
+
10
+
11
+ class GetCollections(BaseModel):
12
+ collections: list["GetCollectionsCollections"]
13
+
14
+
15
+ class GetCollectionsCollections(BaseModel):
16
+ id: str
17
+ user_id: str = Field(alias="userId")
18
+ name: str
19
+ description: Optional[str]
20
+ type_: str = Field(alias="type")
21
+ created_at: str = Field(alias="createdAt")
22
+ updated_at: str = Field(alias="updatedAt")
23
+
24
+
25
+ GetCollections.model_rebuild()
@@ -0,0 +1,38 @@
1
+ # Generated by ariadne-codegen
2
+ # Source: ../graphql/operations.graphql
3
+
4
+ from typing import Optional
5
+
6
+ from pydantic import Field
7
+
8
+ from .base_model import BaseModel
9
+
10
+
11
+ class GetItem(BaseModel):
12
+ item: Optional["GetItemItem"]
13
+
14
+
15
+ class GetItemItem(BaseModel):
16
+ id: str
17
+ user_id: str = Field(alias="userId")
18
+ collection_id: str = Field(alias="collectionId")
19
+ name: str
20
+ description: Optional[str]
21
+ quantity: int
22
+ value: Optional[float]
23
+ condition: Optional[str]
24
+ notes: Optional[str]
25
+ created_at: str = Field(alias="createdAt")
26
+ updated_at: str = Field(alias="updatedAt")
27
+ photo_ref: Optional[str] = Field(alias="photoRef")
28
+ collection: "GetItemItemCollection"
29
+
30
+
31
+ class GetItemItemCollection(BaseModel):
32
+ id: str
33
+ name: str
34
+ type_: str = Field(alias="type")
35
+
36
+
37
+ GetItem.model_rebuild()
38
+ GetItemItem.model_rebuild()
@@ -0,0 +1,30 @@
1
+ # Generated by ariadne-codegen
2
+ # Source: ../graphql/operations.graphql
3
+
4
+ from typing import Optional
5
+
6
+ from pydantic import Field
7
+
8
+ from .base_model import BaseModel
9
+
10
+
11
+ class GetItems(BaseModel):
12
+ items: list["GetItemsItems"]
13
+
14
+
15
+ class GetItemsItems(BaseModel):
16
+ id: str
17
+ user_id: str = Field(alias="userId")
18
+ collection_id: str = Field(alias="collectionId")
19
+ name: str
20
+ description: Optional[str]
21
+ quantity: int
22
+ value: Optional[float]
23
+ condition: Optional[str]
24
+ notes: Optional[str]
25
+ created_at: str = Field(alias="createdAt")
26
+ updated_at: str = Field(alias="updatedAt")
27
+ photo_ref: Optional[str] = Field(alias="photoRef")
28
+
29
+
30
+ GetItems.model_rebuild()
@@ -0,0 +1,3 @@
1
+ # Generated by ariadne-codegen
2
+ # Source: https://sammler-api-3f60d.web.app
3
+
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: sammler-sdk
3
+ Version: 1.0.0
4
+ Summary: Python SDK for interacting with Sammler GraphQL API
5
+ Requires-Python: >=3.8
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: httpx>=0.24.0
8
+ Requires-Dist: pydantic>=2.0
9
+
10
+ # Sammler Python SDK
11
+
12
+ Python client for interacting with the Sammler GraphQL API.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install .
18
+ ```
19
+
20
+ ## Code Generation
21
+
22
+ To regenerate the client using `ariadne-codegen`:
23
+
24
+ ```bash
25
+ # Set up a virtual environment
26
+ python3 -m venv .venv
27
+ source .venv/bin/activate
28
+
29
+ # Install tools
30
+ pip install ariadne-codegen[http]
31
+
32
+ # Generate client
33
+ ariadne-codegen
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```python
39
+ import asyncio
40
+ from sammler import SammlerClient
41
+
42
+ async def main():
43
+ async with SammlerClient(api_key="your_api_key") as client:
44
+ collections = await client.get_collections()
45
+ for col in collections:
46
+ print(col.name)
47
+
48
+ if __name__ == "__main__":
49
+ asyncio.run(main())
50
+ ```
@@ -0,0 +1,20 @@
1
+ README.md
2
+ pyproject.toml
3
+ sammler/__init__.py
4
+ sammler/client.py
5
+ sammler/graphql/__init__.py
6
+ sammler/graphql/async_base_client.py
7
+ sammler/graphql/base_model.py
8
+ sammler/graphql/client.py
9
+ sammler/graphql/enums.py
10
+ sammler/graphql/exceptions.py
11
+ sammler/graphql/get_collection.py
12
+ sammler/graphql/get_collections.py
13
+ sammler/graphql/get_item.py
14
+ sammler/graphql/get_items.py
15
+ sammler/graphql/input_types.py
16
+ sammler_sdk.egg-info/PKG-INFO
17
+ sammler_sdk.egg-info/SOURCES.txt
18
+ sammler_sdk.egg-info/dependency_links.txt
19
+ sammler_sdk.egg-info/requires.txt
20
+ sammler_sdk.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ httpx>=0.24.0
2
+ pydantic>=2.0
@@ -0,0 +1 @@
1
+ sammler
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+