elapi 2.2.0__tar.gz → 2.3.0__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 (77) hide show
  1. {elapi-2.2.0 → elapi-2.3.0}/PKG-INFO +2 -2
  2. {elapi-2.2.0 → elapi-2.3.0}/README.md +1 -1
  3. {elapi-2.2.0 → elapi-2.3.0}/pyproject.toml +1 -1
  4. elapi-2.3.0/src/elapi/VERSION +1 -0
  5. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/cli/elapi.py +94 -26
  6. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/configuration/__init__.py +1 -0
  7. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/configuration/validators.py +42 -124
  8. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/path.py +1 -1
  9. elapi-2.3.0/src/elapi/plugins/bill_teams/__init__.py +7 -0
  10. elapi-2.3.0/src/elapi/plugins/bill_teams/_doc.py +80 -0
  11. elapi-2.3.0/src/elapi/plugins/bill_teams/cli.py +1105 -0
  12. elapi-2.3.0/src/elapi/plugins/bill_teams/configuration.py +54 -0
  13. elapi-2.3.0/src/elapi/plugins/bill_teams/generate_table.py +148 -0
  14. elapi-2.3.0/src/elapi/plugins/bill_teams/names.py +14 -0
  15. elapi-2.3.0/src/elapi/plugins/bill_teams/registry.py +313 -0
  16. elapi-2.3.0/src/elapi/plugins/bill_teams/specification.py +235 -0
  17. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/bill_teams/validators.py +85 -11
  18. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/commons/export.py +13 -2
  19. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/commons/get_information.py +2 -1
  20. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/experiments/cli.py +27 -26
  21. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/styles/formats.py +13 -0
  22. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/utils/__init__.py +2 -0
  23. elapi-2.3.0/src/elapi/utils/typer_patches.py +214 -0
  24. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/utils/utils.py +46 -0
  25. elapi-2.2.0/src/elapi/VERSION +0 -1
  26. elapi-2.2.0/src/elapi/plugins/bill_teams/__init__.py +0 -2
  27. elapi-2.2.0/src/elapi/plugins/bill_teams/_doc.py +0 -43
  28. elapi-2.2.0/src/elapi/plugins/bill_teams/cli.py +0 -551
  29. elapi-2.2.0/src/elapi/plugins/bill_teams/invoice.py +0 -75
  30. elapi-2.2.0/src/elapi/plugins/bill_teams/specification.py +0 -47
  31. {elapi-2.2.0 → elapi-2.3.0}/LICENSE +0 -0
  32. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/__init__.py +0 -0
  33. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/_names.py +0 -0
  34. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/api/__init__.py +0 -0
  35. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/api/api.py +0 -0
  36. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/api/endpoint.py +0 -0
  37. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/api/validators.py +0 -0
  38. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/cli/__init__.py +0 -0
  39. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/cli/__main__.py +0 -0
  40. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/cli/_plugin_handler.py +0 -0
  41. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/cli/_venv_state_manager.py +0 -0
  42. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/cli/doc.py +0 -0
  43. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/configuration/_config_history.py +0 -0
  44. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/configuration/_overload_history.py +0 -0
  45. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/configuration/config.py +0 -0
  46. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/configuration/log_file.py +0 -0
  47. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/configuration/overridable_vars.py +0 -0
  48. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/core_validators/__init__.py +0 -0
  49. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/core_validators/base.py +0 -0
  50. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/core_validators/path.py +0 -0
  51. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/loggers/__init__.py +0 -0
  52. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/loggers/base.py +0 -0
  53. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/loggers/handlers/__init__.py +0 -0
  54. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/loggers/handlers/file.py +0 -0
  55. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/loggers/handlers/stderr.py +0 -0
  56. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/__init__.py +0 -0
  57. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/bill_teams/bill_teams.py +0 -0
  58. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/bill_teams/formats.py +0 -0
  59. /elapi-2.2.0/src/elapi/plugins/bill_teams/generate_table.py → /elapi-2.3.0/src/elapi/plugins/bill_teams/utils.py +0 -0
  60. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/commons/__init__.py +0 -0
  61. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/commons/cli_helpers.py +0 -0
  62. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/commons/get_data_from_input_or_path.py +0 -0
  63. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/commons/get_location_from_headers.py +0 -0
  64. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/experiments/__init__.py +0 -0
  65. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/experiments/_doc.py +0 -0
  66. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/experiments/experiments.py +0 -0
  67. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/experiments/formats.py +0 -0
  68. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/plugins/show_config.py +0 -0
  69. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/styles/__init__.py +0 -0
  70. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/styles/_markdown_doc.py +0 -0
  71. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/styles/_missing.py +0 -0
  72. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/styles/base.py +0 -0
  73. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/styles/colors.py +0 -0
  74. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/styles/highlight.py +0 -0
  75. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/styles/rich_utils.py +0 -0
  76. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/utils/messages.py +0 -0
  77. {elapi-2.2.0 → elapi-2.3.0}/src/elapi/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: elapi
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: elAPI is a powerful, extensible API client for eLabFTW.
5
5
  Home-page: https://www.urz.uni-heidelberg.de/en/service-catalogue/software-and-applications/elabftw
6
6
  License: AGPL-3.0-only
@@ -200,7 +200,7 @@ If both `host` and `api_token` are detected, you are good to go!
200
200
  ### Overriding configuration
201
201
 
202
202
  elAPI now supports `--override/--OC` as a global option that can be used to override the configuration parameters
203
- as detected by `elapi show-cofig`. All plugins will also automatically listen to the overridden configuration. This can
203
+ as detected by `elapi show-config`. All plugins will also automatically listen to the overridden configuration. This can
204
204
  be useful to set certain configurations temporarily. E.g., `elapi --OC '{"timeout": 300"}' get info` will override
205
205
  the `timeout` from the configuration files.
206
206
 
@@ -156,7 +156,7 @@ If both `host` and `api_token` are detected, you are good to go!
156
156
  ### Overriding configuration
157
157
 
158
158
  elAPI now supports `--override/--OC` as a global option that can be used to override the configuration parameters
159
- as detected by `elapi show-cofig`. All plugins will also automatically listen to the overridden configuration. This can
159
+ as detected by `elapi show-config`. All plugins will also automatically listen to the overridden configuration. This can
160
160
  be useful to set certain configurations temporarily. E.g., `elapi --OC '{"timeout": 300"}' get info` will override
161
161
  the `timeout` from the configuration files.
162
162
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "elapi"
3
- version = "2.2.0"
3
+ version = "2.3.0"
4
4
  description = "elAPI is a powerful, extensible API client for eLabFTW."
5
5
  authors = [
6
6
  "Alexander Haller, Mahadi Xion <elabftw@uni-heidelberg.de>",
@@ -0,0 +1 @@
1
+ 2.3.0
@@ -12,8 +12,10 @@ documented in https://doc.elabftw.net/api/v2/ with ease.
12
12
  $ elapi get users --id <id>
13
13
  """
14
14
 
15
+ import platform
15
16
  import sys
16
17
  from functools import partial
18
+ from json import JSONDecodeError
17
19
  from typing import Optional
18
20
 
19
21
  import typer
@@ -27,7 +29,7 @@ from ._plugin_handler import (
27
29
  )
28
30
  from .doc import __PARAMETERS__doc__ as docs
29
31
  from .. import APP_NAME
30
- from ..configuration import FALLBACK_EXPORT_DIR
32
+ from ..configuration import FALLBACK_EXPORT_DIR, get_active_export_dir
31
33
  from ..loggers import Logger, FileLogger
32
34
  from ..plugins.commons.cli_helpers import Typer
33
35
  from ..styles import get_custom_help_text
@@ -37,11 +39,13 @@ from ..styles import (
37
39
  rich_format_help_with_callback,
38
40
  __PACKAGE_IDENTIFIER__ as styles_package_identifier,
39
41
  )
42
+ from ..utils import get_external_python_version, PythonVersionCheckFailed
43
+ from ..utils.typer_patches import patch_typer_flag_value
40
44
 
41
45
  logger = Logger()
42
46
  file_logger = FileLogger()
43
47
  pretty.install()
44
-
48
+ patch_typer_flag_value()
45
49
 
46
50
  app = Typer()
47
51
  SENSITIVE_PLUGIN_NAMES: tuple[str, str, str] = (
@@ -262,7 +266,13 @@ for inter_app_obj in internal_plugin_typer_apps:
262
266
 
263
267
 
264
268
  def disable_plugin(
265
- main_app: Typer, /, *, plugin_name: str, err_msg: str, panel_name: str
269
+ main_app: Typer,
270
+ /,
271
+ *,
272
+ plugin_name: str,
273
+ err_msg: str,
274
+ panel_name: str,
275
+ short_reason: Optional[str] = None,
266
276
  ):
267
277
  import logging
268
278
  from ..utils import add_message
@@ -272,11 +282,15 @@ def disable_plugin(
272
282
  if plugin_name == registered_app.typer_instance.info.name:
273
283
  main_app.registered_groups.pop(i)
274
284
  break
285
+ help_message = (
286
+ f"🚫️ Disabled{' due to ' + short_reason if short_reason is not None else ''}. "
287
+ f"See `--help` or log file to know more."
288
+ )
275
289
 
276
290
  @main_app.command(
277
291
  name=plugin_name,
278
292
  rich_help_panel=panel_name,
279
- help="🚫️ Disabled due to name conflict. See `--help` or log file to know more.",
293
+ help=help_message,
280
294
  )
281
295
  def name_conflict_error():
282
296
  from ..core_validators import Exit
@@ -512,17 +526,16 @@ def get(
512
526
  ),
513
527
  ] = False,
514
528
  export: Annotated[
515
- Optional[bool],
529
+ Optional[str],
516
530
  typer.Option(
517
531
  "--export",
518
532
  "-e",
519
533
  help=docs["export"] + docs["export_details"],
520
- is_flag=True,
521
- is_eager=True,
534
+ is_flag=False,
535
+ flag_value="",
522
536
  show_default=False,
523
537
  ),
524
- ] = False,
525
- _export_dest: Annotated[Optional[str], typer.Argument(hidden=True)] = None,
538
+ ] = None,
526
539
  export_overwrite: Annotated[
527
540
  bool,
528
541
  typer.Option("--overwrite", help=docs["export_overwrite"], show_default=False),
@@ -564,8 +577,8 @@ def get(
564
577
  validate_identity = Validate(HostIdentityValidator())
565
578
  validate_identity()
566
579
 
567
- if export is False:
568
- _export_dest = None
580
+ if export == "":
581
+ export = get_active_export_dir()
569
582
  try:
570
583
  query: dict = get_structured_data(query, option_name="--query")
571
584
  except ValueError:
@@ -575,7 +588,7 @@ def get(
575
588
  except ValueError:
576
589
  raise Exit(1)
577
590
  data_format, export_dest, export_file_ext = CLIExport(
578
- data_format, _export_dest, export_overwrite
591
+ data_format, export, export_overwrite
579
592
  )
580
593
  if not query:
581
594
  format = CLIFormat(data_format, styles_package_identifier, export_file_ext)
@@ -583,9 +596,8 @@ def get(
583
596
  logger.info(
584
597
  "When --query is not empty, formatting with '--format/-F' and highlighting are disabled."
585
598
  )
586
- format = CLIFormat(
587
- "txt", styles_package_identifier, None
588
- ) # Use "txt" formatting to show binary
599
+ highlight_syntax = False
600
+ format = CLIFormat("txt", styles_package_identifier, None)
589
601
 
590
602
  try:
591
603
  session = GETRequest()
@@ -627,13 +639,29 @@ def get(
627
639
  raise Exit(1) from e
628
640
  try:
629
641
  formatted_data = format(response_data := raw_response.json())
642
+ # Because we prioritize the fact that most responses are sent as JSON
630
643
  except UnicodeDecodeError:
631
644
  logger.info(
632
645
  "Response data is in binary (or not UTF-8 encoded). "
633
646
  "--export/-e will not be able to infer the data format if export path is a directory."
634
647
  )
635
648
  formatted_data = format(response_data := raw_response.content)
636
- if export:
649
+ except JSONDecodeError as e:
650
+ if raw_response.status_code == 200:
651
+ logger.info(
652
+ f"Request was successful, but response data could not be parsed as JSON. "
653
+ f"Response will be read as binary."
654
+ )
655
+ formatted_data = format(response_data := raw_response.content)
656
+ else:
657
+ logger.error(
658
+ f"Request for '{endpoint_name}' data was received by the server but "
659
+ f"request was not successful. Response status: {raw_response.status_code}. "
660
+ f"Exception details: '{e!r}'. "
661
+ f"Response: '{raw_response.text}'"
662
+ )
663
+ raise Exit(1) from e
664
+ if export is not None:
637
665
  if isinstance(response_data, bytes):
638
666
  format.name = "binary"
639
667
  format.convention = "bin"
@@ -643,25 +671,25 @@ def get(
643
671
  _query_params = "_".join(map(lambda x: f"{x[0]}={x[1]}", query.items()))
644
672
  file_name_stub += f"_query_{_query_params}" if query else ""
645
673
  file_name_stub = re.sub(r"_{2,}", "_", file_name_stub).rstrip("_")
646
- export = Export(
674
+ export_response = Export(
647
675
  export_dest,
648
676
  file_name_stub=file_name_stub,
649
677
  file_extension=format.convention,
650
678
  format_name=format.name,
651
679
  )
652
680
  if not raw_response.is_success:
653
- export(data=formatted_data, verbose=False)
681
+ export_response(data=formatted_data, verbose=False)
654
682
  logger.warning(
655
683
  "Request was not successful. "
656
- f"Response for '{export.file_name_stub}' is exported to "
657
- f"{export.destination} anyway in {export.format_name} format."
684
+ f"Response for '{export_response.file_name_stub}' is exported to "
685
+ f"{export_response.destination} anyway in {export_response.format_name} format."
658
686
  )
659
687
  raise Exit(1)
660
688
  else:
661
- export(data=formatted_data, verbose=False)
689
+ export_response(data=formatted_data, verbose=False)
662
690
  logger.info(
663
- f"Response for '{export.file_name_stub}' is successfully exported to "
664
- f"{export.destination} in {export.format_name} format."
691
+ f"Response for '{export_response.file_name_stub}' is successfully exported to "
692
+ f"{export_response.destination} in {export_response.format_name} format."
665
693
  )
666
694
  else:
667
695
  if highlight_syntax is True:
@@ -673,6 +701,8 @@ def get(
673
701
  raise Exit(1)
674
702
  stdout_console.print(highlight(formatted_data))
675
703
  else:
704
+ if isinstance(response_data, bytes):
705
+ formatted_data = response_data
676
706
  if not raw_response.is_success:
677
707
  typer.echo(formatted_data, file=sys.stderr)
678
708
  raise Exit(1)
@@ -755,7 +785,6 @@ def post(
755
785
  from ssl import SSLError
756
786
  from .. import APP_NAME
757
787
  from ..api import GlobalSharedSession, POSTRequest, ElabFTWURLError
758
- from json import JSONDecodeError
759
788
  from ..core_validators import Validate
760
789
  from ..api.validators import HostIdentityValidator
761
790
  from ..plugins.commons import get_location_from_headers
@@ -975,7 +1004,6 @@ def patch(
975
1004
  from httpx import ConnectError
976
1005
  from ssl import SSLError
977
1006
  from ..api import GlobalSharedSession, PATCHRequest, ElabFTWURLError
978
- from json import JSONDecodeError
979
1007
  from ..core_validators import Validate
980
1008
  from ..api.validators import HostIdentityValidator
981
1009
  from ..styles import Format, Highlight, NoteText, print_typer_error
@@ -1138,7 +1166,6 @@ def delete(
1138
1166
  from httpx import ConnectError
1139
1167
  from ssl import SSLError
1140
1168
  from ..api import GlobalSharedSession, DELETERequest, ElabFTWURLError
1141
- from json import JSONDecodeError
1142
1169
  from ..core_validators import Validate
1143
1170
  from ..api.validators import HostIdentityValidator
1144
1171
  from ..styles import Format, Highlight, NoteText, print_typer_error
@@ -1303,6 +1330,7 @@ for plugin_info in external_local_plugin_typer_apps:
1303
1330
  plugin_name=app_name,
1304
1331
  err_msg=error_message,
1305
1332
  panel_name=THIRD_PARTY_PLUGIN_PANEL_NAME,
1333
+ short_reason="naming conflict",
1306
1334
  )
1307
1335
  elif app_name in INTERNAL_PLUGIN_NAME_REGISTRY:
1308
1336
  error_message = (
@@ -1320,6 +1348,7 @@ for plugin_info in external_local_plugin_typer_apps:
1320
1348
  plugin_name=app_name,
1321
1349
  err_msg=error_message,
1322
1350
  panel_name=INTERNAL_PLUGIN_PANEL_NAME,
1351
+ short_reason="naming conflict",
1323
1352
  )
1324
1353
  elif app_name in RESERVED_PLUGIN_NAMES:
1325
1354
  error_message = (
@@ -1337,8 +1366,47 @@ for plugin_info in external_local_plugin_typer_apps:
1337
1366
  plugin_name=app_name,
1338
1367
  err_msg=error_message,
1339
1368
  panel_name=THIRD_PARTY_PLUGIN_PANEL_NAME,
1369
+ short_reason="naming conflict",
1340
1370
  )
1341
1371
  else:
1372
+ if _venv is not None:
1373
+ try:
1374
+ external_plugin_python_version = get_external_python_version(
1375
+ venv_dir=_venv
1376
+ )[:2]
1377
+ except PythonVersionCheckFailed as e:
1378
+ error_message = (
1379
+ f"Plugin name '{original_name}' from {_path} uses virtual environment "
1380
+ f"{_venv} whose own Python version could not "
1381
+ f"be determined for the following reason: {e}. Plugin will be disabled."
1382
+ )
1383
+ disable_plugin(
1384
+ app,
1385
+ plugin_name=app_name,
1386
+ err_msg=error_message,
1387
+ panel_name=THIRD_PARTY_PLUGIN_PANEL_NAME,
1388
+ short_reason="undetermined .venv Python version",
1389
+ )
1390
+ continue
1391
+ else:
1392
+ if external_plugin_python_version != (
1393
+ own_python_version := platform.python_version_tuple()[:2]
1394
+ ):
1395
+ error_message = (
1396
+ f"Plugin name '{original_name}' from {_path} uses virtual environment "
1397
+ f"{_venv} whose Python version (major and minor) "
1398
+ f"'{'.'.join(external_plugin_python_version)}' "
1399
+ f"does not match {APP_NAME}'s own Python version "
1400
+ f"'{'.'.join(own_python_version)}'. Plugin will be disabled."
1401
+ )
1402
+ disable_plugin(
1403
+ app,
1404
+ plugin_name=app_name,
1405
+ err_msg=error_message,
1406
+ panel_name=THIRD_PARTY_PLUGIN_PANEL_NAME,
1407
+ short_reason=".venv Python version conflict",
1408
+ )
1409
+ continue
1342
1410
  EXTERNAL_LOCAL_PLUGIN_NAME_REGISTRY[app_name] = PluginInfo(
1343
1411
  ext_app_obj, _path, _venv, _proj_dir
1344
1412
  )
@@ -70,6 +70,7 @@ from .overridable_vars import (
70
70
  get_development_mode,
71
71
  get_active_plugin_configs,
72
72
  )
73
+ from .validators import ConfigurationValidation
73
74
  from .validators import PluginConfigurationValidator as _PluginConfigurationValidator
74
75
 
75
76
  validate_configuration(limited_to=[_PluginConfigurationValidator])
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from typing import Optional, Iterable
2
+ from typing import Optional, Iterable, Union
3
3
 
4
4
  from ._config_history import MinimalActiveConfiguration, FieldValueWithKey
5
5
  from ..core_validators import Validator, CriticalValidationError
@@ -7,25 +7,33 @@ from ..styles import stdout_console, Missing
7
7
  from ..styles.highlight import NoteText
8
8
 
9
9
 
10
- class HostConfigurationValidator(Validator):
11
- ALREADY_VALIDATED: bool = False
12
- __slots__ = ()
13
-
14
- def __init__(self, minimal_active_config_obj: MinimalActiveConfiguration, /):
10
+ class ConfigurationValidation:
11
+ def __init__(
12
+ self, minimal_active_config_obj: Union[MinimalActiveConfiguration, dict], /
13
+ ):
15
14
  self.active_configuration = minimal_active_config_obj
16
15
 
17
16
  @property
18
- def active_configuration(self) -> MinimalActiveConfiguration:
17
+ def active_configuration(self) -> Union[MinimalActiveConfiguration, dict]:
19
18
  return self._active_configuration
20
19
 
21
20
  @active_configuration.setter
22
21
  def active_configuration(self, value: MinimalActiveConfiguration):
23
- if not isinstance(value, MinimalActiveConfiguration):
22
+ if not isinstance(value, (MinimalActiveConfiguration, dict)):
24
23
  raise TypeError(
25
- f"Value must be an instance of {MinimalActiveConfiguration.__name__}."
24
+ f"Value must be an instance of "
25
+ f"{MinimalActiveConfiguration.__name__} or {dict.__name__}."
26
26
  )
27
27
  self._active_configuration = value
28
28
 
29
+
30
+ class HostConfigurationValidator(ConfigurationValidation, Validator):
31
+ ALREADY_VALIDATED: bool = False
32
+ __slots__ = ()
33
+
34
+ def __init__(self, *args):
35
+ super().__init__(*args)
36
+
29
37
  def validate(self) -> str:
30
38
  from ..loggers import Logger
31
39
  from .config import KEY_HOST
@@ -81,24 +89,12 @@ class HostConfigurationValidator(Validator):
81
89
  return host
82
90
 
83
91
 
84
- class APITokenConfigurationValidator(Validator):
92
+ class APITokenConfigurationValidator(ConfigurationValidation, Validator):
85
93
  ALREADY_VALIDATED: bool = False
86
94
  __slots__ = ()
87
95
 
88
- def __init__(self, minimal_active_config_obj: MinimalActiveConfiguration, /):
89
- self.active_configuration = minimal_active_config_obj
90
-
91
- @property
92
- def active_configuration(self) -> MinimalActiveConfiguration:
93
- return self._active_configuration
94
-
95
- @active_configuration.setter
96
- def active_configuration(self, value: MinimalActiveConfiguration):
97
- if not isinstance(value, MinimalActiveConfiguration):
98
- raise TypeError(
99
- f"Value must be an instance of {MinimalActiveConfiguration.__name__}."
100
- )
101
- self._active_configuration = value
96
+ def __init__(self, *args):
97
+ super().__init__(*args)
102
98
 
103
99
  def validate(self) -> str:
104
100
  from ..loggers import Logger
@@ -164,24 +160,12 @@ class APITokenConfigurationValidator(Validator):
164
160
  return api_token
165
161
 
166
162
 
167
- class ExportDirConfigurationValidator(Validator):
163
+ class ExportDirConfigurationValidator(ConfigurationValidation, Validator):
168
164
  ALREADY_VALIDATED: bool = False
169
165
  __slots__ = ()
170
166
 
171
- def __init__(self, minimal_active_config_obj: MinimalActiveConfiguration, /):
172
- self.active_configuration = minimal_active_config_obj
173
-
174
- @property
175
- def active_configuration(self) -> MinimalActiveConfiguration:
176
- return self._active_configuration
177
-
178
- @active_configuration.setter
179
- def active_configuration(self, value: MinimalActiveConfiguration):
180
- if not isinstance(value, MinimalActiveConfiguration):
181
- raise TypeError(
182
- f"Value must be an instance of {MinimalActiveConfiguration.__name__}."
183
- )
184
- self._active_configuration = value
167
+ def __init__(self, *args):
168
+ super().__init__(*args)
185
169
 
186
170
  def validate(self) -> Path:
187
171
  import errno
@@ -263,33 +247,15 @@ class ExportDirConfigurationValidator(Validator):
263
247
  return export_dir
264
248
 
265
249
 
266
- class BooleanWithFallbackConfigurationValidator(Validator):
250
+ class BooleanWithFallbackConfigurationValidator(ConfigurationValidation, Validator):
267
251
  ALREADY_VALIDATED: bool = False
268
252
  __slots__ = ()
269
253
 
270
- def __init__(
271
- self,
272
- minimal_active_config_obj: MinimalActiveConfiguration,
273
- /,
274
- key_name: str,
275
- fallback_value: bool,
276
- ):
277
- self.active_configuration = minimal_active_config_obj
254
+ def __init__(self, *args, key_name: str, fallback_value: bool):
255
+ super().__init__(*args)
278
256
  self.key_name = key_name
279
257
  self.fallback_value = fallback_value
280
258
 
281
- @property
282
- def active_configuration(self) -> MinimalActiveConfiguration:
283
- return self._active_configuration
284
-
285
- @active_configuration.setter
286
- def active_configuration(self, value: MinimalActiveConfiguration):
287
- if not isinstance(value, MinimalActiveConfiguration):
288
- raise TypeError(
289
- f"Value must be an instance of {MinimalActiveConfiguration.__name__}."
290
- )
291
- self._active_configuration = value
292
-
293
259
  def validate(self) -> bool:
294
260
  from ..loggers import Logger
295
261
 
@@ -313,33 +279,15 @@ class BooleanWithFallbackConfigurationValidator(Validator):
313
279
  return value
314
280
 
315
281
 
316
- class DecimalWithFallbackConfigurationValidator(Validator):
282
+ class DecimalWithFallbackConfigurationValidator(ConfigurationValidation, Validator):
317
283
  ALREADY_VALIDATED: bool = False
318
284
  __slots__ = ()
319
285
 
320
- def __init__(
321
- self,
322
- minimal_active_config_obj: MinimalActiveConfiguration,
323
- /,
324
- key_name: str,
325
- fallback_value: float,
326
- ):
327
- self.active_configuration = minimal_active_config_obj
286
+ def __init__(self, *args, key_name: str, fallback_value: float):
287
+ super().__init__(*args)
328
288
  self.key_name = key_name
329
289
  self.fallback_value = fallback_value
330
290
 
331
- @property
332
- def active_configuration(self) -> MinimalActiveConfiguration:
333
- return self._active_configuration
334
-
335
- @active_configuration.setter
336
- def active_configuration(self, value: MinimalActiveConfiguration):
337
- if not isinstance(value, MinimalActiveConfiguration):
338
- raise TypeError(
339
- f"Value must be an instance of {MinimalActiveConfiguration.__name__}."
340
- )
341
- self._active_configuration = value
342
-
343
291
  def validate(self) -> float:
344
292
  from ..loggers import Logger
345
293
 
@@ -363,33 +311,15 @@ class DecimalWithFallbackConfigurationValidator(Validator):
363
311
  return float(value)
364
312
 
365
313
 
366
- class PluginConfigurationValidator(Validator):
314
+ class PluginConfigurationValidator(ConfigurationValidation, Validator):
367
315
  ALREADY_VALIDATED: bool = False
368
316
  __slots__ = ()
369
317
 
370
- def __init__(
371
- self,
372
- minimal_active_config_obj: MinimalActiveConfiguration,
373
- /,
374
- key_name: str,
375
- fallback_value: dict,
376
- ):
377
- self.active_configuration = minimal_active_config_obj
318
+ def __init__(self, *args, key_name: str, fallback_value: dict):
319
+ super().__init__(*args)
378
320
  self.key_name = key_name
379
321
  self.fallback_value = fallback_value
380
322
 
381
- @property
382
- def active_configuration(self) -> MinimalActiveConfiguration:
383
- return self._active_configuration
384
-
385
- @active_configuration.setter
386
- def active_configuration(self, value: MinimalActiveConfiguration):
387
- if not isinstance(value, MinimalActiveConfiguration):
388
- raise TypeError(
389
- f"Value must be an instance of {MinimalActiveConfiguration.__name__}."
390
- )
391
- self._active_configuration = value
392
-
393
323
  def validate(self) -> dict:
394
324
  from dynaconf.utils.boxing import DynaBox
395
325
  from ..loggers import Logger
@@ -429,7 +359,7 @@ class PluginConfigurationValidator(Validator):
429
359
  return value
430
360
 
431
361
 
432
- class MainConfigurationValidator(Validator):
362
+ class MainConfigurationValidator(ConfigurationValidation, Validator):
433
363
  ALL_VALIDATORS: list = [
434
364
  HostConfigurationValidator,
435
365
  APITokenConfigurationValidator,
@@ -457,7 +387,7 @@ class MainConfigurationValidator(Validator):
457
387
  ):
458
388
  from ._config_history import MinimalActiveConfiguration
459
389
 
460
- self.active_configuration = MinimalActiveConfiguration()
390
+ super().__init__(MinimalActiveConfiguration())
461
391
  self.limited_to = limited_to
462
392
 
463
393
  @property
@@ -480,18 +410,6 @@ class MainConfigurationValidator(Validator):
480
410
  )
481
411
  self._limited_to = value
482
412
 
483
- @property
484
- def active_configuration(self) -> MinimalActiveConfiguration:
485
- return self._active_configuration
486
-
487
- @active_configuration.setter
488
- def active_configuration(self, value: MinimalActiveConfiguration):
489
- if not isinstance(value, MinimalActiveConfiguration):
490
- raise TypeError(
491
- f"Value must be an instance of {MinimalActiveConfiguration.__name__}."
492
- )
493
- self._active_configuration = value
494
-
495
413
  def validate(self) -> list[FieldValueWithKey]:
496
414
  from ..core_validators import Validate, ValidationError
497
415
  from .config import KEY_EXPORT_DIR
@@ -533,8 +451,8 @@ class MainConfigurationValidator(Validator):
533
451
  unsafe_token_warning = Validate(
534
452
  BooleanWithFallbackConfigurationValidator(
535
453
  self.active_configuration,
536
- KEY_UNSAFE_TOKEN_WARNING,
537
- UNSAFE_TOKEN_WARNING_DEFAULT_VAL,
454
+ key_name=KEY_UNSAFE_TOKEN_WARNING,
455
+ fallback_value=UNSAFE_TOKEN_WARNING_DEFAULT_VAL,
538
456
  )
539
457
  ).get()
540
458
  # Update validated_fields after validation
@@ -549,8 +467,8 @@ class MainConfigurationValidator(Validator):
549
467
  value = Validate(
550
468
  BooleanWithFallbackConfigurationValidator(
551
469
  self.active_configuration,
552
- key_name,
553
- default_value,
470
+ key_name=key_name,
471
+ fallback_value=default_value,
554
472
  )
555
473
  ).get()
556
474
  # Update validated_fields after validation
@@ -559,8 +477,8 @@ class MainConfigurationValidator(Validator):
559
477
  timeout = Validate(
560
478
  DecimalWithFallbackConfigurationValidator(
561
479
  self.active_configuration,
562
- KEY_TIMEOUT,
563
- TIMEOUT_DEFAULT_VAL,
480
+ key_name=KEY_TIMEOUT,
481
+ fallback_value=TIMEOUT_DEFAULT_VAL,
564
482
  )
565
483
  ).get()
566
484
  # Update validated_fields after validation
@@ -569,8 +487,8 @@ class MainConfigurationValidator(Validator):
569
487
  plugin = Validate(
570
488
  PluginConfigurationValidator(
571
489
  self.active_configuration,
572
- KEY_PLUGIN_KEY_NAME,
573
- PLUGIN_DEFAULT_VALUE,
490
+ key_name=KEY_PLUGIN_KEY_NAME,
491
+ fallback_value=PLUGIN_DEFAULT_VALUE,
574
492
  )
575
493
  ).get()
576
494
  # Update validated_fields after validation
@@ -177,7 +177,7 @@ class ProperPath:
177
177
  self.PathException = exception
178
178
  raise os_err
179
179
 
180
- def _remove_file(self, _file: Path = None, verbose: bool = False) -> None:
180
+ def _remove_file(self, _file: Optional[Path] = None, verbose: bool = False) -> None:
181
181
  file = _file if _file else self.expanded
182
182
  if not isinstance(file, Path):
183
183
  raise ValueError(
@@ -0,0 +1,7 @@
1
+ # ruff: noqa: F401
2
+ try:
3
+ from .cli import app
4
+ except ImportError:
5
+ # Mainly because bill-teams is optional plugin.
6
+ # So, "app" might not be importable unless all necessary dependencies are installed.
7
+ ...