elapi 2.4.4__tar.gz → 2.4.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. {elapi-2.4.4 → elapi-2.4.6}/PKG-INFO +33 -7
  2. {elapi-2.4.4 → elapi-2.4.6}/README.md +31 -6
  3. {elapi-2.4.4 → elapi-2.4.6}/pyproject.toml +3 -1
  4. elapi-2.4.6/src/elapi/VERSION +1 -0
  5. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_core_init/__init__.py +3 -0
  6. elapi-2.4.6/src/elapi/_core_init/_cache.py +53 -0
  7. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_core_init/_loggers/handlers/callback.py +5 -1
  8. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_names.py +33 -0
  9. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/api/__init__.py +8 -0
  10. elapi-2.4.6/src/elapi/api/_handle_unexp_response.py +58 -0
  11. elapi-2.4.6/src/elapi/api/_names.py +55 -0
  12. elapi-2.4.6/src/elapi/api/_openapi_parser.py +100 -0
  13. elapi-2.4.6/src/elapi/api/_supported_versions/5.1.15.json +51 -0
  14. elapi-2.4.6/src/elapi/api/_supported_versions/5.2.0.json +52 -0
  15. elapi-2.4.6/src/elapi/api/_supported_versions/5.2.1.json +53 -0
  16. elapi-2.4.6/src/elapi/api/_supported_versions/5.2.2.json +53 -0
  17. elapi-2.4.6/src/elapi/api/_supported_versions/5.2.3.json +53 -0
  18. elapi-2.4.6/src/elapi/api/_supported_versions/5.2.4.json +53 -0
  19. elapi-2.4.6/src/elapi/api/_supported_versions/5.2.5.json +53 -0
  20. elapi-2.4.6/src/elapi/api/_supported_versions/5.2.6.json +53 -0
  21. elapi-2.4.6/src/elapi/api/_supported_versions/5.2.7.json +53 -0
  22. elapi-2.4.6/src/elapi/api/_supported_versions/5.2.8.json +53 -0
  23. elapi-2.4.6/src/elapi/api/_supported_versions/5.3.0.json +53 -0
  24. elapi-2.4.6/src/elapi/api/_supported_versions/5.3.1.json +53 -0
  25. elapi-2.4.6/src/elapi/api/_supported_versions/5.3.2.json +53 -0
  26. elapi-2.4.6/src/elapi/api/_supported_versions/5.3.3.json +56 -0
  27. elapi-2.4.6/src/elapi/api/_supported_versions/5.3.4.json +56 -0
  28. elapi-2.4.6/src/elapi/api/_supported_versions/5.3.5.json +56 -0
  29. elapi-2.4.6/src/elapi/api/_supported_versions/5.3.6.json +56 -0
  30. elapi-2.4.6/src/elapi/api/_supported_versions/5.3.7.json +56 -0
  31. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/api/api.py +160 -80
  32. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/api/validators.py +8 -27
  33. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/cli/__main__.py +1 -1
  34. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/cli/_plugin_handler.py +148 -147
  35. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/cli/doc.py +31 -5
  36. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/cli/elapi.py +42 -13
  37. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/configuration/__init__.py +6 -0
  38. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/configuration/_overload_history.py +2 -0
  39. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/configuration/config.py +11 -2
  40. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/configuration/overridable_vars.py +7 -0
  41. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/configuration/validators.py +60 -2
  42. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/_doc.py +1 -2
  43. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/bill_teams.py +33 -20
  44. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/cli.py +20 -4
  45. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/commons/__init__.py +3 -3
  46. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/commons/cli_helpers.py +1 -13
  47. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/commons/get_data_from_input_or_path.py +2 -2
  48. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/commons/get_information.py +32 -2
  49. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/commons/get_whoami.py +19 -4
  50. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/experiments/_doc.py +1 -1
  51. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/mail/configuration.py +2 -2
  52. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/mail/mail.py +2 -2
  53. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/show_config.py +27 -2
  54. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/styles/formats.py +1 -1
  55. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/utils/__init__.py +8 -0
  56. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/utils/utils.py +30 -1
  57. elapi-2.4.4/src/elapi/VERSION +0 -1
  58. {elapi-2.4.4 → elapi-2.4.6}/LICENSE +0 -0
  59. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/.DS_Store +0 -0
  60. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/__init__.py +0 -0
  61. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_core_init/_loggers/__init__.py +0 -0
  62. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_core_init/_loggers/base.py +0 -0
  63. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_core_init/_loggers/handlers/__init__.py +0 -0
  64. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_core_init/_loggers/handlers/base.py +0 -0
  65. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_core_init/_loggers/handlers/stderr.py +0 -0
  66. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_core_init/_utils.py +0 -0
  67. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_vendor/__init__.py +0 -0
  68. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_vendor/haggis/LICENSE +0 -0
  69. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_vendor/haggis/__init__.py +0 -0
  70. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_vendor/haggis/logs.py +0 -0
  71. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/_vendor/haggis/version.py +0 -0
  72. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/api/endpoint.py +0 -0
  73. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/cli/__init__.py +0 -0
  74. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/cli/_venv_state_manager.py +0 -0
  75. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/configuration/_config_history.py +0 -0
  76. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/core_validators/__init__.py +0 -0
  77. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/core_validators/base.py +0 -0
  78. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/core_validators/path.py +0 -0
  79. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/loggers/__init__.py +0 -0
  80. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/loggers/base.py +0 -0
  81. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/loggers/handlers/__init__.py +0 -0
  82. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/loggers/handlers/file.py +0 -0
  83. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/loggers/log_file.py +0 -0
  84. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/path.py +0 -0
  85. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/.DS_Store +0 -0
  86. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/__init__.py +0 -0
  87. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/__init__.py +0 -0
  88. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/configuration.py +0 -0
  89. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/formats.py +0 -0
  90. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/generate_table.py +0 -0
  91. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/logger.py +0 -0
  92. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/names.py +0 -0
  93. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/registry.py +0 -0
  94. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/specification.py +0 -0
  95. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/utils.py +0 -0
  96. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/bill_teams/validators.py +0 -0
  97. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/commons/cli.py +0 -0
  98. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/commons/export.py +0 -0
  99. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/commons/get_location_from_headers.py +0 -0
  100. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/experiments/__init__.py +0 -0
  101. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/experiments/cli.py +0 -0
  102. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/experiments/experiments.py +0 -0
  103. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/experiments/formats.py +0 -0
  104. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/mail/__init__.py +0 -0
  105. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/mail/_yagmail_patch.py +0 -0
  106. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/mail/cli.py +0 -0
  107. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/mail/names.py +0 -0
  108. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/plugins/mail/yagmail_configuration.py +0 -0
  109. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/styles/__init__.py +0 -0
  110. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/styles/_markdown_doc.py +0 -0
  111. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/styles/_missing.py +0 -0
  112. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/styles/base.py +0 -0
  113. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/styles/colors.py +0 -0
  114. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/styles/highlight.py +0 -0
  115. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/styles/rich_utils.py +0 -0
  116. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/utils/messages.py +0 -0
  117. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/utils/typer_patches.py +0 -0
  118. {elapi-2.4.4 → elapi-2.4.6}/src/elapi/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elapi
3
- Version: 2.4.4
3
+ Version: 2.4.6
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-4.5%2B%20%7C%205.0%20%7C%205.1-%2323b3be">
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: `4.5`, `4.6`, `4.7`, `4.8`, `4.9`, `4.10`, `5.0`, `5.1`, `5.2`, and above.
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
+ ![elAPI endpoint names in --help](https://heibox.uni-heidelberg.de/f/067f5ec9f489493a8ec9/?dl=1)
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
- __](https://github.com/uhd-urz/elAPI/wiki/mail-plugin) to read more about how to configure the `mail` plugin with your
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-4.5%2B%20%7C%205.0%20%7C%205.1-%2323b3be">
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: `4.5`, `4.6`, `4.7`, `4.8`, `4.9`, `4.10`, `5.0`, `5.1`, `5.2`, and above.
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
+ ![elAPI endpoint names in --help](https://heibox.uni-heidelberg.de/f/067f5ec9f489493a8ec9/?dl=1)
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
- __](https://github.com/uhd-urz/elAPI/wiki/mail-plugin) to read more about how to configure the `mail` plugin with your
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.4"
3
+ version = "2.4.6"
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.6
@@ -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 self._store_okay:
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
+ }