luml_api 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.
luml_api/__init__.py ADDED
@@ -0,0 +1,51 @@
1
+ __version__ = "0.1.0"
2
+ __title__ = "luml-sdk"
3
+
4
+ from luml_api._client import AsyncLumlClient, LumlClient
5
+ from luml_api._exceptions import (
6
+ APIError,
7
+ APIResponseValidationError,
8
+ APIStatusError,
9
+ AuthenticationError,
10
+ BadRequestError,
11
+ ConflictError,
12
+ InternalServerError,
13
+ LumlAPIError,
14
+ NotFoundError,
15
+ PermissionDeniedError,
16
+ UnprocessableEntityError,
17
+ )
18
+ from luml_api._types import (
19
+ ArtifactSortBy,
20
+ ArtifactStatus,
21
+ ArtifactType,
22
+ CollectionSortBy,
23
+ CollectionType,
24
+ CollectionTypeFilter,
25
+ SortOrder,
26
+ )
27
+
28
+ __all__ = [
29
+ "__version__",
30
+ "__title__",
31
+ "LumlClient",
32
+ "AsyncLumlClient",
33
+ "LumlAPIError",
34
+ "APIError",
35
+ "APIResponseValidationError",
36
+ "APIStatusError",
37
+ "BadRequestError",
38
+ "AuthenticationError",
39
+ "PermissionDeniedError",
40
+ "NotFoundError",
41
+ "ConflictError",
42
+ "UnprocessableEntityError",
43
+ "InternalServerError",
44
+ "ArtifactSortBy",
45
+ "ArtifactStatus",
46
+ "ArtifactType",
47
+ "CollectionSortBy",
48
+ "CollectionType",
49
+ "CollectionTypeFilter",
50
+ "SortOrder",
51
+ ]
@@ -0,0 +1,284 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+ import httpx
5
+ from httpx import URL
6
+
7
+ from luml_api._exceptions import (
8
+ APIStatusError,
9
+ AuthenticationError,
10
+ BadRequestError,
11
+ ConflictError,
12
+ InternalServerError,
13
+ NotFoundError,
14
+ PermissionDeniedError,
15
+ UnprocessableEntityError,
16
+ )
17
+
18
+
19
+ class BaseClient(ABC):
20
+ def __init__(
21
+ self,
22
+ base_url: str | URL,
23
+ timeout: float = 30.0,
24
+ ) -> None:
25
+ self._base_url: URL = URL(base_url) if isinstance(base_url, str) else base_url
26
+ self._timeout: float = timeout
27
+
28
+ @property
29
+ def base_url(self) -> URL:
30
+ return self._base_url
31
+
32
+ @base_url.setter
33
+ def base_url(self, url: URL) -> None:
34
+ self._base_url = url
35
+
36
+ @property
37
+ def timeout(self) -> float:
38
+ return self._timeout
39
+
40
+ @timeout.setter
41
+ def timeout(self, timeout: float) -> None:
42
+ self._timeout = timeout
43
+
44
+ @property
45
+ def auth_headers(self) -> dict[str, str]:
46
+ return {}
47
+
48
+ @property
49
+ def default_headers(self) -> dict[str, str]:
50
+ return {
51
+ "Accept": "application/json",
52
+ "Content-Type": "application/json",
53
+ "User-Agent": "luml-sdk/0.1.0",
54
+ **self.auth_headers,
55
+ }
56
+
57
+ @staticmethod
58
+ def _make_status_error(
59
+ err_msg: str,
60
+ *,
61
+ body: object,
62
+ response: httpx.Response,
63
+ ) -> APIStatusError:
64
+ if response.status_code == 400:
65
+ return BadRequestError(err_msg, response=response, body=body)
66
+
67
+ if response.status_code == 401:
68
+ return AuthenticationError(err_msg, response=response, body=body)
69
+
70
+ if response.status_code == 403:
71
+ return PermissionDeniedError(err_msg, response=response, body=body)
72
+
73
+ if response.status_code == 404:
74
+ return NotFoundError(err_msg, response=response, body=body)
75
+
76
+ if response.status_code == 409:
77
+ return ConflictError(err_msg, response=response, body=body)
78
+
79
+ if response.status_code == 422:
80
+ return UnprocessableEntityError(err_msg, response=response, body=body)
81
+
82
+ if response.status_code >= 500:
83
+ return InternalServerError(err_msg, response=response, body=body)
84
+ return APIStatusError(err_msg, response=response, body=body)
85
+
86
+ @abstractmethod
87
+ def _process_response(self, response: httpx.Response) -> Any: # noqa: ANN401
88
+ raise NotImplementedError()
89
+
90
+ @abstractmethod
91
+ def request(
92
+ self,
93
+ method: str,
94
+ url: str,
95
+ *,
96
+ json: dict[str, Any] | None = None,
97
+ params: dict[str, Any] | None = None,
98
+ headers: dict[str, str] | None = None,
99
+ **kwargs: Any, # noqa: ANN401
100
+ ) -> Any: # noqa: ANN401
101
+ raise NotImplementedError()
102
+
103
+ def get(self, url: str, **kwargs) -> Any: # noqa: ANN401
104
+ raise NotImplementedError()
105
+
106
+ def post(self, url: str, **kwargs) -> Any: # noqa: ANN401
107
+ raise NotImplementedError()
108
+
109
+ def patch(self, url: str, **kwargs) -> Any: # noqa: ANN401
110
+ raise NotImplementedError()
111
+
112
+ def delete(self, url: str, **kwargs) -> Any: # noqa: ANN401
113
+ raise NotImplementedError()
114
+
115
+ def put(self, url: str, **kwargs) -> Any: # noqa: ANN401
116
+ raise NotImplementedError()
117
+
118
+ @staticmethod
119
+ def filter_none(data: dict[str, Any]) -> dict[str, Any]: # noqa: ANN401
120
+ return {key: value for key, value in data.items() if value is not None}
121
+
122
+
123
+ class SyncBaseClient(BaseClient):
124
+ _client: httpx.Client
125
+
126
+ def __init__(
127
+ self,
128
+ base_url: str | URL,
129
+ timeout: float = 30.0,
130
+ ) -> None:
131
+ self._client = httpx.Client(base_url=base_url, timeout=timeout)
132
+ super().__init__(base_url=base_url, timeout=timeout)
133
+
134
+ def _process_response(self, response: httpx.Response) -> dict | None:
135
+ if response.status_code >= 400:
136
+ try:
137
+ body = response.json()
138
+ except Exception:
139
+ body = response.text if response.content else None
140
+
141
+ error_detail = ""
142
+ if isinstance(body, dict) and "detail" in body:
143
+ error_detail = f": {body['detail']}"
144
+
145
+ raise self._make_status_error(
146
+ f"\nError response {response.status_code} "
147
+ f"while requesting {response.request.method} "
148
+ f"{response.url}\n{error_detail}",
149
+ body=body,
150
+ response=response,
151
+ )
152
+
153
+ if response.status_code == 204 or not response.content:
154
+ return None
155
+ try:
156
+ return response.json()
157
+ except Exception as e:
158
+ raise InternalServerError(
159
+ f"Failed to parse response from "
160
+ f"{response.request.method} {response.url}: {e}",
161
+ response=response,
162
+ body=response.text,
163
+ ) from e
164
+
165
+ def request(
166
+ self,
167
+ method: str,
168
+ url: str,
169
+ *,
170
+ json: dict[str, Any] | None = None,
171
+ params: dict[str, Any] | None = None,
172
+ headers: dict[str, str] | None = None,
173
+ **kwargs: Any, # noqa: ANN401
174
+ ) -> Any: # noqa: ANN401
175
+ final_headers = {**self.default_headers}
176
+ if headers:
177
+ final_headers.update(headers)
178
+
179
+ response = self._client.request(
180
+ method=method,
181
+ url=url,
182
+ headers=final_headers,
183
+ json=json,
184
+ params=params,
185
+ **kwargs,
186
+ )
187
+ return self._process_response(response)
188
+
189
+ def get(self, url: str, **kwargs) -> Any: # noqa: ANN401
190
+ return self.request("GET", url, **kwargs)
191
+
192
+ def post(self, url: str, **kwargs) -> Any: # noqa: ANN401
193
+ return self.request("POST", url, **kwargs)
194
+
195
+ def patch(self, url: str, **kwargs) -> Any: # noqa: ANN401
196
+ return self.request("PATCH", url, **kwargs)
197
+
198
+ def delete(self, url: str, **kwargs) -> Any: # noqa: ANN401
199
+ return self.request("DELETE", url, **kwargs)
200
+
201
+ def put(self, url: str, **kwargs) -> Any: # noqa: ANN401
202
+ return self.request("PUT", url, **kwargs)
203
+
204
+
205
+ class AsyncBaseClient(BaseClient):
206
+ _client: httpx.AsyncClient
207
+
208
+ def __init__(
209
+ self,
210
+ base_url: str | URL,
211
+ timeout: float = 30.0,
212
+ ) -> None:
213
+ self._client = httpx.AsyncClient(base_url=base_url, timeout=timeout)
214
+ super().__init__(base_url=base_url, timeout=timeout)
215
+
216
+ async def _process_response(self, response: httpx.Response) -> dict | None:
217
+ if response.status_code >= 400:
218
+ try:
219
+ body = response.json()
220
+ except Exception:
221
+ body = response.text if response.content else None
222
+
223
+ error_detail = ""
224
+ if isinstance(body, dict) and "detail" in body:
225
+ error_detail = f": {body['detail']}"
226
+
227
+ raise self._make_status_error(
228
+ f"\nError response {response.status_code} "
229
+ f"while requesting {response.request.method} "
230
+ f"{response.url}\n{error_detail}",
231
+ body=body,
232
+ response=response,
233
+ )
234
+
235
+ if response.status_code == 204 or not response.content:
236
+ return None
237
+ try:
238
+ return response.json()
239
+ except Exception as e:
240
+ raise InternalServerError(
241
+ f"Failed to parse response from "
242
+ f"{response.request.method} {response.url}: {e}",
243
+ response=response,
244
+ body=response.text,
245
+ ) from e
246
+
247
+ async def request(
248
+ self,
249
+ method: str,
250
+ url: str,
251
+ *,
252
+ json: dict[str, Any] | None = None,
253
+ params: dict[str, Any] | None = None,
254
+ headers: dict[str, str] | None = None,
255
+ **kwargs: Any, # noqa: ANN401
256
+ ) -> Any: # noqa: ANN401
257
+ final_headers = {**self.default_headers}
258
+ if headers:
259
+ final_headers.update(headers)
260
+
261
+ response = await self._client.request(
262
+ method=method,
263
+ url=url,
264
+ headers=final_headers,
265
+ json=json,
266
+ params=params,
267
+ **kwargs,
268
+ )
269
+ return await self._process_response(response)
270
+
271
+ async def get(self, url: str, **kwargs) -> Any: # noqa: ANN401
272
+ return await self.request("GET", url, **kwargs)
273
+
274
+ async def post(self, url: str, **kwargs) -> Any: # noqa: ANN401
275
+ return await self.request("POST", url, **kwargs)
276
+
277
+ async def patch(self, url: str, **kwargs) -> Any: # noqa: ANN401
278
+ return await self.request("PATCH", url, **kwargs)
279
+
280
+ async def delete(self, url: str, **kwargs) -> Any: # noqa: ANN401
281
+ return await self.request("DELETE", url, **kwargs)
282
+
283
+ async def put(self, url: str, **kwargs) -> Any: # noqa: ANN401
284
+ return await self.request("PUT", url, **kwargs)