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.
- {elapi-1.0.7 → elapi-1.0.7.dev1}/PKG-INFO +2 -1
- {elapi-1.0.7 → elapi-1.0.7.dev1}/pyproject.toml +2 -1
- elapi-1.0.7.dev1/src/elapi/VERSION +1 -0
- elapi-1.0.7.dev1/src/elapi/api.py +552 -0
- elapi-1.0.7.dev1/src/elapi/cli/__init__.py +1 -0
- elapi-1.0.7.dev1/src/elapi/cli/_plugin_handler.py +44 -0
- elapi-1.0.7.dev1/src/elapi/cli/doc.py +86 -0
- elapi-1.0.7.dev1/src/elapi/cli/elapi.py +711 -0
- elapi-1.0.7.dev1/src/elapi/cli/helpers.py +78 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/configuration/__init__.py +3 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/configuration/config.py +9 -9
- elapi-1.0.7.dev1/src/elapi/endpoint.py +220 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/path.py +4 -2
- elapi-1.0.7.dev1/src/elapi/plugins/bill_teams/__init__.py +2 -0
- elapi-1.0.7.dev1/src/elapi/plugins/bill_teams/_doc.py +3 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/plugins/bill_teams/bill_teams.py +27 -21
- elapi-1.0.7.dev1/src/elapi/plugins/bill_teams/cli.py +143 -0
- elapi-1.0.7.dev1/src/elapi/plugins/experiments/__init__.py +3 -0
- elapi-1.0.7.dev1/src/elapi/plugins/experiments/_doc.py +24 -0
- elapi-1.0.7.dev1/src/elapi/plugins/experiments/cli.py +309 -0
- elapi-1.0.7.dev1/src/elapi/plugins/experiments/experiments.py +171 -0
- elapi-1.0.7.dev1/src/elapi/plugins/experiments/formats.py +77 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/plugins/show_config.py +5 -5
- elapi-1.0.7.dev1/src/elapi/plugins/utils/__init__.py +2 -0
- {elapi-1.0.7/src/elapi/plugins → elapi-1.0.7.dev1/src/elapi/plugins/utils}/export.py +17 -7
- elapi-1.0.7.dev1/src/elapi/plugins/utils/get_location_from_headers.py +27 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/format.py +4 -8
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/validators/identity.py +15 -19
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/validators/permission.py +2 -2
- elapi-1.0.7/src/elapi/VERSION +0 -1
- elapi-1.0.7/src/elapi/api.py +0 -200
- elapi-1.0.7/src/elapi/cli/_doc.py +0 -49
- elapi-1.0.7/src/elapi/cli/elapi.py +0 -481
- elapi-1.0.7/src/elapi/endpoint.py +0 -77
- elapi-1.0.7/src/elapi/plugins/__init__.py +0 -0
- elapi-1.0.7/src/elapi/plugins/bill_teams/__init__.py +0 -3
- {elapi-1.0.7 → elapi-1.0.7.dev1}/LICENSE +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/README.md +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/__init__.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/_names.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/cli/__main__.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/configuration/_config_history.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/configuration/log_file.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/loggers/__init__.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/loggers/base.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/loggers/handlers/__init__.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/loggers/handlers/file.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/loggers/handlers/stderr.py +0 -0
- {elapi-1.0.7/src/elapi/cli → elapi-1.0.7.dev1/src/elapi/plugins}/__init__.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/plugins/bill_teams/invoice.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/plugins/version.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/__init__.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/_markdown_doc.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/_missing.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/base.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/colors.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/styles/highlight.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/validators/__init__.py +0 -0
- {elapi-1.0.7 → elapi-1.0.7.dev1}/src/elapi/validators/base.py +0 -0
- {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()
|