elapi 2.4.3__tar.gz → 2.4.5__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.3 → elapi-2.4.5}/PKG-INFO +33 -7
- {elapi-2.4.3 → elapi-2.4.5}/README.md +31 -6
- {elapi-2.4.3 → elapi-2.4.5}/pyproject.toml +3 -1
- elapi-2.4.5/src/elapi/VERSION +1 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_core_init/__init__.py +3 -0
- elapi-2.4.5/src/elapi/_core_init/_cache.py +53 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_core_init/_loggers/handlers/callback.py +5 -1
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_names.py +33 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/api/__init__.py +8 -0
- elapi-2.4.5/src/elapi/api/_handle_unexp_response.py +58 -0
- elapi-2.4.5/src/elapi/api/_names.py +55 -0
- elapi-2.4.5/src/elapi/api/_openapi_parser.py +100 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.1.15.json +51 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.2.0.json +52 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.2.1.json +53 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.2.2.json +53 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.2.3.json +53 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.2.4.json +53 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.2.5.json +53 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.2.6.json +53 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.2.7.json +53 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.2.8.json +53 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.3.0.json +53 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.3.1.json +53 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.3.2.json +53 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.3.3.json +56 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.3.4.json +56 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.3.5.json +56 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.3.6.json +56 -0
- elapi-2.4.5/src/elapi/api/_supported_versions/5.3.7.json +56 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/api/api.py +160 -80
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/api/validators.py +8 -27
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/cli/__main__.py +1 -1
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/cli/_plugin_handler.py +148 -147
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/cli/doc.py +31 -5
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/cli/elapi.py +43 -14
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/configuration/__init__.py +6 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/configuration/_overload_history.py +2 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/configuration/config.py +11 -2
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/configuration/overridable_vars.py +7 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/configuration/validators.py +59 -2
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/_doc.py +1 -2
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/bill_teams.py +33 -20
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/cli.py +20 -5
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/commons/__init__.py +3 -3
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/commons/cli_helpers.py +1 -13
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/commons/get_data_from_input_or_path.py +2 -2
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/commons/get_information.py +32 -2
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/commons/get_whoami.py +19 -4
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/experiments/_doc.py +1 -1
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/mail/configuration.py +2 -2
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/mail/mail.py +2 -2
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/show_config.py +27 -2
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/styles/formats.py +1 -1
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/utils/__init__.py +8 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/utils/utils.py +30 -1
- elapi-2.4.3/src/elapi/VERSION +0 -1
- {elapi-2.4.3 → elapi-2.4.5}/LICENSE +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/.DS_Store +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_core_init/_loggers/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_core_init/_loggers/base.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_core_init/_loggers/handlers/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_core_init/_loggers/handlers/base.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_core_init/_loggers/handlers/stderr.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_core_init/_utils.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_vendor/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_vendor/haggis/LICENSE +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_vendor/haggis/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_vendor/haggis/logs.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/_vendor/haggis/version.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/api/endpoint.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/cli/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/cli/_venv_state_manager.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/configuration/_config_history.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/core_validators/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/core_validators/base.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/core_validators/path.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/loggers/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/loggers/base.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/loggers/handlers/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/loggers/handlers/file.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/loggers/log_file.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/path.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/.DS_Store +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/configuration.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/formats.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/generate_table.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/logger.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/names.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/registry.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/specification.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/utils.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/bill_teams/validators.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/commons/cli.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/commons/export.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/commons/get_location_from_headers.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/experiments/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/experiments/cli.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/experiments/experiments.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/experiments/formats.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/mail/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/mail/_yagmail_patch.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/mail/cli.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/mail/names.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/plugins/mail/yagmail_configuration.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/styles/__init__.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/styles/_markdown_doc.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/styles/_missing.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/styles/base.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/styles/colors.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/styles/highlight.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/styles/rich_utils.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/utils/messages.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/src/elapi/utils/typer_patches.py +0 -0
- {elapi-2.4.3 → elapi-2.4.5}/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.5
|
|
4
4
|
Summary: elAPI is a powerful, extensible API client for eLabFTW.
|
|
5
5
|
Keywords: elabftw,api
|
|
6
6
|
Author: Alexander Haller, Mahadi Xion
|
|
@@ -30,6 +30,7 @@ Requires-Dist: mistune==3.1.3
|
|
|
30
30
|
Requires-Dist: click>=8.1.8,<8.2
|
|
31
31
|
Requires-Dist: httpx-limiter>=0.3.0,<0.4.0
|
|
32
32
|
Requires-Dist: httpx-aiohttp>=0.1.8,<0.2
|
|
33
|
+
Requires-Dist: pydantic>=2.12.3
|
|
33
34
|
Requires-Dist: jinja2>=3.1.6,<3.2 ; extra == 'mail'
|
|
34
35
|
Requires-Dist: nameparser>=1.1.3 ; extra == 'mail'
|
|
35
36
|
Requires-Dist: yagmail>=0.15.293,<0.16 ; extra == 'mail'
|
|
@@ -56,8 +57,8 @@ Description-Content-Type: text/markdown
|
|
|
56
57
|
<img alt="Package version" src="https://badge.fury.io/py/elapi.svg/?branch=main" />
|
|
57
58
|
</a>
|
|
58
59
|
<a href="#compatibility">
|
|
59
|
-
<img alt="Static Badge" src="https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-%230d7dbe">
|
|
60
|
-
<img alt="Static Badge" src="https://img.shields.io/badge/eLabFTW%20support-
|
|
60
|
+
<img alt="Static Badge" src="https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-%230d7dbe">
|
|
61
|
+
<img alt="Static Badge" src="https://img.shields.io/badge/eLabFTW%20support-5.1%20%7C%205.2%20%7C%205.3+-%2323b3be">
|
|
61
62
|
</a>
|
|
62
63
|
</p>
|
|
63
64
|
|
|
@@ -140,9 +141,19 @@ your configuration details.
|
|
|
140
141
|
|
|
141
142
|
## Compatibility
|
|
142
143
|
|
|
143
|
-
elAPI is compatible with the following Python versions: `3.11`, `3.12`. elAPI supports
|
|
144
|
+
elAPI is compatible with the following Python versions: `3.11`, `3.12`, `3.13`, `3.14`. elAPI supports
|
|
144
145
|
the [eLabFTW REST API v2](https://doc.elabftw.net/api/v2/), and can be used with the following eLabFTW
|
|
145
|
-
versions: `
|
|
146
|
+
versions: `5.3.6`, `5.3.5`, `5.3.4`, `5.3.3`, `5.3.2`, `5.3.1`, `5.3.0`, `5.2.8`, `5.2.7`, `5.2.6`, `5.2.5`, `5.2.4`,
|
|
147
|
+
`5.2.3`, `5.2.2`, `5.2.1`, `5.2.0`, `5.1.1`.
|
|
148
|
+
|
|
149
|
+
### elAPI strict version support
|
|
150
|
+
|
|
151
|
+
Starting from elAPI version **2.4.5**, elAPI maintains a [strict set of supported eLabFTW versions](#compatibility).
|
|
152
|
+
elAPI will throw an error if the detected eLabFTW version is not supported. This is to prevent unexpected behavior when
|
|
153
|
+
eLabFTW introduces breaking changes to the REST API. If your eLab version is supported, elAPI will cache it, and show
|
|
154
|
+
the valid endpoint names for your server in `--help` page (e.g., `elapi get --help`). You can clear this cache with
|
|
155
|
+
`elapi clear-cache`. You can also control what elAPI should do when an unsupported eLab version is used with
|
|
156
|
+
`elab_strict_version_match` configuration field. See [advanced configuration](#advanced-configuration) for more details.
|
|
146
157
|
|
|
147
158
|
## Configuration
|
|
148
159
|
|
|
@@ -175,6 +186,7 @@ host: <host API url>
|
|
|
175
186
|
api_token: <token with at least read-access>
|
|
176
187
|
# "A.k.a API key". You can generate an API token from eLabFTW user panel -> API keys tab.
|
|
177
188
|
export_dir: ~/Downloads/elAPI
|
|
189
|
+
elab_strict_version_match: "warn"
|
|
178
190
|
unsafe_api_token_warning: true
|
|
179
191
|
enable_http2: false
|
|
180
192
|
verify_ssl: true
|
|
@@ -188,6 +200,15 @@ development_mode: false
|
|
|
188
200
|
- When `unsafe_api_token_warning` is `True`, elAPI will show a warning if you're storing `elapi.yml` in the current
|
|
189
201
|
working directory, as it typically happens that developers accidentally commit and push configuration files with
|
|
190
202
|
secrets.
|
|
203
|
+
- `elab_strict_version_match` controls what elAPI should do if an unsupported eLabFTW version is detected. eLabFTW does
|
|
204
|
+
introduce breaking API change from time to time. Starting from elAPI 2.4.5, elAPI maintains [a strict set of eLabFTW
|
|
205
|
+
versions](#compatibility), i.e., elAPI and its plugins are expected to work for these versions. This is to prevent
|
|
206
|
+
unexpected behavior due to the breaking changes. If the detected eLabFTW server version is not supported, elAPI by
|
|
207
|
+
default (`elab_strict_version_match: warn`) will throw a warning
|
|
208
|
+
message and continue to run any command/plugin. When `elab_strict_version_match` is set to `abort`, elAPI will show an
|
|
209
|
+
`ERROR`
|
|
210
|
+
message and abort immediately. When `elab_strict_version_match` is set to `yolo`, elAPI will ignore any version
|
|
211
|
+
support conflict.
|
|
191
212
|
- `enable_http2` enables HTTP/2 protocol support which by default is turned off. Be aware
|
|
192
213
|
of [known issues](https://github.com/encode/httpx/discussions/2112) with HTTP/2 if you are making async requests with
|
|
193
214
|
a heavy load.
|
|
@@ -285,6 +306,11 @@ of the team with team ID 1.
|
|
|
285
306
|
$ elapi get -H teams --id 1
|
|
286
307
|
```
|
|
287
308
|
|
|
309
|
+
elAPI runs a weak endpoint name validation against your eLabFTW API version. Run `elapi get --help` to see the
|
|
310
|
+
supported endpoint names for your eLabFTW server:
|
|
311
|
+
|
|
312
|
+

|
|
313
|
+
|
|
288
314
|
### `POST` requests
|
|
289
315
|
|
|
290
316
|
Create a new user by the name 'John Doe':
|
|
@@ -353,8 +379,8 @@ $ elapi experiments upload-attachment --id <experiment ID> --path <path to attac
|
|
|
353
379
|
|
|
354
380
|
Sometimes when a script has finished, you and your team would want to receive an email about its success/failure. elAPI
|
|
355
381
|
offers a no-code solution for this: the `mail` plugin. The `mail` plugin is not enabled by default. To enable it,
|
|
356
|
-
install elAPI with the optional dependency `mail`: `pip install elapi[mail]` or `uv add elapi[mail]`. [__See the wiki
|
|
357
|
-
|
|
382
|
+
install elAPI with the optional dependency `mail`: `pip install elapi[mail]` or `uv add elapi[mail]`. [__See the wiki__](https://github.com/uhd-urz/elAPI/wiki/mail-plugin)
|
|
383
|
+
to read more about how to configure the `mail` plugin with your
|
|
358
384
|
mail server configuration and trigger conditions. In a
|
|
359
385
|
nutshell, the `mail` plugin will scan the logs when a script/plugin is done to look for pre-configured trigger
|
|
360
386
|
conditions, and if found, it will send an email. The trigger conditions can be atomic and scope-based, i.e., an email
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
<img alt="Package version" src="https://badge.fury.io/py/elapi.svg/?branch=main" />
|
|
8
8
|
</a>
|
|
9
9
|
<a href="#compatibility">
|
|
10
|
-
<img alt="Static Badge" src="https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13-%230d7dbe">
|
|
11
|
-
<img alt="Static Badge" src="https://img.shields.io/badge/eLabFTW%20support-
|
|
10
|
+
<img alt="Static Badge" src="https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-%230d7dbe">
|
|
11
|
+
<img alt="Static Badge" src="https://img.shields.io/badge/eLabFTW%20support-5.1%20%7C%205.2%20%7C%205.3+-%2323b3be">
|
|
12
12
|
</a>
|
|
13
13
|
</p>
|
|
14
14
|
|
|
@@ -91,9 +91,19 @@ your configuration details.
|
|
|
91
91
|
|
|
92
92
|
## Compatibility
|
|
93
93
|
|
|
94
|
-
elAPI is compatible with the following Python versions: `3.11`, `3.12`. elAPI supports
|
|
94
|
+
elAPI is compatible with the following Python versions: `3.11`, `3.12`, `3.13`, `3.14`. elAPI supports
|
|
95
95
|
the [eLabFTW REST API v2](https://doc.elabftw.net/api/v2/), and can be used with the following eLabFTW
|
|
96
|
-
versions: `
|
|
96
|
+
versions: `5.3.6`, `5.3.5`, `5.3.4`, `5.3.3`, `5.3.2`, `5.3.1`, `5.3.0`, `5.2.8`, `5.2.7`, `5.2.6`, `5.2.5`, `5.2.4`,
|
|
97
|
+
`5.2.3`, `5.2.2`, `5.2.1`, `5.2.0`, `5.1.1`.
|
|
98
|
+
|
|
99
|
+
### elAPI strict version support
|
|
100
|
+
|
|
101
|
+
Starting from elAPI version **2.4.5**, elAPI maintains a [strict set of supported eLabFTW versions](#compatibility).
|
|
102
|
+
elAPI will throw an error if the detected eLabFTW version is not supported. This is to prevent unexpected behavior when
|
|
103
|
+
eLabFTW introduces breaking changes to the REST API. If your eLab version is supported, elAPI will cache it, and show
|
|
104
|
+
the valid endpoint names for your server in `--help` page (e.g., `elapi get --help`). You can clear this cache with
|
|
105
|
+
`elapi clear-cache`. You can also control what elAPI should do when an unsupported eLab version is used with
|
|
106
|
+
`elab_strict_version_match` configuration field. See [advanced configuration](#advanced-configuration) for more details.
|
|
97
107
|
|
|
98
108
|
## Configuration
|
|
99
109
|
|
|
@@ -126,6 +136,7 @@ host: <host API url>
|
|
|
126
136
|
api_token: <token with at least read-access>
|
|
127
137
|
# "A.k.a API key". You can generate an API token from eLabFTW user panel -> API keys tab.
|
|
128
138
|
export_dir: ~/Downloads/elAPI
|
|
139
|
+
elab_strict_version_match: "warn"
|
|
129
140
|
unsafe_api_token_warning: true
|
|
130
141
|
enable_http2: false
|
|
131
142
|
verify_ssl: true
|
|
@@ -139,6 +150,15 @@ development_mode: false
|
|
|
139
150
|
- When `unsafe_api_token_warning` is `True`, elAPI will show a warning if you're storing `elapi.yml` in the current
|
|
140
151
|
working directory, as it typically happens that developers accidentally commit and push configuration files with
|
|
141
152
|
secrets.
|
|
153
|
+
- `elab_strict_version_match` controls what elAPI should do if an unsupported eLabFTW version is detected. eLabFTW does
|
|
154
|
+
introduce breaking API change from time to time. Starting from elAPI 2.4.5, elAPI maintains [a strict set of eLabFTW
|
|
155
|
+
versions](#compatibility), i.e., elAPI and its plugins are expected to work for these versions. This is to prevent
|
|
156
|
+
unexpected behavior due to the breaking changes. If the detected eLabFTW server version is not supported, elAPI by
|
|
157
|
+
default (`elab_strict_version_match: warn`) will throw a warning
|
|
158
|
+
message and continue to run any command/plugin. When `elab_strict_version_match` is set to `abort`, elAPI will show an
|
|
159
|
+
`ERROR`
|
|
160
|
+
message and abort immediately. When `elab_strict_version_match` is set to `yolo`, elAPI will ignore any version
|
|
161
|
+
support conflict.
|
|
142
162
|
- `enable_http2` enables HTTP/2 protocol support which by default is turned off. Be aware
|
|
143
163
|
of [known issues](https://github.com/encode/httpx/discussions/2112) with HTTP/2 if you are making async requests with
|
|
144
164
|
a heavy load.
|
|
@@ -236,6 +256,11 @@ of the team with team ID 1.
|
|
|
236
256
|
$ elapi get -H teams --id 1
|
|
237
257
|
```
|
|
238
258
|
|
|
259
|
+
elAPI runs a weak endpoint name validation against your eLabFTW API version. Run `elapi get --help` to see the
|
|
260
|
+
supported endpoint names for your eLabFTW server:
|
|
261
|
+
|
|
262
|
+

|
|
263
|
+
|
|
239
264
|
### `POST` requests
|
|
240
265
|
|
|
241
266
|
Create a new user by the name 'John Doe':
|
|
@@ -304,8 +329,8 @@ $ elapi experiments upload-attachment --id <experiment ID> --path <path to attac
|
|
|
304
329
|
|
|
305
330
|
Sometimes when a script has finished, you and your team would want to receive an email about its success/failure. elAPI
|
|
306
331
|
offers a no-code solution for this: the `mail` plugin. The `mail` plugin is not enabled by default. To enable it,
|
|
307
|
-
install elAPI with the optional dependency `mail`: `pip install elapi[mail]` or `uv add elapi[mail]`. [__See the wiki
|
|
308
|
-
|
|
332
|
+
install elAPI with the optional dependency `mail`: `pip install elapi[mail]` or `uv add elapi[mail]`. [__See the wiki__](https://github.com/uhd-urz/elAPI/wiki/mail-plugin)
|
|
333
|
+
to read more about how to configure the `mail` plugin with your
|
|
309
334
|
mail server configuration and trigger conditions. In a
|
|
310
335
|
nutshell, the `mail` plugin will scan the logs when a script/plugin is done to look for pre-configured trigger
|
|
311
336
|
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.5"
|
|
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"
|
|
@@ -35,6 +35,7 @@ dependencies = [
|
|
|
35
35
|
"click>=8.1.8,<8.2",
|
|
36
36
|
"httpx-limiter>=0.3.0,<0.4.0",
|
|
37
37
|
"httpx-aiohttp>=0.1.8,<0.2",
|
|
38
|
+
"pydantic>=2.12.3",
|
|
38
39
|
]
|
|
39
40
|
license-files = [
|
|
40
41
|
"LICENSE",
|
|
@@ -68,6 +69,7 @@ build-backend = "uv_build"
|
|
|
68
69
|
|
|
69
70
|
[dependency-groups]
|
|
70
71
|
dev = [
|
|
72
|
+
"mypy>=1.18.2",
|
|
71
73
|
"pip>=25.1.1", # vendy requires pip
|
|
72
74
|
"ptpython>=3.0.23,<4",
|
|
73
75
|
"vendy>=0.0.3",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.4.5
|
|
@@ -16,11 +16,14 @@ __all__ = [
|
|
|
16
16
|
"GlobalCLIGracefulCallback",
|
|
17
17
|
"LogItemList",
|
|
18
18
|
"PatternNotFoundError",
|
|
19
|
+
"get_cached_data",
|
|
20
|
+
"update_cache",
|
|
19
21
|
]
|
|
20
22
|
import logging
|
|
21
23
|
|
|
22
24
|
from .._vendor import haggis
|
|
23
25
|
from .._vendor.haggis.logs import add_logging_level
|
|
26
|
+
from ._cache import get_cached_data, update_cache
|
|
24
27
|
from ._loggers import (
|
|
25
28
|
BaseHandler,
|
|
26
29
|
DefaultLogLevels,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from json import JSONDecodeError
|
|
4
|
+
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
from .._names import CacheFileProperties, CacheModel, CACHE_PATH
|
|
8
|
+
from ._loggers import Logger
|
|
9
|
+
|
|
10
|
+
logger = Logger()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_cached_data() -> CacheModel:
|
|
14
|
+
def _new_cache() -> CacheModel:
|
|
15
|
+
cache_ = CacheModel(date=datetime.now())
|
|
16
|
+
update_cache(cache_)
|
|
17
|
+
return cache_
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
raw_cache = json.loads(
|
|
21
|
+
CACHE_PATH.read_text(encoding=CacheFileProperties.encoding)
|
|
22
|
+
)
|
|
23
|
+
except (FileNotFoundError, JSONDecodeError, UnicodeDecodeError):
|
|
24
|
+
raw_cache = {}
|
|
25
|
+
try:
|
|
26
|
+
cache = CacheModel(**raw_cache)
|
|
27
|
+
except ValidationError:
|
|
28
|
+
logger.debug(
|
|
29
|
+
f"Cache found in '{CACHE_PATH}' is either empty or invalid. "
|
|
30
|
+
f"New cache will be created."
|
|
31
|
+
)
|
|
32
|
+
return _new_cache()
|
|
33
|
+
else:
|
|
34
|
+
if (datetime.now() - cache.date).seconds > CacheFileProperties.expires_in_days:
|
|
35
|
+
logger.debug(
|
|
36
|
+
f"Cache found in '{CACHE_PATH}' is older than "
|
|
37
|
+
f"{CacheFileProperties.expires_in_days} days. "
|
|
38
|
+
f"New cache will be created."
|
|
39
|
+
)
|
|
40
|
+
return _new_cache()
|
|
41
|
+
else:
|
|
42
|
+
return cache
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def update_cache(cache: CacheModel) -> None:
|
|
46
|
+
cache.date = datetime.now()
|
|
47
|
+
if not CACHE_PATH.exists():
|
|
48
|
+
CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
CACHE_PATH.touch(exist_ok=True)
|
|
50
|
+
CACHE_PATH.write_text(
|
|
51
|
+
cache.model_dump_json(indent=CacheFileProperties.indent),
|
|
52
|
+
encoding=CacheFileProperties.encoding,
|
|
53
|
+
)
|
|
@@ -16,6 +16,10 @@ class ResultCallbackHandler(Handler):
|
|
|
16
16
|
cls._store_okay = True
|
|
17
17
|
cls._client_count += 1
|
|
18
18
|
|
|
19
|
+
@classmethod
|
|
20
|
+
def disable_store_okay(cls) -> None:
|
|
21
|
+
cls._store_okay = False
|
|
22
|
+
|
|
19
23
|
@classmethod
|
|
20
24
|
def is_store_okay(cls) -> bool:
|
|
21
25
|
return cls._store_okay
|
|
@@ -25,6 +29,6 @@ class ResultCallbackHandler(Handler):
|
|
|
25
29
|
return cls._client_count
|
|
26
30
|
|
|
27
31
|
def emit(self, record):
|
|
28
|
-
if
|
|
32
|
+
if ResultCallbackHandler._store_okay:
|
|
29
33
|
if record.levelno >= self.level:
|
|
30
34
|
self.log_container.append(record)
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import StrEnum
|
|
2
5
|
from pathlib import Path
|
|
6
|
+
from typing import ClassVar
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
3
9
|
|
|
4
10
|
# variables with leading underscores here indicate that they are to be overwritten by config.py
|
|
5
11
|
# In which case, import their counterparts from src/config.py
|
|
@@ -53,6 +59,33 @@ KEY_TIMEOUT: str = "TIMEOUT"
|
|
|
53
59
|
KEY_ASYNC_RATE_LIMIT: str = "ASYNC_RATE_LIMIT"
|
|
54
60
|
KEY_DEVELOPMENT_MODE: str = "DEVELOPMENT_MODE"
|
|
55
61
|
KEY_PLUGIN_KEY_NAME: str = "PLUGINS"
|
|
62
|
+
KEY_ELAB_STRICT_VERSION_MATCH: str = "ELAB_STRICT_VERSION_MATCH"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ElabStrictVersionMatchModes(StrEnum):
|
|
66
|
+
abort = "abort"
|
|
67
|
+
warn = "warn"
|
|
68
|
+
yolo = "yolo"
|
|
69
|
+
|
|
56
70
|
|
|
57
71
|
# Log data directory with root permission
|
|
58
72
|
LOG_DIR_ROOT: Path = Path(f"/var/log/{APP_NAME}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _ElabHostsCacheModel(BaseModel):
|
|
76
|
+
url: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CacheModel(BaseModel):
|
|
80
|
+
date: datetime
|
|
81
|
+
elab_hosts: dict[str, str] = {}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class CacheFileProperties:
|
|
86
|
+
expires_in_days: ClassVar[int] = 6 * 60 * 60
|
|
87
|
+
encoding: ClassVar[str] = "utf-8"
|
|
88
|
+
indent: ClassVar[int] = 4
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
CACHE_PATH: Path = Path("~/.cache").expanduser() / APP_NAME / f"{APP_NAME}.json"
|
|
@@ -15,8 +15,15 @@ __all__ = [
|
|
|
15
15
|
"AsyncDELETERequest",
|
|
16
16
|
"FixedEndpoint",
|
|
17
17
|
"FixedAsyncEndpoint",
|
|
18
|
+
"handle_new_user_teams",
|
|
19
|
+
"ElabUserGroups",
|
|
20
|
+
"ElabScopes",
|
|
21
|
+
"ElabVersionDefaults",
|
|
22
|
+
"ElabFTWUnsupportedVersion"
|
|
18
23
|
]
|
|
19
24
|
|
|
25
|
+
from ._handle_unexp_response import handle_new_user_teams
|
|
26
|
+
from ._names import ElabScopes, ElabUserGroups, ElabVersionDefaults
|
|
20
27
|
from .api import (
|
|
21
28
|
APIRequest,
|
|
22
29
|
AsyncDELETERequest,
|
|
@@ -24,6 +31,7 @@ from .api import (
|
|
|
24
31
|
AsyncPATCHRequest,
|
|
25
32
|
AsyncPOSTRequest,
|
|
26
33
|
DELETERequest,
|
|
34
|
+
ElabFTWUnsupportedVersion,
|
|
27
35
|
ElabFTWURL,
|
|
28
36
|
ElabFTWURLError,
|
|
29
37
|
GETRequest,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from ..loggers import Logger
|
|
4
|
+
from ..utils import UnexpectedAPIResponseType
|
|
5
|
+
from ._names import ElabUserGroups
|
|
6
|
+
|
|
7
|
+
logger = Logger()
|
|
8
|
+
NewUserTeamsType = list[dict[str, int | bool | str]]
|
|
9
|
+
|
|
10
|
+
_DEBUG_LOG_EMIT_ONCE: bool = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def handle_new_user_teams(user_teams: str | NewUserTeamsType) -> NewUserTeamsType:
|
|
14
|
+
global _DEBUG_LOG_EMIT_ONCE
|
|
15
|
+
|
|
16
|
+
struct_user_teams: list[dict[str, int | bool | str]] = []
|
|
17
|
+
match user_teams:
|
|
18
|
+
case list():
|
|
19
|
+
struct_user_teams = user_teams
|
|
20
|
+
case str():
|
|
21
|
+
try:
|
|
22
|
+
struct_user_teams = json.loads(user_teams)
|
|
23
|
+
except json.JSONDecodeError as e:
|
|
24
|
+
raise UnexpectedAPIResponseType(
|
|
25
|
+
"Given user_teams is a string, but it could not be "
|
|
26
|
+
f"parsed as a valid JSON. Exception: {e}"
|
|
27
|
+
) from e
|
|
28
|
+
case _:
|
|
29
|
+
raise UnexpectedAPIResponseType(
|
|
30
|
+
f"Given user_teams type '{type(user_teams)}' is not supported."
|
|
31
|
+
)
|
|
32
|
+
for user_team in struct_user_teams:
|
|
33
|
+
try:
|
|
34
|
+
user_group = user_team["usergroup"]
|
|
35
|
+
except KeyError:
|
|
36
|
+
try:
|
|
37
|
+
user_group = ElabUserGroups.get_group_from_admin_status(
|
|
38
|
+
bool(user_team["is_admin"])
|
|
39
|
+
)
|
|
40
|
+
except KeyError as e:
|
|
41
|
+
raise UnexpectedAPIResponseType(
|
|
42
|
+
"user_team doesn't have 'usergroup', so a "
|
|
43
|
+
"'is_admin' field was expected. "
|
|
44
|
+
f"But no 'is_admin' is found either! User team: {user_team}"
|
|
45
|
+
) from e
|
|
46
|
+
else:
|
|
47
|
+
if _DEBUG_LOG_EMIT_ONCE is False:
|
|
48
|
+
logger.debug(
|
|
49
|
+
"Field 'usergroup' is not found in user team info. "
|
|
50
|
+
"This likely means the eLabFTW version is using a "
|
|
51
|
+
"newer API version. 'is_admin' field is found and has "
|
|
52
|
+
"been used instead."
|
|
53
|
+
)
|
|
54
|
+
_DEBUG_LOG_EMIT_ONCE = True
|
|
55
|
+
user_team["usergroup"] = user_group
|
|
56
|
+
else:
|
|
57
|
+
pass
|
|
58
|
+
return struct_user_teams
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ElabUserGroups(IntEnum):
|
|
6
|
+
"""eLabFTW's default permission groups.
|
|
7
|
+
See: https://github.com/elabftw/elabftw/blob/master/src/Enums/Usergroup.php
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
sysadmin = 1
|
|
11
|
+
admin = 2
|
|
12
|
+
user = 4
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def group_names(cls) -> list[str]:
|
|
16
|
+
return [_.name for _ in cls]
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def group_values(cls) -> list[int]:
|
|
20
|
+
return [_.value for _ in cls]
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def get_group_from_admin_status(cls, status: bool) -> int:
|
|
24
|
+
return cls.admin.value if status else cls.user.value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ElabScopes(IntEnum):
|
|
28
|
+
self = 1
|
|
29
|
+
team = 2
|
|
30
|
+
everything = 3
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ElabVersionDefaults:
|
|
34
|
+
supported_versions: tuple[str, ...] = (
|
|
35
|
+
"5.3.7",
|
|
36
|
+
"5.3.6",
|
|
37
|
+
"5.3.5",
|
|
38
|
+
"5.3.4",
|
|
39
|
+
"5.3.3",
|
|
40
|
+
"5.3.2",
|
|
41
|
+
"5.3.1",
|
|
42
|
+
"5.3.0",
|
|
43
|
+
"5.2.8",
|
|
44
|
+
"5.2.7",
|
|
45
|
+
"5.2.6",
|
|
46
|
+
"5.2.5",
|
|
47
|
+
"5.2.4",
|
|
48
|
+
"5.2.3",
|
|
49
|
+
"5.2.2",
|
|
50
|
+
"5.2.1",
|
|
51
|
+
"5.2.0",
|
|
52
|
+
"5.1.15",
|
|
53
|
+
)
|
|
54
|
+
versions_dir: Path = Path(__file__).parent / "_supported_versions"
|
|
55
|
+
file_ext: str = "json"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import yaml
|
|
8
|
+
from httpx import HTTPError
|
|
9
|
+
|
|
10
|
+
from .._names import ELAB_BRAND_NAME
|
|
11
|
+
from ..loggers import Logger
|
|
12
|
+
from ..utils import OpenAPISpecificationException
|
|
13
|
+
from ._names import ElabVersionDefaults
|
|
14
|
+
|
|
15
|
+
logger = Logger()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def read_openapi_spec(url: str) -> dict[str, Any]:
|
|
19
|
+
try:
|
|
20
|
+
spec = httpx.get(url)
|
|
21
|
+
except HTTPError as e:
|
|
22
|
+
raise OpenAPISpecificationException(
|
|
23
|
+
f"{ELAB_BRAND_NAME} OpenAPI specification URL could not be fetched. "
|
|
24
|
+
f"Exception details: {e}"
|
|
25
|
+
) from e
|
|
26
|
+
else:
|
|
27
|
+
try:
|
|
28
|
+
spec_yaml = yaml.safe_load(spec.text)
|
|
29
|
+
except yaml.YAMLError as e:
|
|
30
|
+
raise OpenAPISpecificationException(
|
|
31
|
+
f"{ELAB_BRAND_NAME} OpenAPI specification could not parsed "
|
|
32
|
+
f"as a valid YAML. Exception details: {e}"
|
|
33
|
+
) from e
|
|
34
|
+
else:
|
|
35
|
+
return spec_yaml
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_openapi_spec(spec: dict[str, Any]) -> dict[str, list[str]]:
|
|
39
|
+
endpoints: dict[str, set[str]] = defaultdict(set)
|
|
40
|
+
try:
|
|
41
|
+
spec_paths = spec["paths"]
|
|
42
|
+
except KeyError as e:
|
|
43
|
+
raise OpenAPISpecificationException(
|
|
44
|
+
"The OpenAPI specification does not contain the 'paths' field! "
|
|
45
|
+
) from e
|
|
46
|
+
for path in spec_paths:
|
|
47
|
+
endpoints_pattern = re.compile(r"^/([{}\w\-]+)(/{id}/([\w\-]+)(/{subid})?)?")
|
|
48
|
+
if match := endpoints_pattern.match(path):
|
|
49
|
+
main_endpoint_name, sub_endpoint_name = match.group(1, 3)
|
|
50
|
+
match main_endpoint_name:
|
|
51
|
+
case "{entity_type}":
|
|
52
|
+
try:
|
|
53
|
+
for parameter in spec_paths[path]["parameters"]:
|
|
54
|
+
match parameter["name"]:
|
|
55
|
+
case "entity_type":
|
|
56
|
+
entities = parameter["schema"]["enum"]
|
|
57
|
+
for entity in entities:
|
|
58
|
+
match sub_endpoint_name:
|
|
59
|
+
case None:
|
|
60
|
+
endpoints[entity] = set()
|
|
61
|
+
case _:
|
|
62
|
+
endpoints[entity].add(sub_endpoint_name)
|
|
63
|
+
break
|
|
64
|
+
case _:
|
|
65
|
+
continue
|
|
66
|
+
except KeyError as e:
|
|
67
|
+
raise OpenAPISpecificationException(
|
|
68
|
+
f"'entity_type' parameter is found in the OpenAPI specification "
|
|
69
|
+
f"path '{path}' but other expected field names were not found!"
|
|
70
|
+
) from e
|
|
71
|
+
case _:
|
|
72
|
+
match sub_endpoint_name:
|
|
73
|
+
case None:
|
|
74
|
+
endpoints[main_endpoint_name] = set()
|
|
75
|
+
case _:
|
|
76
|
+
endpoints[main_endpoint_name].add(sub_endpoint_name)
|
|
77
|
+
else:
|
|
78
|
+
raise OpenAPISpecificationException(
|
|
79
|
+
f"The OpenAPI specification path '{path}' does not match "
|
|
80
|
+
f"the expected pattern. This likely means a new type of endpoint "
|
|
81
|
+
f"path has been added to the specification that needs to be "
|
|
82
|
+
f"re-considered.."
|
|
83
|
+
)
|
|
84
|
+
return {k: sorted(list(v)) for k, v in sorted(endpoints.items())}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
versions_dir = ElabVersionDefaults.versions_dir
|
|
89
|
+
for version in ElabVersionDefaults.supported_versions:
|
|
90
|
+
version_file = versions_dir / f"{version}.{ElabVersionDefaults.file_ext}"
|
|
91
|
+
if version_file.exists():
|
|
92
|
+
logger.info(f"Version '{version}' already exists in {versions_dir}.")
|
|
93
|
+
continue
|
|
94
|
+
loaded_spec = read_openapi_spec(
|
|
95
|
+
f"https://raw.githubusercontent.com/elabftw/elabftw/refs/tags/{version}/apidoc/v2/openapi.yaml"
|
|
96
|
+
)
|
|
97
|
+
version_file.write_text(
|
|
98
|
+
json.dumps(parse_openapi_spec(loaded_spec), indent=4), encoding="utf-8"
|
|
99
|
+
)
|
|
100
|
+
logger.info(f"Version '{version}' data has been stored in {versions_dir}.")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apikeys": [],
|
|
3
|
+
"config": [],
|
|
4
|
+
"event": [],
|
|
5
|
+
"events": [],
|
|
6
|
+
"experiments": [
|
|
7
|
+
"comments",
|
|
8
|
+
"experiments_links",
|
|
9
|
+
"items_links",
|
|
10
|
+
"revisions",
|
|
11
|
+
"steps",
|
|
12
|
+
"tags",
|
|
13
|
+
"uploads"
|
|
14
|
+
],
|
|
15
|
+
"experiments_templates": [
|
|
16
|
+
"revisions",
|
|
17
|
+
"steps",
|
|
18
|
+
"tags"
|
|
19
|
+
],
|
|
20
|
+
"exports": [],
|
|
21
|
+
"extra_fields_keys": [],
|
|
22
|
+
"favtags": [],
|
|
23
|
+
"idps": [],
|
|
24
|
+
"import": [],
|
|
25
|
+
"info": [],
|
|
26
|
+
"items": [
|
|
27
|
+
"comments",
|
|
28
|
+
"experiments_links",
|
|
29
|
+
"items_links",
|
|
30
|
+
"revisions",
|
|
31
|
+
"steps",
|
|
32
|
+
"tags",
|
|
33
|
+
"uploads"
|
|
34
|
+
],
|
|
35
|
+
"items_types": [
|
|
36
|
+
"steps",
|
|
37
|
+
"tags"
|
|
38
|
+
],
|
|
39
|
+
"teams": [
|
|
40
|
+
"experiments_categories",
|
|
41
|
+
"experiments_status",
|
|
42
|
+
"items_status",
|
|
43
|
+
"teamgroups"
|
|
44
|
+
],
|
|
45
|
+
"todolist": [],
|
|
46
|
+
"unfinished_steps": [],
|
|
47
|
+
"users": [
|
|
48
|
+
"notifications",
|
|
49
|
+
"uploads"
|
|
50
|
+
]
|
|
51
|
+
}
|