elapi 1.0.7__tar.gz → 1.0.7.dev1__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.
Files changed (60) hide show
  1. {elapi-1.0.7 → elapi-1.0.7.dev1}/PKG-INFO +2 -1
  2. {elapi-1.0.7 → elapi-1.0.7.dev1}/pyproject.toml +2 -1
  3. elapi-1.0.7.dev1/src/elapi/VERSION +1 -0
  4. elapi-1.0.7.dev1/src/elapi/api.py +552 -0
  5. elapi-1.0.7.dev1/src/elapi/cli/__init__.py +1 -0
  6. elapi-1.0.7.dev1/src/elapi/cli/_plugin_handler.py +44 -0
  7. elapi-1.0.7.dev1/src/elapi/cli/doc.py +86 -0
  8. elapi-1.0.7.dev1/src/elapi/cli/elapi.py +711 -0
  9. elapi-1.0.7.dev1/src/elapi/cli/helpers.py +78 -0
  10. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/configuration/__init__.py +3 -0
  11. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/configuration/config.py +9 -9
  12. elapi-1.0.7.dev1/src/elapi/endpoint.py +220 -0
  13. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/path.py +4 -2
  14. elapi-1.0.7.dev1/src/elapi/plugins/bill_teams/__init__.py +2 -0
  15. elapi-1.0.7.dev1/src/elapi/plugins/bill_teams/_doc.py +3 -0
  16. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/plugins/bill_teams/bill_teams.py +27 -21
  17. elapi-1.0.7.dev1/src/elapi/plugins/bill_teams/cli.py +143 -0
  18. elapi-1.0.7.dev1/src/elapi/plugins/experiments/__init__.py +3 -0
  19. elapi-1.0.7.dev1/src/elapi/plugins/experiments/_doc.py +24 -0
  20. elapi-1.0.7.dev1/src/elapi/plugins/experiments/cli.py +309 -0
  21. elapi-1.0.7.dev1/src/elapi/plugins/experiments/experiments.py +171 -0
  22. elapi-1.0.7.dev1/src/elapi/plugins/experiments/formats.py +77 -0
  23. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/plugins/show_config.py +5 -5
  24. elapi-1.0.7.dev1/src/elapi/plugins/utils/__init__.py +2 -0
  25. {elapi-1.0.7/src/elapi/plugins → elapi-1.0.7.dev1/src/elapi/plugins/utils}/export.py +17 -7
  26. elapi-1.0.7.dev1/src/elapi/plugins/utils/get_location_from_headers.py +27 -0
  27. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/format.py +4 -8
  28. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/validators/identity.py +15 -19
  29. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/validators/permission.py +2 -2
  30. elapi-1.0.7/src/elapi/VERSION +0 -1
  31. elapi-1.0.7/src/elapi/api.py +0 -200
  32. elapi-1.0.7/src/elapi/cli/_doc.py +0 -49
  33. elapi-1.0.7/src/elapi/cli/elapi.py +0 -481
  34. elapi-1.0.7/src/elapi/endpoint.py +0 -77
  35. elapi-1.0.7/src/elapi/plugins/__init__.py +0 -0
  36. elapi-1.0.7/src/elapi/plugins/bill_teams/__init__.py +0 -3
  37. {elapi-1.0.7 → elapi-1.0.7.dev1}/LICENSE +0 -0
  38. {elapi-1.0.7 → elapi-1.0.7.dev1}/README.md +0 -0
  39. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/__init__.py +0 -0
  40. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/_names.py +0 -0
  41. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/cli/__main__.py +0 -0
  42. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/configuration/_config_history.py +0 -0
  43. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/configuration/log_file.py +0 -0
  44. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/loggers/__init__.py +0 -0
  45. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/loggers/base.py +0 -0
  46. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/loggers/handlers/__init__.py +0 -0
  47. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/loggers/handlers/file.py +0 -0
  48. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/loggers/handlers/stderr.py +0 -0
  49. {elapi-1.0.7/src/elapi/cli → elapi-1.0.7.dev1/src/elapi/plugins}/__init__.py +0 -0
  50. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/plugins/bill_teams/invoice.py +0 -0
  51. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/plugins/version.py +0 -0
  52. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/__init__.py +0 -0
  53. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/_markdown_doc.py +0 -0
  54. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/_missing.py +0 -0
  55. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/base.py +0 -0
  56. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/colors.py +0 -0
  57. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/highlight.py +0 -0
  58. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/validators/__init__.py +0 -0
  59. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/validators/base.py +0 -0
  60. {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/validators/path.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: elapi
3
- Version: 1.0.7
3
+ Version: 1.0.7.dev1
4
4
  Summary: elAPI is a powerful, extensible API interface to eLabFTW built for the University Computing Centre (URZ, FIRE division) at Universität Heidelberg.
5
5
  Home-page: https://www.urz.uni-heidelberg.de/en/service-catalogue/software-and-applications/elabftw
6
6
  License: AGPL-3.0-only
@@ -29,6 +29,7 @@ Classifier: Topic :: Utilities
29
29
  Requires-Dist: dynaconf (>=3.2.0,<4.0.0)
30
30
  Requires-Dist: httpx (>=0.24.1,<0.25.0)
31
31
  Requires-Dist: httpx-auth (>=0.17.0,<0.18.0)
32
+ Requires-Dist: mistune (>=3.0.2,<4.0.0)
32
33
  Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
33
34
  Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
34
35
  Requires-Dist: tenacity (>=8.2.3,<9.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "elapi"
3
- version = "1.0.7"
3
+ version = "1.0.7-dev1"
4
4
  description = """elAPI is a powerful, extensible API interface to eLabFTW built for \
5
5
  the University Computing Centre (URZ, FIRE division) at Universität Heidelberg."""
6
6
  authors = [
@@ -40,6 +40,7 @@ typer = { extras = ["all"], version = "^0.9.0" }
40
40
  pyyaml = "^6.0.1"
41
41
  python-dateutil = "^2.8.2"
42
42
  tenacity = "^8.2.3"
43
+ mistune = "^3.0.2"
43
44
 
44
45
  [tool.poetry.group.dev.dependencies]
45
46
  ptpython = "^3.0.23"
@@ -0,0 +1 @@
1
+ 1.0.7-dev1
@@ -0,0 +1,552 @@
1
+ import re
2
+ from abc import ABC, abstractmethod
3
+ from typing import Union, Optional, Tuple
4
+
5
+ from httpx import Response, Client, AsyncClient, Limits
6
+ from httpx_auth import HeaderApiKey
7
+
8
+ from .configuration import API_TOKEN, TOKEN_BEARER, HOST
9
+
10
+
11
+ class APIRequest(ABC):
12
+ __slots__ = "keep_session_open", "_client"
13
+ host: str = HOST
14
+ api_token: str = API_TOKEN
15
+ header_name: str = TOKEN_BEARER
16
+
17
+ def __init__(self, keep_session_open: bool = False, **kwargs):
18
+ self.keep_session_open = keep_session_open
19
+ _client = Client if not self.is_async_client else AsyncClient
20
+ self._client: Union[Client, AsyncClient] = _client(
21
+ auth=HeaderApiKey(api_key=self.api_token, header_name=self.header_name),
22
+ verify=True,
23
+ **kwargs,
24
+ )
25
+
26
+ # noinspection PyMethodOverriding
27
+ def __init_subclass__(cls, /, is_async_client: bool = False, **kwargs):
28
+ super().__init_subclass__(**kwargs)
29
+ cls.is_async_client = is_async_client
30
+
31
+ @property
32
+ def client(self) -> Union[Client, AsyncClient]:
33
+ return self._client
34
+
35
+ @client.setter
36
+ def client(self, value):
37
+ raise AttributeError("Client cannot be modified!")
38
+
39
+ @abstractmethod
40
+ def _make(self, *args, **kwargs): ...
41
+
42
+ @abstractmethod
43
+ def close(self):
44
+ if not self.client.is_closed:
45
+ self.client.close()
46
+
47
+ @abstractmethod
48
+ def __call__(self, *args, **kwargs):
49
+ response = self._make(*args, **kwargs)
50
+ if not self.keep_session_open:
51
+ self.close()
52
+ return response
53
+
54
+
55
+ class ElabFTWURLError(Exception): ...
56
+
57
+
58
+ class ElabFTWURL:
59
+ VALID_ENDPOINTS: dict[str : Tuple[str]] = {
60
+ "apikeys": (),
61
+ "config": (),
62
+ "experiments": (
63
+ "uploads",
64
+ "revisions",
65
+ "comments",
66
+ "items_links",
67
+ "experiments_links",
68
+ "steps",
69
+ "tags",
70
+ ),
71
+ "info": (),
72
+ "items": (
73
+ "uploads",
74
+ "revisions",
75
+ "comments",
76
+ "items_links",
77
+ "experiments_links",
78
+ "steps",
79
+ "tags",
80
+ ),
81
+ "experiments_templates": (
82
+ "revisions",
83
+ "steps",
84
+ "tags",
85
+ ),
86
+ "items_types": (
87
+ "steps",
88
+ "tags",
89
+ ),
90
+ "events": (),
91
+ "team_tags": (),
92
+ "teams": (
93
+ "experiments_categories",
94
+ "experiments_status",
95
+ "items_status",
96
+ "teamgroups",
97
+ ),
98
+ "todolist": (),
99
+ "unfinished_steps": (),
100
+ "users": ("notifications",),
101
+ "idps": (),
102
+ }
103
+ host = APIRequest.host
104
+
105
+ def __init__(
106
+ self,
107
+ endpoint_name: str,
108
+ endpoint_id: Union[int, str, None] = None,
109
+ sub_endpoint_name: Optional[str] = None,
110
+ sub_endpoint_id: Union[int, str, None] = None,
111
+ query: Optional[dict] = None,
112
+ ) -> None:
113
+ self.endpoint_name = endpoint_name
114
+ self.endpoint_id = endpoint_id
115
+ self.sub_endpoint_name = sub_endpoint_name
116
+ self.sub_endpoint_id = sub_endpoint_id
117
+ self.query = query
118
+
119
+ @property
120
+ def endpoint_name(self) -> str:
121
+ return self._endpoint_name
122
+
123
+ @endpoint_name.setter
124
+ def endpoint_name(self, value: str):
125
+ if value is not None:
126
+ if value.lower() not in ElabFTWURL.VALID_ENDPOINTS.keys():
127
+ raise ElabFTWURLError(
128
+ f"Endpoint must be one of valid eLabFTW endpoints: {', '.join(ElabFTWURL.VALID_ENDPOINTS.keys())}."
129
+ )
130
+ self._endpoint_name = value
131
+ else:
132
+ self._endpoint_name = ""
133
+
134
+ @property
135
+ def sub_endpoint_name(self) -> str:
136
+ return self._sub_endpoint_name
137
+
138
+ @sub_endpoint_name.setter
139
+ def sub_endpoint_name(self, value: str):
140
+ if value is not None:
141
+ if value.lower() not in ElabFTWURL.VALID_ENDPOINTS[self.endpoint_name]:
142
+ raise ElabFTWURLError(
143
+ f"A Sub-endpoint for endpoint '{self._endpoint_name}' must be "
144
+ f"one of valid eLabFTW sub-endpoints: {', '.join(ElabFTWURL.VALID_ENDPOINTS[self.endpoint_name])}."
145
+ )
146
+ self._sub_endpoint_name = value
147
+ else:
148
+ self._sub_endpoint_name = ""
149
+
150
+ @property
151
+ def endpoint_id(self) -> Union[int, str, None]:
152
+ return self._endpoint_id
153
+
154
+ @endpoint_id.setter
155
+ def endpoint_id(self, value):
156
+ if value is not None:
157
+ if not re.match(r"^\w+$", value := str(value)):
158
+ # Although, eLabFTW primarily supports integer-only IDs, there are exceptions, like the alias
159
+ # ID "me" for receiving one's own user information.
160
+ raise ElabFTWURLError("Invalid endpoint ID (or entity ID).")
161
+ self._endpoint_id = value
162
+ else:
163
+ self._endpoint_id = ""
164
+
165
+ @property
166
+ def sub_endpoint_id(self) -> Union[int, str, None]:
167
+ return self._sub_endpoint_id
168
+
169
+ @sub_endpoint_id.setter
170
+ def sub_endpoint_id(self, value):
171
+ if self.sub_endpoint_name is None:
172
+ raise ElabFTWURLError(
173
+ "Sub-endpoint ID cannot be defined without first specifying its sub-endpoint name."
174
+ )
175
+ if value is not None:
176
+ if not re.match(r"^\w+$", value := str(value)):
177
+ raise ElabFTWURLError("Invalid sub-endpoint ID (or entity sub-ID).")
178
+ self._sub_endpoint_id = value
179
+ else:
180
+ self._sub_endpoint_id = ""
181
+ # Similar to an empty endpoint_id, an empty sub_endpoint_id sends back
182
+ # the whole list of available resources for a given sub-endpoint as response.
183
+
184
+ @property
185
+ def query(self) -> str:
186
+ return self._query
187
+
188
+ @query.setter
189
+ def query(self, value: dict):
190
+ self._query = "&".join([f"{k}={v}" for k, v in (value or dict()).items()])
191
+
192
+ def get(self) -> str:
193
+ url = (
194
+ f"{ElabFTWURL.host}/{self.endpoint_name}/{self.endpoint_id}/"
195
+ f"{self.sub_endpoint_name}/{self.sub_endpoint_id}"
196
+ ).rstrip("/")
197
+ if self.query:
198
+ url += f"?{self.query}"
199
+ return url
200
+
201
+
202
+ class GETRequest(APIRequest):
203
+ __slots__ = ()
204
+
205
+ def __init__(self, **kwargs):
206
+ super().__init__(**kwargs)
207
+
208
+ def _make(self, *args) -> Response:
209
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
210
+ url = ElabFTWURL(
211
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
212
+ )
213
+ return super().client.get(url.get(), headers={"Accept": "application/json"})
214
+
215
+ def close(self):
216
+ super().close()
217
+
218
+ def __call__(
219
+ self,
220
+ endpoint_name: str,
221
+ endpoint_id: Union[str, int, None] = None,
222
+ sub_endpoint_name: Optional[str] = None,
223
+ sub_endpoint_id: Union[int, str, None] = None,
224
+ query: Optional[dict] = None,
225
+ ) -> Response:
226
+ return super().__call__(
227
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
228
+ )
229
+
230
+
231
+ class POSTRequest(APIRequest):
232
+ __slots__ = ()
233
+
234
+ def __init__(self, **kwargs):
235
+ super().__init__(**kwargs)
236
+
237
+ def _make(self, *args, **kwargs) -> Response:
238
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
239
+ url = ElabFTWURL(
240
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
241
+ )
242
+ data = {
243
+ k: v.strip() if isinstance(v, str) else v
244
+ for k, v in (kwargs.pop("data", dict())).items()
245
+ }
246
+ files = kwargs.pop("files", None)
247
+ return super().client.post(
248
+ url.get(),
249
+ headers={
250
+ "Accept": "*/*",
251
+ # If json argument isn't empty, '"Content-Type": "application/json"' is automatically set.
252
+ # '"Content-Type": "multipart/form-data"', takes no effect, and
253
+ # the server will return a 400 bad request.
254
+ # See: https://blog.ian.stapletoncordas.co/2024/02/a-retrospective-on-requests
255
+ # '"Content-Type": "application/json"' doesn't (and shouldn't) work when "files" isn't empty.
256
+ },
257
+ json=data,
258
+ files=files,
259
+ **kwargs,
260
+ )
261
+
262
+ def close(self):
263
+ super().close()
264
+
265
+ def __call__(
266
+ self,
267
+ endpoint_name: str,
268
+ endpoint_id: Union[str, int, None] = None,
269
+ sub_endpoint_name: Optional[str] = None,
270
+ sub_endpoint_id: Union[int, str, None] = None,
271
+ query: Optional[dict] = None,
272
+ **kwargs,
273
+ ) -> Response:
274
+ return super().__call__(
275
+ endpoint_name,
276
+ endpoint_id,
277
+ sub_endpoint_name,
278
+ sub_endpoint_id,
279
+ query,
280
+ **kwargs,
281
+ )
282
+
283
+
284
+ class AsyncPOSTRequest(APIRequest, is_async_client=True):
285
+ __slots__ = ()
286
+
287
+ def __init__(self, **kwargs):
288
+ super().__init__(
289
+ timeout=180,
290
+ limits=Limits(
291
+ max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
292
+ ),
293
+ **kwargs,
294
+ )
295
+
296
+ async def _make(self, *args, **kwargs) -> Response:
297
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
298
+ url = ElabFTWURL(
299
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
300
+ )
301
+ data = {
302
+ k: v.strip() if isinstance(v, str) else v
303
+ for k, v in (kwargs.pop("data", dict())).items()
304
+ }
305
+ files = kwargs.pop("files", None)
306
+ return await super().client.post(
307
+ url.get(),
308
+ headers={"Accept": "*/*"},
309
+ json=data,
310
+ files=files,
311
+ **kwargs,
312
+ )
313
+
314
+ async def close(self):
315
+ if not self.client.is_closed:
316
+ await self.client.aclose()
317
+
318
+ async def __call__(
319
+ self,
320
+ endpoint_name: str,
321
+ endpoint_id: Union[str, int, None] = None,
322
+ sub_endpoint_name: Optional[str] = None,
323
+ sub_endpoint_id: Union[int, str, None] = None,
324
+ query: Optional[dict] = None,
325
+ **kwargs,
326
+ ) -> Response:
327
+ response = await self._make(
328
+ endpoint_name,
329
+ endpoint_id,
330
+ sub_endpoint_name,
331
+ sub_endpoint_id,
332
+ query,
333
+ **kwargs,
334
+ )
335
+ if not self.keep_session_open:
336
+ await self.close()
337
+ return response
338
+
339
+
340
+ class AsyncGETRequest(APIRequest, is_async_client=True):
341
+ __slots__ = ()
342
+
343
+ def __init__(self, **kwargs):
344
+ super().__init__(
345
+ timeout=180,
346
+ limits=Limits(
347
+ max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
348
+ ),
349
+ **kwargs,
350
+ )
351
+
352
+ async def _make(self, *args) -> Response:
353
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
354
+ url = ElabFTWURL(
355
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
356
+ )
357
+ return await super().client.get(
358
+ url.get(),
359
+ headers={"Accept": "application/json"},
360
+ )
361
+
362
+ async def close(self):
363
+ if not self.client.is_closed:
364
+ await self.client.aclose()
365
+
366
+ async def __call__(
367
+ self,
368
+ endpoint_name: str,
369
+ endpoint_id: Union[str, int, None] = None,
370
+ sub_endpoint_name: Optional[str] = None,
371
+ sub_endpoint_id: Union[int, str, None] = None,
372
+ query: Optional[dict] = None,
373
+ ) -> Response:
374
+ response = await self._make(
375
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
376
+ )
377
+ if not self.keep_session_open:
378
+ await self.close()
379
+ return response
380
+
381
+
382
+ class PATCHRequest(APIRequest):
383
+ __slots__ = ()
384
+
385
+ def __init__(self, **kwargs):
386
+ super().__init__(**kwargs)
387
+
388
+ def _make(self, *args, **kwargs) -> Response:
389
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
390
+ url = ElabFTWURL(
391
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
392
+ )
393
+ data = {
394
+ k: v.strip() if isinstance(v, str) else v
395
+ for k, v in kwargs.pop("data", dict()).items()
396
+ }
397
+ return super().client.patch(
398
+ url.get(),
399
+ headers={
400
+ "Accept": "application/json"
401
+ }, # '"Content-Type": "application/json"' is passed automatically when json argument is passed.
402
+ json=data,
403
+ **kwargs,
404
+ )
405
+
406
+ def close(self):
407
+ super().close()
408
+
409
+ def __call__(
410
+ self,
411
+ endpoint_name: str,
412
+ endpoint_id: Union[str, int, None] = None,
413
+ sub_endpoint_name: Optional[str] = None,
414
+ sub_endpoint_id: Union[int, str, None] = None,
415
+ query: Optional[dict] = None,
416
+ **kwargs,
417
+ ) -> Response:
418
+ return super().__call__(
419
+ endpoint_name,
420
+ endpoint_id,
421
+ sub_endpoint_name,
422
+ sub_endpoint_id,
423
+ query,
424
+ **kwargs,
425
+ )
426
+
427
+
428
+ class AsyncPATCHRequest(APIRequest, is_async_client=True):
429
+ __slots__ = ()
430
+
431
+ def __init__(self, **kwargs):
432
+ super().__init__(
433
+ timeout=180,
434
+ limits=Limits(
435
+ max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
436
+ ),
437
+ **kwargs,
438
+ )
439
+
440
+ async def _make(self, *args, **kwargs) -> Response:
441
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
442
+ url = ElabFTWURL(
443
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
444
+ )
445
+ data = {
446
+ k: v.strip() if isinstance(v, str) else v
447
+ for k, v in kwargs.pop("data", dict()).items()
448
+ }
449
+ return await super().client.patch(
450
+ url.get(),
451
+ headers={"Accept": "application/json"},
452
+ json=data,
453
+ **kwargs,
454
+ )
455
+
456
+ async def close(self):
457
+ if not self.client.is_closed:
458
+ await self.client.aclose()
459
+
460
+ async def __call__(
461
+ self,
462
+ endpoint_name: str,
463
+ endpoint_id: Union[str, int, None] = None,
464
+ sub_endpoint_name: Optional[str] = None,
465
+ sub_endpoint_id: Union[int, str, None] = None,
466
+ query: Optional[dict] = None,
467
+ **kwargs,
468
+ ) -> Response:
469
+ response = await self._make(
470
+ endpoint_name,
471
+ endpoint_id,
472
+ sub_endpoint_name,
473
+ sub_endpoint_id,
474
+ query,
475
+ **kwargs,
476
+ )
477
+ if not self.keep_session_open:
478
+ await self.close()
479
+ return response
480
+
481
+
482
+ class DELETERequest(APIRequest):
483
+ __slots__ = ()
484
+
485
+ def __init__(self, **kwargs):
486
+ super().__init__(**kwargs)
487
+
488
+ def _make(self, *args) -> Response:
489
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
490
+ url = ElabFTWURL(
491
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
492
+ )
493
+ return super().client.delete(
494
+ url.get(), headers={"Accept": "*/*", "Content-Type": "application/json"}
495
+ )
496
+
497
+ def close(self):
498
+ super().close()
499
+
500
+ def __call__(
501
+ self,
502
+ endpoint_name: str,
503
+ endpoint_id: Union[str, int, None] = None,
504
+ sub_endpoint_name: Optional[str] = None,
505
+ sub_endpoint_id: Union[int, str, None] = None,
506
+ query: Optional[dict] = None,
507
+ ) -> Response:
508
+ return super().__call__(
509
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
510
+ )
511
+
512
+
513
+ class AsyncDELETERequest(APIRequest, is_async_client=True):
514
+ __slots__ = ()
515
+
516
+ def __init__(self, **kwargs):
517
+ super().__init__(
518
+ timeout=180,
519
+ limits=Limits(
520
+ max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
521
+ ),
522
+ **kwargs,
523
+ )
524
+
525
+ async def _make(self, *args) -> Response:
526
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
527
+ url = ElabFTWURL(
528
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
529
+ )
530
+ return await super().client.delete(
531
+ url.get(),
532
+ headers={"Accept": "*/*", "Content-Type": "application/json"},
533
+ )
534
+
535
+ async def close(self):
536
+ if not self.client.is_closed:
537
+ await self.client.aclose()
538
+
539
+ async def __call__(
540
+ self,
541
+ endpoint_name: str,
542
+ endpoint_id: Union[str, int, None] = None,
543
+ sub_endpoint_name: Optional[str] = None,
544
+ sub_endpoint_id: Union[int, str, None] = None,
545
+ query: Optional[dict] = None,
546
+ ) -> Response:
547
+ response = await self._make(
548
+ endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
549
+ )
550
+ if not self.keep_session_open:
551
+ await self.close()
552
+ return response
@@ -0,0 +1 @@
1
+ from .helpers import CLIExport, CLIFormat, OrderedCommands # noqa: F401
@@ -0,0 +1,44 @@
1
+ import importlib.util
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import List, Tuple, Generator
5
+
6
+ import typer
7
+
8
+ from ..configuration.config import (
9
+ ROOT_INSTALLATION_DIR,
10
+ INTERNAL_PLUGIN_DIRECTORY_NAME,
11
+ INTERNAL_PLUGIN_TYPER_APP_FILE_NAME_PREFIX,
12
+ INTERNAL_PLUGIN_TYPER_APP_FILE_NAME,
13
+ INTERNAL_PLUGIN_TYPER_APP_VAR_NAME,
14
+ )
15
+
16
+
17
+ class InternalPluginHandler:
18
+ @property
19
+ def plugin_locations(self) -> List[Tuple[str, Path]]:
20
+ _paths = []
21
+ for path in (ROOT_INSTALLATION_DIR / INTERNAL_PLUGIN_DIRECTORY_NAME).iterdir():
22
+ if path.is_dir():
23
+ if (path / INTERNAL_PLUGIN_TYPER_APP_FILE_NAME).exists():
24
+ _paths.append((path.name, path))
25
+ return _paths
26
+
27
+ def get_typer_apps(self) -> Generator[typer.Typer, None, None]:
28
+ for plugin_name, path in self.plugin_locations:
29
+ spec = importlib.util.spec_from_file_location(
30
+ plugin_name,
31
+ path / INTERNAL_PLUGIN_TYPER_APP_FILE_NAME,
32
+ )
33
+ module = importlib.util.module_from_spec(spec)
34
+ sys.modules[spec.name] = module
35
+ module.__package__ = (
36
+ f"{__package__.removesuffix(f'.{INTERNAL_PLUGIN_TYPER_APP_FILE_NAME_PREFIX}')}"
37
+ f".{INTERNAL_PLUGIN_DIRECTORY_NAME}.{plugin_name}"
38
+ ) # Python will find module relative to __package__ path,
39
+ # without this module.__package__ change Python will throw an ImportError.
40
+ spec.loader.exec_module(module)
41
+ yield getattr(module, INTERNAL_PLUGIN_TYPER_APP_VAR_NAME)
42
+
43
+
44
+ internal_plugin_typer_apps = InternalPluginHandler().get_typer_apps()