elapi 2.4.11__tar.gz → 2.4.12__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 (141) hide show
  1. {elapi-2.4.11 → elapi-2.4.12}/PKG-INFO +4 -2
  2. {elapi-2.4.11 → elapi-2.4.12}/README.md +2 -1
  3. {elapi-2.4.11 → elapi-2.4.12}/pyproject.toml +3 -2
  4. elapi-2.4.12/src/elapi/VERSION +1 -0
  5. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_handle_unexp_response.py +14 -2
  6. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_names.py +3 -0
  7. elapi-2.4.12/src/elapi/api/_supported_versions/5.5.10.json +62 -0
  8. elapi-2.4.12/src/elapi/api/_supported_versions/5.5.11.json +62 -0
  9. elapi-2.4.12/src/elapi/api/_supported_versions/5.5.9.json +59 -0
  10. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/path.py +1 -1
  11. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/bill_teams.py +65 -48
  12. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/cli.py +29 -2
  13. elapi-2.4.11/src/elapi/VERSION +0 -1
  14. {elapi-2.4.11 → elapi-2.4.12}/LICENSE +0 -0
  15. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/.DS_Store +0 -0
  16. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/__init__.py +0 -0
  17. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_core_init/__init__.py +0 -0
  18. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_core_init/_cache.py +0 -0
  19. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_core_init/_loggers/__init__.py +0 -0
  20. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_core_init/_loggers/base.py +0 -0
  21. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_core_init/_loggers/handlers/__init__.py +0 -0
  22. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_core_init/_loggers/handlers/base.py +0 -0
  23. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_core_init/_loggers/handlers/callback.py +0 -0
  24. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_core_init/_loggers/handlers/stderr.py +0 -0
  25. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_core_init/_utils.py +0 -0
  26. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_names.py +0 -0
  27. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_vendor/__init__.py +0 -0
  28. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_vendor/haggis/LICENSE +0 -0
  29. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_vendor/haggis/__init__.py +0 -0
  30. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_vendor/haggis/logs.py +0 -0
  31. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/_vendor/haggis/version.py +0 -0
  32. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/__init__.py +0 -0
  33. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_openapi_parser.py +0 -0
  34. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.1.15.json +0 -0
  35. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.2.0.json +0 -0
  36. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.2.1.json +0 -0
  37. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.2.2.json +0 -0
  38. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.2.3.json +0 -0
  39. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.2.4.json +0 -0
  40. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.2.5.json +0 -0
  41. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.2.6.json +0 -0
  42. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.2.7.json +0 -0
  43. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.2.8.json +0 -0
  44. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.0.json +0 -0
  45. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.1.json +0 -0
  46. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.10.json +0 -0
  47. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.11.json +0 -0
  48. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.2.json +0 -0
  49. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.3.json +0 -0
  50. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.4.json +0 -0
  51. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.5.json +0 -0
  52. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.6.json +0 -0
  53. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.7.json +0 -0
  54. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.8.json +0 -0
  55. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.3.9.json +0 -0
  56. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.4.0.json +0 -0
  57. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.4.1.json +0 -0
  58. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.4.2.json +0 -0
  59. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.4.3.json +0 -0
  60. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.4.4.json +0 -0
  61. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.4.5.json +0 -0
  62. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.4.6.json +0 -0
  63. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.5.0.json +0 -0
  64. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.5.1.json +0 -0
  65. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.5.2.json +0 -0
  66. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.5.3.json +0 -0
  67. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.5.4.json +0 -0
  68. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.5.5.json +0 -0
  69. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.5.6.json +0 -0
  70. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.5.7.json +0 -0
  71. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/_supported_versions/5.5.8.json +0 -0
  72. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/api.py +0 -0
  73. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/endpoint.py +0 -0
  74. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/api/validators.py +0 -0
  75. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/cli/__init__.py +0 -0
  76. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/cli/__main__.py +0 -0
  77. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/cli/_plugin_handler.py +0 -0
  78. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/cli/_venv_state_manager.py +0 -0
  79. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/cli/doc.py +0 -0
  80. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/cli/elapi.py +0 -0
  81. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/configuration/__init__.py +0 -0
  82. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/configuration/_config_history.py +0 -0
  83. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/configuration/_overload_history.py +0 -0
  84. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/configuration/config.py +0 -0
  85. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/configuration/overridable_vars.py +0 -0
  86. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/configuration/validators.py +0 -0
  87. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/core_validators/__init__.py +0 -0
  88. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/core_validators/base.py +0 -0
  89. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/core_validators/path.py +0 -0
  90. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/loggers/__init__.py +0 -0
  91. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/loggers/base.py +0 -0
  92. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/loggers/handlers/__init__.py +0 -0
  93. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/loggers/handlers/file.py +0 -0
  94. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/loggers/log_file.py +0 -0
  95. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/.DS_Store +0 -0
  96. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/__init__.py +0 -0
  97. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/__init__.py +0 -0
  98. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/_doc.py +0 -0
  99. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/configuration.py +0 -0
  100. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/formats.py +0 -0
  101. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/generate_table.py +0 -0
  102. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/logger.py +0 -0
  103. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/names.py +0 -0
  104. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/registry.py +0 -0
  105. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/specification.py +0 -0
  106. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/utils.py +0 -0
  107. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/bill_teams/validators.py +0 -0
  108. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/commons/__init__.py +0 -0
  109. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/commons/cli.py +0 -0
  110. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/commons/cli_helpers.py +0 -0
  111. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/commons/export.py +0 -0
  112. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/commons/get_data_from_input_or_path.py +0 -0
  113. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/commons/get_information.py +0 -0
  114. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/commons/get_location_from_headers.py +0 -0
  115. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/commons/get_whoami.py +0 -0
  116. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/experiments/__init__.py +0 -0
  117. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/experiments/_doc.py +0 -0
  118. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/experiments/cli.py +0 -0
  119. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/experiments/experiments.py +0 -0
  120. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/experiments/formats.py +0 -0
  121. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/mail/__init__.py +0 -0
  122. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/mail/_yagmail_patch.py +0 -0
  123. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/mail/cli.py +0 -0
  124. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/mail/configuration.py +0 -0
  125. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/mail/mail.py +0 -0
  126. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/mail/names.py +0 -0
  127. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/mail/yagmail_configuration.py +0 -0
  128. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/plugins/show_config.py +0 -0
  129. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/styles/__init__.py +0 -0
  130. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/styles/_markdown_doc.py +0 -0
  131. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/styles/_missing.py +0 -0
  132. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/styles/base.py +0 -0
  133. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/styles/colors.py +0 -0
  134. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/styles/formats.py +0 -0
  135. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/styles/highlight.py +0 -0
  136. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/styles/rich_utils.py +0 -0
  137. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/utils/__init__.py +0 -0
  138. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/utils/messages.py +0 -0
  139. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/utils/typer_patches.py +0 -0
  140. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/utils/utils.py +0 -0
  141. {elapi-2.4.11 → elapi-2.4.12}/src/elapi/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: elapi
3
- Version: 2.4.11
3
+ Version: 2.4.12
4
4
  Summary: elAPI is a powerful, extensible API client for eLabFTW.
5
5
  Keywords: elabftw,api
6
6
  Author: Alexander Haller, Mahadi Xion
@@ -33,6 +33,7 @@ Requires-Dist: httpx-aiohttp>=0.1.12,<0.2
33
33
  Requires-Dist: pydantic>=2.12.3
34
34
  Requires-Dist: urllib3>=2.6.3
35
35
  Requires-Dist: aiohttp>=3.13.5
36
+ Requires-Dist: lxml>=6.1.0
36
37
  Requires-Dist: jinja2>=3.1.6,<3.2 ; extra == 'mail'
37
38
  Requires-Dist: nameparser>=1.1.3 ; extra == 'mail'
38
39
  Requires-Dist: yagmail>=0.15.293,<0.16 ; extra == 'mail'
@@ -148,7 +149,8 @@ your configuration details.
148
149
 
149
150
  elAPI is compatible with the following Python versions: `3.11`, `3.12`, `3.13`, `3.14`. elAPI supports
150
151
  the [eLabFTW REST API v2](https://doc.elabftw.net/api/v2/), and can be used with the following eLabFTW
151
- versions: `5.5.8`, `5.5.7`, `5.5.6`, `5.5.5`, `5.5.4`, `5.5.3`, `5.5.2`, `5.5.1`, `5.5.0`, `5.4.6`, `5.4.5`, `5.4.4`, `5.4.3`, `5.4.2`, `5.4.1`,
152
+ versions: `5.5.11`, `5.5.10`, `5.5.9`, `5.5.8`, `5.5.7`, `5.5.6`, `5.5.5`, `5.5.4`, `5.5.3`, `5.5.2`, `5.5.1`, `5.5.0`,
153
+ `5.4.6`, `5.4.5`, `5.4.4`, `5.4.3`, `5.4.2`, `5.4.1`,
152
154
  `5.4.0`, `5.3.11`, `5.3.10`, `5.3.9`, `5.3.8`, `5.3.7`, `5.3.6`,
153
155
  `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`,
154
156
  `5.2.3`, `5.2.2`, `5.2.1`, `5.2.0`, `5.1.1`.
@@ -96,7 +96,8 @@ your configuration details.
96
96
 
97
97
  elAPI is compatible with the following Python versions: `3.11`, `3.12`, `3.13`, `3.14`. elAPI supports
98
98
  the [eLabFTW REST API v2](https://doc.elabftw.net/api/v2/), and can be used with the following eLabFTW
99
- versions: `5.5.8`, `5.5.7`, `5.5.6`, `5.5.5`, `5.5.4`, `5.5.3`, `5.5.2`, `5.5.1`, `5.5.0`, `5.4.6`, `5.4.5`, `5.4.4`, `5.4.3`, `5.4.2`, `5.4.1`,
99
+ versions: `5.5.11`, `5.5.10`, `5.5.9`, `5.5.8`, `5.5.7`, `5.5.6`, `5.5.5`, `5.5.4`, `5.5.3`, `5.5.2`, `5.5.1`, `5.5.0`,
100
+ `5.4.6`, `5.4.5`, `5.4.4`, `5.4.3`, `5.4.2`, `5.4.1`,
100
101
  `5.4.0`, `5.3.11`, `5.3.10`, `5.3.9`, `5.3.8`, `5.3.7`, `5.3.6`,
101
102
  `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`,
102
103
  `5.2.3`, `5.2.2`, `5.2.1`, `5.2.0`, `5.1.1`.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "elapi"
3
- version = "2.4.11"
3
+ version = "2.4.12"
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"
@@ -40,6 +40,7 @@ dependencies = [
40
40
  # Used by yagmail, but added explicitly here to get an important security update
41
41
  "aiohttp>=3.13.5",
42
42
  # Used by httpx-aiohttp, but added explicitly here to get an important security update
43
+ "lxml>=6.1.0",
43
44
  ]
44
45
  license-files = [
45
46
  "LICENSE",
@@ -74,7 +75,7 @@ build-backend = "uv_build"
74
75
  [dependency-groups]
75
76
  dev = [
76
77
  "mypy>=1.18.2",
77
- "pip>=25.1.1", # vendy requires pip
78
+ "pip>26.0.1", # vendy requires pip
78
79
  "ptpython>=3.0.23,<4",
79
80
  "vendy>=0.0.3",
80
81
  ]
@@ -0,0 +1 @@
1
+ 2.4.12
@@ -1,11 +1,23 @@
1
1
  import json
2
+ from typing import TypedDict
2
3
 
3
4
  from ..loggers import Logger
4
5
  from ..utils import UnexpectedAPIResponseType
5
6
  from ._names import ElabUserGroups
6
7
 
7
8
  logger = Logger()
8
- NewUserTeamsType = list[dict[str, int | bool | str]]
9
+
10
+
11
+ class _ElabUserTeamsDict(TypedDict, total=False):
12
+ id: int
13
+ name: str
14
+ usergroup: int
15
+ is_admin: bool
16
+ is_owner: bool
17
+ is_archived: bool
18
+
19
+
20
+ NewUserTeamsType = list[_ElabUserTeamsDict]
9
21
 
10
22
  _DEBUG_LOG_EMIT_ONCE: bool = False
11
23
 
@@ -13,7 +25,7 @@ _DEBUG_LOG_EMIT_ONCE: bool = False
13
25
  def handle_new_user_teams(user_teams: str | NewUserTeamsType) -> NewUserTeamsType:
14
26
  global _DEBUG_LOG_EMIT_ONCE
15
27
 
16
- struct_user_teams: list[dict[str, int | bool | str]] = []
28
+ struct_user_teams: list[_ElabUserTeamsDict] = []
17
29
  match user_teams:
18
30
  case list():
19
31
  struct_user_teams = user_teams
@@ -32,6 +32,9 @@ class ElabScopes(IntEnum):
32
32
 
33
33
  class ElabVersionDefaults:
34
34
  supported_versions: tuple[str, ...] = (
35
+ "5.5.11",
36
+ "5.5.10",
37
+ "5.5.9",
35
38
  "5.5.8",
36
39
  "5.5.7",
37
40
  "5.5.6",
@@ -0,0 +1,62 @@
1
+ {
2
+ "apikeys": [],
3
+ "compounds": [],
4
+ "config": [],
5
+ "dspace": [],
6
+ "event": [],
7
+ "events": [],
8
+ "experiments": [
9
+ "comments",
10
+ "compounds_links",
11
+ "experiments_links",
12
+ "items_links",
13
+ "revisions",
14
+ "steps",
15
+ "tags",
16
+ "uploads"
17
+ ],
18
+ "experiments_templates": [
19
+ "revisions",
20
+ "steps",
21
+ "tags"
22
+ ],
23
+ "exports": [],
24
+ "extra_fields_keys": [],
25
+ "favtags": [],
26
+ "idps": [
27
+ "certs",
28
+ "endpoints"
29
+ ],
30
+ "idps_sources": [],
31
+ "import": [],
32
+ "info": [],
33
+ "items": [
34
+ "comments",
35
+ "compounds_links",
36
+ "experiments_links",
37
+ "items_links",
38
+ "revisions",
39
+ "steps",
40
+ "tags",
41
+ "uploads"
42
+ ],
43
+ "items_types": [
44
+ "steps",
45
+ "tags"
46
+ ],
47
+ "reports": [],
48
+ "storage_units": [],
49
+ "teams": [
50
+ "experiments_categories",
51
+ "experiments_status",
52
+ "items_status",
53
+ "resources_categories",
54
+ "teamgroups"
55
+ ],
56
+ "todolist": [],
57
+ "unfinished_steps": [],
58
+ "users": [
59
+ "notifications",
60
+ "uploads"
61
+ ]
62
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "apikeys": [],
3
+ "compounds": [],
4
+ "config": [],
5
+ "dspace": [],
6
+ "event": [],
7
+ "events": [],
8
+ "experiments": [
9
+ "comments",
10
+ "compounds_links",
11
+ "experiments_links",
12
+ "items_links",
13
+ "revisions",
14
+ "steps",
15
+ "tags",
16
+ "uploads"
17
+ ],
18
+ "experiments_templates": [
19
+ "revisions",
20
+ "steps",
21
+ "tags"
22
+ ],
23
+ "exports": [],
24
+ "extra_fields_keys": [],
25
+ "favtags": [],
26
+ "idps": [
27
+ "certs",
28
+ "endpoints"
29
+ ],
30
+ "idps_sources": [],
31
+ "import": [],
32
+ "info": [],
33
+ "items": [
34
+ "comments",
35
+ "compounds_links",
36
+ "experiments_links",
37
+ "items_links",
38
+ "revisions",
39
+ "steps",
40
+ "tags",
41
+ "uploads"
42
+ ],
43
+ "items_types": [
44
+ "steps",
45
+ "tags"
46
+ ],
47
+ "reports": [],
48
+ "storage_units": [],
49
+ "teams": [
50
+ "experiments_categories",
51
+ "experiments_status",
52
+ "items_status",
53
+ "resources_categories",
54
+ "teamgroups"
55
+ ],
56
+ "todolist": [],
57
+ "unfinished_steps": [],
58
+ "users": [
59
+ "notifications",
60
+ "uploads"
61
+ ]
62
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "apikeys": [],
3
+ "compounds": [],
4
+ "config": [],
5
+ "dspace": [],
6
+ "event": [],
7
+ "events": [],
8
+ "experiments": [
9
+ "comments",
10
+ "compounds_links",
11
+ "experiments_links",
12
+ "items_links",
13
+ "revisions",
14
+ "steps",
15
+ "tags",
16
+ "uploads"
17
+ ],
18
+ "experiments_templates": [
19
+ "revisions",
20
+ "steps",
21
+ "tags"
22
+ ],
23
+ "exports": [],
24
+ "extra_fields_keys": [],
25
+ "favtags": [],
26
+ "idps": [],
27
+ "idps_sources": [],
28
+ "import": [],
29
+ "info": [],
30
+ "items": [
31
+ "comments",
32
+ "compounds_links",
33
+ "experiments_links",
34
+ "items_links",
35
+ "revisions",
36
+ "steps",
37
+ "tags",
38
+ "uploads"
39
+ ],
40
+ "items_types": [
41
+ "steps",
42
+ "tags"
43
+ ],
44
+ "reports": [],
45
+ "storage_units": [],
46
+ "teams": [
47
+ "experiments_categories",
48
+ "experiments_status",
49
+ "items_status",
50
+ "resources_categories",
51
+ "teamgroups"
52
+ ],
53
+ "todolist": [],
54
+ "unfinished_steps": [],
55
+ "users": [
56
+ "notifications",
57
+ "uploads"
58
+ ]
59
+ }
@@ -240,7 +240,7 @@ class ProperPath:
240
240
  f"Could not open file {self._error_helper_compare_path_source(self.name, file)}. "
241
241
  f"Exception: {e!r}"
242
242
  )
243
- self.PathException = e
243
+ self.PathException = e.__class__
244
244
  raise e
245
245
  except (os_exception := OSError) as e:
246
246
  self.err_logger.debug(
@@ -1,6 +1,8 @@
1
+ from collections import defaultdict
1
2
  from datetime import datetime
3
+ from functools import cached_property
2
4
  from pathlib import Path
3
- from typing import Union
5
+ from typing import TypedDict, Union
4
6
 
5
7
  from dateutil import parser
6
8
  from dateutil.relativedelta import relativedelta
@@ -14,6 +16,30 @@ from ..commons import AsyncInformation, Information, RecursiveInformation
14
16
  logger = Logger()
15
17
 
16
18
 
19
+ class TeamMembersDict(TypedDict):
20
+ firstname: str
21
+ lastname: str
22
+ email: str
23
+ usergroup: str
24
+ is_archived: bool
25
+ is_expired: bool
26
+
27
+
28
+ class TeamsDict(TypedDict, total=False):
29
+ active_member_count: int
30
+ admins: dict[str, TeamMembersDict]
31
+ members: dict[str, TeamMembersDict]
32
+ on_trial: bool
33
+ team_created_at: str
34
+ team_id: int
35
+ team_name: str
36
+ total_archived_member_count: int
37
+ total_expired_member_count: int
38
+ total_member_count: int
39
+ total_unarchived_member_count: int
40
+ trial_ends_at: str
41
+
42
+
17
43
  class UsersInformation:
18
44
  __slots__ = "users", "user_id_prefix"
19
45
  endpoint_name = "users"
@@ -27,6 +53,7 @@ class UsersInformation:
27
53
  )
28
54
  for user in all_users:
29
55
  if (user_teams := user.get("teams")) is not None:
56
+ # noinspection PyTypeChecker
30
57
  if handle_new_user_teams(user_teams) is not None:
31
58
  return all_users
32
59
  return await fallback_all_users.items()
@@ -102,7 +129,7 @@ class OwnersInformation:
102
129
 
103
130
 
104
131
  class TeamsList:
105
- __slots__ = "users", "teams", "contract"
132
+ __slots__ = "users", "teams", "contract", "__dict__"
106
133
 
107
134
  def __init__(
108
135
  self,
@@ -112,16 +139,12 @@ class TeamsList:
112
139
  self.users = users_information
113
140
  self.teams = teams_information
114
141
 
115
- @property
142
+ @cached_property
116
143
  def BILL_RUN_DATE(self) -> datetime:
144
+ # cached_property is used to make sure that the same
145
+ # current datetime is used for all comparisons
117
146
  return datetime.now()
118
147
 
119
- @BILL_RUN_DATE.setter
120
- def BILL_RUN_DATE(self, value):
121
- raise AttributeError(
122
- "BILL_RUN_DATE is always the current date. It cannot be modified!"
123
- )
124
-
125
148
  @property
126
149
  def LAUNCH_DATE(self) -> datetime:
127
150
  return datetime(2023, 8, 1, 0, 0, 0)
@@ -141,62 +164,44 @@ class TeamsList:
141
164
  return True
142
165
  return False
143
166
 
144
- def _get_owners(self, admins_only: bool = False) -> dict:
167
+ def _get_teams(self, admins_only: bool = False) -> dict:
145
168
  # Generate teams information with team owners
146
- team_members: dict[str, dict[str, dict[str, str | bool | int]]] = {}
147
- admins: dict[str, dict[str, dict[str, str | bool | int]]] = {}
148
- teams: dict[str, dict[str, str | int | dict]] = {}
169
+ # noinspection PyTypeChecker
170
+ team_members: defaultdict[str, dict[str, TeamMembersDict]] = defaultdict(dict)
171
+ # noinspection PyTypeChecker
172
+ admins: defaultdict[str, dict[str, TeamMembersDict]] = defaultdict(dict)
173
+ teams: dict[str, TeamsDict] = {}
149
174
  for u in self.users:
150
175
  user_teams = handle_new_user_teams(u["teams"])
151
- for team in user_teams: # O(n^2): user_teams is again an iterable!
176
+ for team in user_teams: # O(n^2): we iterate over the "teams" field
152
177
  uid = u["userid"]
153
178
  # Get teams user count
154
179
  team_id = str(team["id"])
155
- if not team_members.get(team_id):
156
- team_members[team_id] = {
180
+ team_members[team_id].update(
181
+ {
157
182
  uid: {
158
183
  "firstname": u["firstname"],
159
184
  "lastname": u["lastname"],
160
185
  "email": u["email"],
161
186
  "usergroup": team["usergroup"],
162
- "expired": self.is_user_expired(user_data=u),
187
+ "is_archived": bool(team["is_archived"]),
188
+ "is_expired": self.is_user_expired(user_data=u),
163
189
  }
164
190
  }
165
- else:
166
- team_members[team_id].update(
191
+ )
192
+ if team["usergroup"] == ElabUserGroups.admin:
193
+ admins[team_id].update(
167
194
  {
168
195
  uid: {
169
196
  "firstname": u["firstname"],
170
197
  "lastname": u["lastname"],
171
198
  "email": u["email"],
172
199
  "usergroup": team["usergroup"],
173
- "expired": self.is_user_expired(user_data=u),
200
+ "is_archived": bool(team["is_archived"]),
201
+ "is_expired": self.is_user_expired(user_data=u),
174
202
  }
175
203
  }
176
204
  )
177
- if team["usergroup"] == ElabUserGroups.admin:
178
- if not admins.get(team_id):
179
- admins[team_id] = {
180
- uid: {
181
- "firstname": u["firstname"],
182
- "lastname": u["lastname"],
183
- "email": u["email"],
184
- "usergroup": team["usergroup"],
185
- "expired": self.is_user_expired(user_data=u),
186
- }
187
- }
188
- else:
189
- admins[team_id].update(
190
- {
191
- uid: {
192
- "firstname": u["firstname"],
193
- "lastname": u["lastname"],
194
- "email": u["email"],
195
- "usergroup": team["usergroup"],
196
- "expired": self.is_user_expired(user_data=u),
197
- }
198
- }
199
- )
200
205
  # Get team basic information
201
206
  teams[team_id] = {}
202
207
  teams[team_id]["team_name"] = team["name"]
@@ -214,10 +219,22 @@ class TeamsList:
214
219
  teams[team_id]["members"] = {}
215
220
  teams[team_id]["members"] = team_members[team_id]
216
221
  teams[team_id]["admins"] = admins.get(team_id, {})
217
- teams[team_id]["total_unarchived_member_count"] = len(team_members[team_id])
218
- teams[team_id]["active_member_count"]: int = 0
222
+ teams[team_id]["total_member_count"] = len(team_members[team_id])
223
+ teams[team_id]["total_unarchived_member_count"] = 0
224
+ teams[team_id]["total_archived_member_count"] = 0
225
+ teams[team_id]["total_expired_member_count"] = 0
226
+ teams[team_id]["active_member_count"] = 0
219
227
  for k in team_members[team_id]:
220
- if team_members[team_id][k]["expired"] is False:
228
+ if not team_members[team_id][k]["is_archived"]:
229
+ teams[team_id]["total_unarchived_member_count"] += 1
230
+ if team_members[team_id][k]["is_archived"]:
231
+ teams[team_id]["total_archived_member_count"] += 1
232
+ if team_members[team_id][k]["is_expired"]:
233
+ teams[team_id]["total_expired_member_count"] += 1
234
+ if not (
235
+ team_members[team_id][k]["is_expired"]
236
+ or team_members[team_id][k]["is_archived"]
237
+ ):
221
238
  teams[team_id]["active_member_count"] += 1
222
239
  # Add trial information
223
240
  trial_starts_at = self.team_trial_start_date(
@@ -225,12 +242,12 @@ class TeamsList:
225
242
  )
226
243
  trial_ends_at = trial_starts_at + self.TRIAL_PERIOD
227
244
  teams[team_id]["trial_ends_at"] = str(trial_ends_at)
228
- teams[team_id]["on_trial"] = trial_ends_at > datetime.now()
245
+ teams[team_id]["on_trial"] = trial_ends_at > self.BILL_RUN_DATE
229
246
 
230
247
  return teams
231
248
 
232
249
  def items(self, admins_only: bool = False) -> dict:
233
- return self._get_owners(admins_only)
250
+ return self._get_teams(admins_only)
234
251
 
235
252
 
236
253
  class OwnersList:
@@ -9,6 +9,7 @@ from types import NoneType
9
9
 
10
10
  import typer
11
11
 
12
+ from ...path import ProperPath
12
13
  from ...utils import UnexpectedAPIResponseType
13
14
  from .generate_table import is_team_on_trial
14
15
  from .names import PLUGIN_NAME, REGISTRY_SUB_PLUGIN_NAME, TARGET_GROUP_NAME
@@ -135,6 +136,17 @@ else:
135
136
  # meant to suppress raising the final exception once all attempts have been made
136
137
  )
137
138
  def get_teams(
139
+ users_data_path: Annotated[
140
+ Optional[str],
141
+ typer.Option(
142
+ "--users-data-path",
143
+ "-U",
144
+ help="File path to users JSON data. If passed, "
145
+ "this will be considered as the source of user data truth. The JSON structure "
146
+ "must match the API response of 'elapi get users'",
147
+ show_default=False,
148
+ ),
149
+ ] = None,
138
150
  admins_only: Annotated[
139
151
  bool,
140
152
  typer.Option(
@@ -183,6 +195,16 @@ else:
183
195
  from ...api.validators import HostIdentityValidator, PermissionValidator
184
196
  from ...core_validators import Validate
185
197
 
198
+ if users_data_path is not None:
199
+ try:
200
+ users_data_path_ = ProperPath(users_data_path)
201
+ except (ValueError, TypeError):
202
+ print_typer_error(
203
+ f"--users-data '{users_data_path}' must be a valid file path."
204
+ )
205
+ raise Exit(1)
206
+ else:
207
+ users_data_path_ = None
186
208
  global_session = GlobalSharedSession(
187
209
  async_rate_limit=(
188
210
  async_rate_limit := _retry_init_handler.get_updated_rate_limit(
@@ -237,11 +259,16 @@ else:
237
259
  UsersInformation,
238
260
  )
239
261
 
240
- users_info, teams_info = UsersInformation(), TeamsInformation()
262
+ teams_info = TeamsInformation()
241
263
 
242
264
  async def gather_teams_list() -> TeamsList:
265
+ if users_data_path_ is not None:
266
+ with users_data_path_.open("r", encoding="utf-8") as users_data_file:
267
+ users_info = json.load(users_data_file)
268
+ else:
269
+ users_info = await UsersInformation().items()
243
270
  try:
244
- tl = TeamsList(await users_info.items(), await teams_info.aitems())
271
+ tl = TeamsList(users_info, await teams_info.aitems())
245
272
  except (RuntimeError, InterruptedError) as error:
246
273
  logger.error(
247
274
  f"Retrieving users/teams data has failed. Exception details: {error}"
@@ -1 +0,0 @@
1
- 2.4.11
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes