elapi 2.4.0__tar.gz → 2.4.2__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-2.4.0 → elapi-2.4.2}/PKG-INFO +42 -6
- {elapi-2.4.0 → elapi-2.4.2}/README.md +39 -4
- {elapi-2.4.0 → elapi-2.4.2}/pyproject.toml +3 -2
- elapi-2.4.2/src/elapi/VERSION +1 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_names.py +1 -1
- elapi-2.4.2/src/elapi/api/__init__.py +36 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/api/api.py +153 -66
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/api/endpoint.py +1 -1
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/api/validators.py +92 -91
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/cli/_plugin_handler.py +20 -18
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/cli/elapi.py +83 -23
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/configuration/__init__.py +74 -5
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/configuration/_overload_history.py +3 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/configuration/config.py +86 -21
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/configuration/overridable_vars.py +8 -18
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/configuration/validators.py +56 -70
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/path.py +26 -117
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/bill_teams.py +38 -8
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/cli.py +81 -12
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/commons/__init__.py +2 -0
- elapi-2.4.2/src/elapi/plugins/commons/cli.py +6 -0
- elapi-2.4.2/src/elapi/plugins/commons/get_whoami.py +126 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/show_config.py +24 -6
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/styles/highlight.py +6 -2
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/utils/__init__.py +6 -2
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/utils/utils.py +18 -2
- elapi-2.4.0/src/elapi/VERSION +0 -1
- elapi-2.4.0/src/elapi/api/__init__.py +0 -18
- {elapi-2.4.0 → elapi-2.4.2}/LICENSE +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/.DS_Store +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_core_init/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_core_init/_loggers/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_core_init/_loggers/base.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_core_init/_loggers/handlers/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_core_init/_loggers/handlers/base.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_core_init/_loggers/handlers/callback.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_core_init/_loggers/handlers/stderr.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_core_init/_utils.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_vendor/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_vendor/haggis/LICENSE +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_vendor/haggis/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_vendor/haggis/logs.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/_vendor/haggis/version.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/cli/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/cli/__main__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/cli/_venv_state_manager.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/cli/doc.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/configuration/_config_history.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/core_validators/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/core_validators/base.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/core_validators/path.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/loggers/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/loggers/base.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/loggers/handlers/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/loggers/handlers/file.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/loggers/log_file.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/.DS_Store +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/_doc.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/configuration.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/formats.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/generate_table.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/logger.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/names.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/registry.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/specification.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/utils.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/bill_teams/validators.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/commons/cli_helpers.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/commons/export.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/commons/get_data_from_input_or_path.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/commons/get_information.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/commons/get_location_from_headers.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/experiments/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/experiments/_doc.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/experiments/cli.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/experiments/experiments.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/experiments/formats.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/mail/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/mail/_yagmail_patch.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/mail/cli.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/mail/configuration.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/mail/mail.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/mail/names.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/plugins/mail/yagmail_configuration.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/styles/__init__.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/styles/_markdown_doc.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/styles/_missing.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/styles/base.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/styles/colors.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/styles/formats.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/styles/rich_utils.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/utils/messages.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/utils/typer_patches.py +0 -0
- {elapi-2.4.0 → elapi-2.4.2}/src/elapi/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: elapi
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: elAPI is a powerful, extensible API client for eLabFTW.
|
|
5
5
|
Keywords: elabftw,api
|
|
6
6
|
Author: Alexander Haller, Mahadi Xion
|
|
@@ -28,7 +28,8 @@ Requires-Dist: typer[all]>=0.9.0,<0.10
|
|
|
28
28
|
Requires-Dist: pyyaml==6.0.2
|
|
29
29
|
Requires-Dist: mistune==3.1.3
|
|
30
30
|
Requires-Dist: click>=8.1.8,<8.2
|
|
31
|
-
Requires-Dist: httpx-limiter>=0.
|
|
31
|
+
Requires-Dist: httpx-limiter>=0.3.0,<0.4.0
|
|
32
|
+
Requires-Dist: httpx-aiohttp>=0.1.8,<0.2
|
|
32
33
|
Requires-Dist: jinja2>=3.1.6,<3.2 ; extra == 'mail'
|
|
33
34
|
Requires-Dist: nameparser>=1.1.3 ; extra == 'mail'
|
|
34
35
|
Requires-Dist: yagmail>=0.15.293,<0.16 ; extra == 'mail'
|
|
@@ -133,7 +134,9 @@ $ elapi init
|
|
|
133
134
|
<video src='https://github.com/user-attachments/assets/8d5f69ed-b644-4d75-b816-d06d4e937105'> </video>
|
|
134
135
|
<p align="center">elapi init demo</p>
|
|
135
136
|
|
|
136
|
-
That's all! Run [`elapi
|
|
137
|
+
That's all! Run [`elapi whoami`](#elapi-whoami) to see a short summary of your eLab user. Run [
|
|
138
|
+
`elapi show-config`](#show-config) to view
|
|
139
|
+
your configuration details.
|
|
137
140
|
|
|
138
141
|
## Compatibility
|
|
139
142
|
|
|
@@ -176,7 +179,8 @@ unsafe_api_token_warning: true
|
|
|
176
179
|
enable_http2: false
|
|
177
180
|
verify_ssl: true
|
|
178
181
|
timeout: 90
|
|
179
|
-
async_rate_limit:
|
|
182
|
+
async_rate_limit: 20
|
|
183
|
+
async_capacity: 500
|
|
180
184
|
development_mode: false
|
|
181
185
|
```
|
|
182
186
|
|
|
@@ -192,9 +196,13 @@ development_mode: false
|
|
|
192
196
|
- `timeout` can be modified to your needs. E.g., a poor internet connection might benefit from a higher timeout number.
|
|
193
197
|
The default timeout is `90` seconds.
|
|
194
198
|
- `async_rate_limit` controls the maximum number of requests per second (i.e., throughput) you want to make to the
|
|
195
|
-
server.
|
|
199
|
+
server. The default `async_rate_limit` is `null` which means no limit. An eLab server might be configured to limit
|
|
196
200
|
a high number of requests to prevent spam, `async_rate_limit` can then be set to the maximum number of requests
|
|
197
201
|
allowed by the server.
|
|
202
|
+
- `async_capacity` controls the maximum number of in-flight async requests. The default `async_capacity` is `null` which
|
|
203
|
+
means no limit on capacity. Both `async_rate_limit` and `async_capacity` can be used to better limit the traffic load
|
|
204
|
+
put on the server. See this [SA answer](https://stackoverflow.com/a/52100884/7696241) about how they differ from each
|
|
205
|
+
other.
|
|
198
206
|
- `development_mode` can be set to `True` to show debug logs, Python traceback on the CLI instead of a clean exit, etc.
|
|
199
207
|
This mode should not be turned on for production-ready scripts.
|
|
200
208
|
|
|
@@ -218,6 +226,33 @@ as detected by `elapi show-config`. All plugins will also automatically listen t
|
|
|
218
226
|
be useful to set certain configurations temporarily. E.g., `elapi --OC '{"timeout": 300"}' get info` will override
|
|
219
227
|
the `timeout` from the configuration files.
|
|
220
228
|
|
|
229
|
+
## elAPI `whoami`
|
|
230
|
+
|
|
231
|
+
Oftentimes, you work with multiple API tokens/keys for multiple teams on multiple eLab servers. elAPI enables robust
|
|
232
|
+
workflow management with built-in [advanced configuration](#advanced-configuration) orchestration. During development
|
|
233
|
+
though,
|
|
234
|
+
juggling between multiple API keys can still lead to confusion as to which API key is being actually used. elAPI version
|
|
235
|
+
`2.4.1` adds the `whomai` command that shows the essential information about the requesting eLab user.
|
|
236
|
+
|
|
237
|
+
```shell
|
|
238
|
+
$ elapi whoami
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+

|
|
242
|
+
|
|
243
|
+
So, if your API task requires write permission, but only `Read-only` is shown next to the API key, you know you're using
|
|
244
|
+
the wrong API key. But having to run `elapi whoami` every single time for this would be cumbersome. So, we added the
|
|
245
|
+
`whoami` information to the debug messages. To enable debug messages, set `development_mode` to `True` in
|
|
246
|
+
the [configuration file](#configuration). Once set, before any elAPI command and plugin is run, elAPI will show the
|
|
247
|
+
`whoami` information in the debug messages.
|
|
248
|
+
|
|
249
|
+
```shell
|
|
250
|
+
DEBUG Based on the detected configuration, the requests will be made to the server get_whoami.py:114
|
|
251
|
+
https://elabftw-dev-002.uni-heidelberg.de (eLabFTW version: 5.2.8), with API token '84-fc******2a384' (Read/Write),
|
|
252
|
+
by user 'John Doe' (ID: 2607), from team 'North Kathy' (ID: 19), in user group 'User', not as a sysadmin. Scopes:
|
|
253
|
+
experiments = 'Team', items = 'Team', experiments_templates = 'Team', teamgroups = 'Everything'
|
|
254
|
+
```
|
|
255
|
+
|
|
221
256
|
## Examples and usage
|
|
222
257
|
|
|
223
258
|
Check out the [examples directory](https://github.com/uhd-urz/elAPI/tree/main/examples) for some comprehensive code
|
|
@@ -318,7 +353,8 @@ $ elapi experiments upload-attachment --id <experiment ID> --path <path to attac
|
|
|
318
353
|
|
|
319
354
|
Sometimes when a script has finished, you and your team would want to receive an email about its success/failure. elAPI
|
|
320
355
|
offers a no-code solution for this: the `mail` plugin. The `mail` plugin is not enabled by default. To enable it,
|
|
321
|
-
install elAPI with the optional dependency `mail`: `pip install elapi[mail]` or `uv add elapi[mail]`. [__See the
|
|
356
|
+
install elAPI with the optional dependency `mail`: `pip install elapi[mail]` or `uv add elapi[mail]`. [__See the wiki
|
|
357
|
+
__](https://github.com/uhd-urz/elAPI/wiki/mail-plugin) to read more about how to configure the `mail` plugin with your
|
|
322
358
|
mail server configuration and trigger conditions. In a
|
|
323
359
|
nutshell, the `mail` plugin will scan the logs when a script/plugin is done to look for pre-configured trigger
|
|
324
360
|
conditions, and if found, it will send an email. The trigger conditions can be atomic and scope-based, i.e., an email
|
|
@@ -85,7 +85,9 @@ $ elapi init
|
|
|
85
85
|
<video src='https://github.com/user-attachments/assets/8d5f69ed-b644-4d75-b816-d06d4e937105'> </video>
|
|
86
86
|
<p align="center">elapi init demo</p>
|
|
87
87
|
|
|
88
|
-
That's all! Run [`elapi
|
|
88
|
+
That's all! Run [`elapi whoami`](#elapi-whoami) to see a short summary of your eLab user. Run [
|
|
89
|
+
`elapi show-config`](#show-config) to view
|
|
90
|
+
your configuration details.
|
|
89
91
|
|
|
90
92
|
## Compatibility
|
|
91
93
|
|
|
@@ -128,7 +130,8 @@ unsafe_api_token_warning: true
|
|
|
128
130
|
enable_http2: false
|
|
129
131
|
verify_ssl: true
|
|
130
132
|
timeout: 90
|
|
131
|
-
async_rate_limit:
|
|
133
|
+
async_rate_limit: 20
|
|
134
|
+
async_capacity: 500
|
|
132
135
|
development_mode: false
|
|
133
136
|
```
|
|
134
137
|
|
|
@@ -144,9 +147,13 @@ development_mode: false
|
|
|
144
147
|
- `timeout` can be modified to your needs. E.g., a poor internet connection might benefit from a higher timeout number.
|
|
145
148
|
The default timeout is `90` seconds.
|
|
146
149
|
- `async_rate_limit` controls the maximum number of requests per second (i.e., throughput) you want to make to the
|
|
147
|
-
server.
|
|
150
|
+
server. The default `async_rate_limit` is `null` which means no limit. An eLab server might be configured to limit
|
|
148
151
|
a high number of requests to prevent spam, `async_rate_limit` can then be set to the maximum number of requests
|
|
149
152
|
allowed by the server.
|
|
153
|
+
- `async_capacity` controls the maximum number of in-flight async requests. The default `async_capacity` is `null` which
|
|
154
|
+
means no limit on capacity. Both `async_rate_limit` and `async_capacity` can be used to better limit the traffic load
|
|
155
|
+
put on the server. See this [SA answer](https://stackoverflow.com/a/52100884/7696241) about how they differ from each
|
|
156
|
+
other.
|
|
150
157
|
- `development_mode` can be set to `True` to show debug logs, Python traceback on the CLI instead of a clean exit, etc.
|
|
151
158
|
This mode should not be turned on for production-ready scripts.
|
|
152
159
|
|
|
@@ -170,6 +177,33 @@ as detected by `elapi show-config`. All plugins will also automatically listen t
|
|
|
170
177
|
be useful to set certain configurations temporarily. E.g., `elapi --OC '{"timeout": 300"}' get info` will override
|
|
171
178
|
the `timeout` from the configuration files.
|
|
172
179
|
|
|
180
|
+
## elAPI `whoami`
|
|
181
|
+
|
|
182
|
+
Oftentimes, you work with multiple API tokens/keys for multiple teams on multiple eLab servers. elAPI enables robust
|
|
183
|
+
workflow management with built-in [advanced configuration](#advanced-configuration) orchestration. During development
|
|
184
|
+
though,
|
|
185
|
+
juggling between multiple API keys can still lead to confusion as to which API key is being actually used. elAPI version
|
|
186
|
+
`2.4.1` adds the `whomai` command that shows the essential information about the requesting eLab user.
|
|
187
|
+
|
|
188
|
+
```shell
|
|
189
|
+
$ elapi whoami
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+

|
|
193
|
+
|
|
194
|
+
So, if your API task requires write permission, but only `Read-only` is shown next to the API key, you know you're using
|
|
195
|
+
the wrong API key. But having to run `elapi whoami` every single time for this would be cumbersome. So, we added the
|
|
196
|
+
`whoami` information to the debug messages. To enable debug messages, set `development_mode` to `True` in
|
|
197
|
+
the [configuration file](#configuration). Once set, before any elAPI command and plugin is run, elAPI will show the
|
|
198
|
+
`whoami` information in the debug messages.
|
|
199
|
+
|
|
200
|
+
```shell
|
|
201
|
+
DEBUG Based on the detected configuration, the requests will be made to the server get_whoami.py:114
|
|
202
|
+
https://elabftw-dev-002.uni-heidelberg.de (eLabFTW version: 5.2.8), with API token '84-fc******2a384' (Read/Write),
|
|
203
|
+
by user 'John Doe' (ID: 2607), from team 'North Kathy' (ID: 19), in user group 'User', not as a sysadmin. Scopes:
|
|
204
|
+
experiments = 'Team', items = 'Team', experiments_templates = 'Team', teamgroups = 'Everything'
|
|
205
|
+
```
|
|
206
|
+
|
|
173
207
|
## Examples and usage
|
|
174
208
|
|
|
175
209
|
Check out the [examples directory](https://github.com/uhd-urz/elAPI/tree/main/examples) for some comprehensive code
|
|
@@ -270,7 +304,8 @@ $ elapi experiments upload-attachment --id <experiment ID> --path <path to attac
|
|
|
270
304
|
|
|
271
305
|
Sometimes when a script has finished, you and your team would want to receive an email about its success/failure. elAPI
|
|
272
306
|
offers a no-code solution for this: the `mail` plugin. The `mail` plugin is not enabled by default. To enable it,
|
|
273
|
-
install elAPI with the optional dependency `mail`: `pip install elapi[mail]` or `uv add elapi[mail]`. [__See the
|
|
307
|
+
install elAPI with the optional dependency `mail`: `pip install elapi[mail]` or `uv add elapi[mail]`. [__See the wiki
|
|
308
|
+
__](https://github.com/uhd-urz/elAPI/wiki/mail-plugin) to read more about how to configure the `mail` plugin with your
|
|
274
309
|
mail server configuration and trigger conditions. In a
|
|
275
310
|
nutshell, the `mail` plugin will scan the logs when a script/plugin is done to look for pre-configured trigger
|
|
276
311
|
conditions, and if found, it will send an email. The trigger conditions can be atomic and scope-based, i.e., an email
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "elapi"
|
|
3
|
-
version = "2.4.
|
|
3
|
+
version = "2.4.2"
|
|
4
4
|
description = "elAPI is a powerful, extensible API client for eLabFTW."
|
|
5
5
|
authors = [{ name = "Alexander Haller, Mahadi Xion", email = "elabftw@uni-heidelberg.de" }]
|
|
6
6
|
requires-python = ">=3.11.0,<4"
|
|
@@ -33,7 +33,8 @@ dependencies = [
|
|
|
33
33
|
"pyyaml==6.0.2",
|
|
34
34
|
"mistune==3.1.3",
|
|
35
35
|
"click>=8.1.8,<8.2",
|
|
36
|
-
"httpx-limiter>=0.
|
|
36
|
+
"httpx-limiter>=0.3.0,<0.4.0",
|
|
37
|
+
"httpx-aiohttp>=0.1.8,<0.2",
|
|
37
38
|
]
|
|
38
39
|
license-files = [
|
|
39
40
|
"LICENSE",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.4.2
|
|
@@ -27,7 +27,6 @@ ELAB_HOST_URL_API_SUFFIX: str = f"/api/v{ELAB_API_EXPECTED_VERSION}"
|
|
|
27
27
|
ENV_XDG_DATA_HOME: str = "XDG_DATA_HOME"
|
|
28
28
|
ENV_XDG_DOWNLOAD_DIR: str = "XDG_DOWNLOAD_DIR"
|
|
29
29
|
ENV_XDG_CONFIG_HOME: str = "XDG_CONFIG_HOME"
|
|
30
|
-
TMP_DIR: Path = Path(f"/var/tmp/{APP_NAME}")
|
|
31
30
|
|
|
32
31
|
# Fallback definitions
|
|
33
32
|
FALLBACK_DIR: Path = user_home / ".local/share" / APP_NAME
|
|
@@ -48,6 +47,7 @@ KEY_API_TOKEN: str = "API_TOKEN"
|
|
|
48
47
|
KEY_EXPORT_DIR: str = "EXPORT_DIR"
|
|
49
48
|
KEY_UNSAFE_TOKEN_WARNING: str = "UNSAFE_API_TOKEN_WARNING"
|
|
50
49
|
KEY_ENABLE_HTTP2: str = "ENABLE_HTTP2"
|
|
50
|
+
KEY_ASYNC_CAPACITY: str = "ASYNC_CAPACITY"
|
|
51
51
|
KEY_VERIFY_SSL: str = "VERIFY_SSL"
|
|
52
52
|
KEY_TIMEOUT: str = "TIMEOUT"
|
|
53
53
|
KEY_ASYNC_RATE_LIMIT: str = "ASYNC_RATE_LIMIT"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
"SessionDefaults",
|
|
3
|
+
"SimpleClient",
|
|
4
|
+
"GlobalSharedSession",
|
|
5
|
+
"APIRequest",
|
|
6
|
+
"ElabFTWURLError",
|
|
7
|
+
"ElabFTWURL",
|
|
8
|
+
"GETRequest",
|
|
9
|
+
"AsyncGETRequest",
|
|
10
|
+
"POSTRequest",
|
|
11
|
+
"AsyncPOSTRequest",
|
|
12
|
+
"PATCHRequest",
|
|
13
|
+
"AsyncPATCHRequest",
|
|
14
|
+
"DELETERequest",
|
|
15
|
+
"AsyncDELETERequest",
|
|
16
|
+
"FixedEndpoint",
|
|
17
|
+
"FixedAsyncEndpoint",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
from .api import (
|
|
21
|
+
APIRequest,
|
|
22
|
+
AsyncDELETERequest,
|
|
23
|
+
AsyncGETRequest,
|
|
24
|
+
AsyncPATCHRequest,
|
|
25
|
+
AsyncPOSTRequest,
|
|
26
|
+
DELETERequest,
|
|
27
|
+
ElabFTWURL,
|
|
28
|
+
ElabFTWURLError,
|
|
29
|
+
GETRequest,
|
|
30
|
+
GlobalSharedSession,
|
|
31
|
+
PATCHRequest,
|
|
32
|
+
POSTRequest,
|
|
33
|
+
SessionDefaults,
|
|
34
|
+
SimpleClient,
|
|
35
|
+
)
|
|
36
|
+
from .endpoint import FixedAsyncEndpoint, FixedEndpoint
|
|
@@ -8,29 +8,50 @@ from typing import Literal, Optional, Tuple, Union
|
|
|
8
8
|
|
|
9
9
|
# noinspection PyProtectedMember
|
|
10
10
|
import httpx._client as httpx_private_client_module
|
|
11
|
-
from httpx import
|
|
11
|
+
from httpx import (
|
|
12
|
+
AsyncBaseTransport,
|
|
13
|
+
AsyncClient,
|
|
14
|
+
BaseTransport,
|
|
15
|
+
Client,
|
|
16
|
+
Limits,
|
|
17
|
+
Response,
|
|
18
|
+
)
|
|
12
19
|
|
|
13
20
|
# noinspection PyProtectedMember
|
|
14
21
|
from httpx._client import BaseClient
|
|
15
22
|
|
|
16
23
|
# noinspection PyProtectedMember
|
|
17
24
|
from httpx._types import AuthTypes
|
|
25
|
+
from httpx_aiohttp import AiohttpTransport
|
|
18
26
|
from httpx_auth import HeaderApiKey
|
|
19
27
|
from httpx_limiter import AsyncRateLimitedTransport, Rate
|
|
20
28
|
|
|
21
|
-
from .. import APP_BRAND_NAME
|
|
29
|
+
from .. import APP_BRAND_NAME
|
|
22
30
|
from ..configuration import (
|
|
31
|
+
KEY_API_TOKEN,
|
|
32
|
+
KEY_ASYNC_RATE_LIMIT,
|
|
33
|
+
KEY_ENABLE_HTTP2,
|
|
34
|
+
KEY_HOST,
|
|
35
|
+
KEY_TIMEOUT,
|
|
36
|
+
KEY_VERIFY_SSL,
|
|
23
37
|
TOKEN_BEARER,
|
|
38
|
+
MinimalActiveConfiguration,
|
|
24
39
|
get_active_api_token,
|
|
40
|
+
get_active_async_capacity,
|
|
25
41
|
get_active_async_rate_limit,
|
|
26
42
|
get_active_enable_http2,
|
|
27
43
|
get_active_host,
|
|
28
44
|
get_active_timeout,
|
|
29
45
|
get_active_verify_ssl,
|
|
46
|
+
preventive_missing_warning,
|
|
30
47
|
)
|
|
48
|
+
from ..configuration.config import APIToken
|
|
31
49
|
from ..loggers import Logger
|
|
32
50
|
from ..styles import Missing
|
|
33
|
-
from ..utils import
|
|
51
|
+
from ..utils import (
|
|
52
|
+
get_app_version,
|
|
53
|
+
update_kwargs_with_defaults,
|
|
54
|
+
)
|
|
34
55
|
|
|
35
56
|
USER_AGENT: str = (
|
|
36
57
|
f"{APP_BRAND_NAME}/{get_app_version()} {httpx_private_client_module.USER_AGENT}"
|
|
@@ -55,90 +76,126 @@ class SessionDefaults:
|
|
|
55
76
|
session_defaults = SessionDefaults()
|
|
56
77
|
|
|
57
78
|
|
|
58
|
-
class _CustomHeaderApiKey(HeaderApiKey):
|
|
79
|
+
class _CustomHeaderApiKey(HeaderApiKey):
|
|
80
|
+
def __init__(self):
|
|
81
|
+
self.api_key = str(Missing())
|
|
82
|
+
self.header_name = TOKEN_BEARER
|
|
83
|
+
super().__init__(self.api_key, self.header_name)
|
|
59
84
|
|
|
60
85
|
|
|
61
86
|
class SimpleClient(BaseClient):
|
|
62
|
-
def __new__(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
def __new__(cls, *, is_async_client: bool, **kwargs) -> Union[Client, AsyncClient]:
|
|
88
|
+
host: str = kwargs.pop("host", get_active_host())
|
|
89
|
+
api_token: str | APIToken = kwargs.pop("api_token", get_active_api_token())
|
|
90
|
+
if not isinstance(api_token, APIToken):
|
|
91
|
+
api_token = APIToken(api_token)
|
|
92
|
+
auth: Optional[AuthTypes] = kwargs.pop("auth", _CustomHeaderApiKey())
|
|
93
|
+
if auth is None:
|
|
94
|
+
auth: AuthTypes = _CustomHeaderApiKey()
|
|
95
|
+
auth.api_key = api_token.token
|
|
96
|
+
logger.debug(
|
|
97
|
+
f"Argument 'auth' passed to {SimpleClient.__name__} is {None}. "
|
|
98
|
+
f"It has been be set to {HeaderApiKey} with "
|
|
99
|
+
f"API key/token '{api_token}'."
|
|
100
|
+
)
|
|
101
|
+
elif isinstance(auth, HeaderApiKey):
|
|
102
|
+
auth.api_key = api_token.token
|
|
103
|
+
else:
|
|
104
|
+
logger.debug(
|
|
105
|
+
f"'auth' has been set to {auth!r} for {SimpleClient.__name__} "
|
|
106
|
+
f"by an external method."
|
|
107
|
+
f"API token from configuration file '{api_token}' will be ignored."
|
|
108
|
+
)
|
|
109
|
+
enable_http2: bool = kwargs.pop("http2", get_active_enable_http2())
|
|
110
|
+
verify_ssl: bool = kwargs.pop("verify", get_active_verify_ssl())
|
|
111
|
+
timeout: float = kwargs.pop("timeout", get_active_timeout())
|
|
112
|
+
async_rate_limit: Optional[int] = kwargs.pop(
|
|
113
|
+
"async_rate_limit", get_active_async_rate_limit()
|
|
114
|
+
)
|
|
115
|
+
async_capacity: Optional[int] = kwargs.pop(
|
|
116
|
+
"async_capacity", get_active_async_capacity()
|
|
117
|
+
)
|
|
118
|
+
async_transport: Optional[AsyncBaseTransport] = kwargs.pop(
|
|
119
|
+
"async_transport", None
|
|
120
|
+
)
|
|
121
|
+
sync_transport: Optional[BaseTransport] = kwargs.pop("sync_transport", None)
|
|
122
|
+
|
|
123
|
+
for key_name, value in (
|
|
94
124
|
(KEY_HOST, host),
|
|
95
125
|
(KEY_API_TOKEN, api_token),
|
|
96
126
|
(KEY_ENABLE_HTTP2, enable_http2),
|
|
97
127
|
(KEY_VERIFY_SSL, verify_ssl),
|
|
98
128
|
(KEY_TIMEOUT, timeout),
|
|
99
129
|
):
|
|
100
|
-
preventive_missing_warning(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
preventive_missing_warning((key_name, value))
|
|
131
|
+
if MinimalActiveConfiguration().get_value(key_name) != value:
|
|
132
|
+
logger.debug(
|
|
133
|
+
f"Configuration value for '{key_name}' "
|
|
134
|
+
f"is set to '{value}' for {SimpleClient.__name__} by an "
|
|
135
|
+
f"external method."
|
|
136
|
+
)
|
|
137
|
+
if is_async_client is True:
|
|
138
|
+
if async_transport is None and async_rate_limit is not None:
|
|
139
|
+
# noinspection PyTypeChecker
|
|
140
|
+
async_transport = AsyncRateLimitedTransport.create(
|
|
141
|
+
rate=Rate.create(magnitude=async_rate_limit, duration=1),
|
|
142
|
+
# see more: https://midnighter.github.io/httpx-limiter/latest/tutorial/#single-rate-limit
|
|
143
|
+
# In our case, magnitude=1, duration=1/async_rate_limit yields worse results.
|
|
144
|
+
http2=enable_http2, # type: ignore
|
|
145
|
+
# See: https://github.com/Midnighter/httpx-limiter/issues/7#issuecomment-3197487921
|
|
146
|
+
# http2 needs to be passed here if AsyncRateLimitedTransport is used
|
|
147
|
+
verify=verify_ssl, # type: ignore
|
|
148
|
+
)
|
|
149
|
+
logger.debug(
|
|
150
|
+
f"Async rate limit is set to {async_transport.__class__.__name__} "
|
|
151
|
+
f"instance {async_transport!r}: {async_rate_limit}"
|
|
152
|
+
)
|
|
153
|
+
elif async_transport is not None:
|
|
154
|
+
logger.debug(
|
|
155
|
+
f"Async transport has been set to {async_transport!r} by an "
|
|
156
|
+
f"external method."
|
|
157
|
+
)
|
|
158
|
+
if async_rate_limit is not None:
|
|
114
159
|
logger.warning(
|
|
115
160
|
f"When a manual transport parameter is passed to {SimpleClient}, "
|
|
116
161
|
f"the '{KEY_ASYNC_RATE_LIMIT}' in the configuration file is ignored. "
|
|
117
162
|
)
|
|
118
|
-
|
|
119
|
-
|
|
163
|
+
elif async_transport is None:
|
|
164
|
+
async_transport = AiohttpTransport(
|
|
120
165
|
http2=enable_http2,
|
|
121
166
|
verify=verify_ssl,
|
|
122
|
-
timeout=timeout,
|
|
123
|
-
|
|
124
|
-
|
|
167
|
+
timeout=timeout, # type: ignore
|
|
168
|
+
)
|
|
169
|
+
logger.debug(
|
|
170
|
+
f"Async transport has been to {async_transport!r} by {APP_BRAND_NAME}."
|
|
125
171
|
)
|
|
126
|
-
|
|
172
|
+
async_client = AsyncClient(
|
|
127
173
|
auth=auth,
|
|
128
174
|
http2=enable_http2,
|
|
129
175
|
verify=verify_ssl,
|
|
130
176
|
timeout=timeout,
|
|
177
|
+
transport=async_transport,
|
|
131
178
|
**kwargs,
|
|
132
179
|
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
180
|
+
if async_capacity is not None:
|
|
181
|
+
async_client._async_semaphore_ = asyncio.Semaphore(async_capacity)
|
|
182
|
+
logger.debug(
|
|
183
|
+
f"Async semaphore is set to {async_client.__class__.__name__} "
|
|
184
|
+
f"instance {async_client!r}: {async_capacity}"
|
|
185
|
+
)
|
|
186
|
+
return async_client
|
|
187
|
+
if sync_transport is not None:
|
|
188
|
+
logger.debug(
|
|
189
|
+
f"Sync transport has been set to {sync_transport!r} by an external method."
|
|
140
190
|
)
|
|
141
|
-
|
|
191
|
+
return Client(
|
|
192
|
+
auth=auth,
|
|
193
|
+
http2=enable_http2,
|
|
194
|
+
verify=verify_ssl,
|
|
195
|
+
timeout=timeout,
|
|
196
|
+
transport=sync_transport,
|
|
197
|
+
**kwargs,
|
|
198
|
+
)
|
|
142
199
|
|
|
143
200
|
|
|
144
201
|
class GlobalSharedSession:
|
|
@@ -172,13 +229,23 @@ class GlobalSharedSession:
|
|
|
172
229
|
@cached_property
|
|
173
230
|
def sync_client(self) -> Optional[Client]:
|
|
174
231
|
if self.limited_to in ("sync", "all"):
|
|
175
|
-
|
|
232
|
+
client = SimpleClient(is_async_client=False, **self._kwargs)
|
|
233
|
+
logger.debug(
|
|
234
|
+
f"{GlobalSharedSession.__name__} instance {self!r} "
|
|
235
|
+
f"injected sync client {client!r}."
|
|
236
|
+
)
|
|
237
|
+
return client
|
|
176
238
|
return None
|
|
177
239
|
|
|
178
240
|
@cached_property
|
|
179
241
|
def async_client(self) -> Optional[AsyncClient]:
|
|
180
242
|
if self.limited_to in ("async", "all"):
|
|
181
|
-
|
|
243
|
+
client = SimpleClient(is_async_client=True, **self._kwargs)
|
|
244
|
+
logger.debug(
|
|
245
|
+
f"{GlobalSharedSession.__name__} instance {self!r} "
|
|
246
|
+
f"injected async client {client!r}."
|
|
247
|
+
)
|
|
248
|
+
return client
|
|
182
249
|
return None
|
|
183
250
|
|
|
184
251
|
def close(self) -> None:
|
|
@@ -186,6 +253,9 @@ class GlobalSharedSession:
|
|
|
186
253
|
if self.sync_client is not None:
|
|
187
254
|
if self.sync_client.is_closed is False:
|
|
188
255
|
self.sync_client.close()
|
|
256
|
+
logger.debug(
|
|
257
|
+
f"{self.__class__.__name__} has closed sync client {self.sync_client!r}."
|
|
258
|
+
)
|
|
189
259
|
if self.async_client is not None:
|
|
190
260
|
if self.async_client.is_closed is False:
|
|
191
261
|
# nest_asyncio is needed if there are multiple asyncio.run.
|
|
@@ -209,8 +279,15 @@ class GlobalSharedSession:
|
|
|
209
279
|
) from e
|
|
210
280
|
else:
|
|
211
281
|
event_loop.close()
|
|
282
|
+
logger.debug(
|
|
283
|
+
f"{self.__class__.__name__} has closed async client {self.async_client!r}."
|
|
284
|
+
)
|
|
212
285
|
else:
|
|
213
286
|
event_loop.create_task(self.async_client.aclose())
|
|
287
|
+
logger.debug(
|
|
288
|
+
f"{self.__class__.__name__} has added a task for async client "
|
|
289
|
+
f"{self.async_client!r} to be closed."
|
|
290
|
+
)
|
|
214
291
|
|
|
215
292
|
def __enter__(self):
|
|
216
293
|
self._outer_instance = GlobalSharedSession._instance
|
|
@@ -339,6 +416,7 @@ class APIRequest(ABC):
|
|
|
339
416
|
return NotImplemented
|
|
340
417
|
if not self.client.is_closed:
|
|
341
418
|
self.client.close()
|
|
419
|
+
logger.debug(f"Sync client {self.client!r} is closed.")
|
|
342
420
|
return None
|
|
343
421
|
|
|
344
422
|
@abstractmethod
|
|
@@ -347,6 +425,7 @@ class APIRequest(ABC):
|
|
|
347
425
|
return NotImplemented
|
|
348
426
|
if not self.client.is_closed:
|
|
349
427
|
await self.client.aclose()
|
|
428
|
+
logger.debug(f"Async client {self.client!r} is closed.")
|
|
350
429
|
return None
|
|
351
430
|
|
|
352
431
|
@abstractmethod
|
|
@@ -721,6 +800,14 @@ class AsyncGETRequest(APIRequest, is_async_client=True):
|
|
|
721
800
|
url = ElabFTWURL(
|
|
722
801
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
|
|
723
802
|
)
|
|
803
|
+
client = super().client
|
|
804
|
+
if async_semaphore := getattr(client, "_async_semaphore_", None):
|
|
805
|
+
async with async_semaphore:
|
|
806
|
+
return await client.get(
|
|
807
|
+
url.get(),
|
|
808
|
+
headers=headers or {"Accept": "application/json"},
|
|
809
|
+
**kwargs,
|
|
810
|
+
)
|
|
724
811
|
return await super().client.get(
|
|
725
812
|
url.get(), headers=headers or {"Accept": "application/json"}, **kwargs
|
|
726
813
|
)
|
|
@@ -112,7 +112,7 @@ class FixedAsyncEndpoint:
|
|
|
112
112
|
)
|
|
113
113
|
|
|
114
114
|
async def aclose(self) -> Optional[NotImplementedType]:
|
|
115
|
-
if self._is_global_shared_instance_none is
|
|
115
|
+
if self._is_global_shared_instance_none is True:
|
|
116
116
|
await self._client.aclose()
|
|
117
117
|
return NotImplemented
|
|
118
118
|
|