elapi 1.0.7.dev2__tar.gz → 1.0.7.dev3__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 (58) hide show
  1. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/PKG-INFO +3 -3
  2. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/pyproject.toml +2 -3
  3. elapi-1.0.7.dev3/src/elapi/VERSION +1 -0
  4. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/cli/doc.py +4 -0
  5. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/cli/elapi.py +7 -1
  6. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/configuration/config.py +19 -10
  7. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/configuration/log_file.py +1 -1
  8. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/path.py +15 -11
  9. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/_doc.py +14 -0
  10. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/cli.py +19 -5
  11. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/commons/__init__.py +1 -1
  12. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/commons/cli_helpers.py +13 -3
  13. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/commons/export.py +37 -3
  14. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/experiments/_doc.py +12 -0
  15. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/experiments/cli.py +19 -5
  16. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/validators/__init__.py +1 -1
  17. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/validators/base.py +6 -8
  18. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/validators/path.py +45 -5
  19. elapi-1.0.7.dev2/src/elapi/VERSION +0 -1
  20. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/LICENSE +0 -0
  21. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/README.md +0 -0
  22. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/__init__.py +0 -0
  23. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/_exceptions.py +0 -0
  24. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/_names.py +0 -0
  25. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/api.py +0 -0
  26. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/cli/__init__.py +0 -0
  27. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/cli/__main__.py +0 -0
  28. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/cli/_plugin_handler.py +0 -0
  29. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/configuration/__init__.py +0 -0
  30. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/configuration/_config_history.py +0 -0
  31. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/endpoint.py +0 -0
  32. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/loggers/__init__.py +0 -0
  33. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/loggers/base.py +0 -0
  34. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/loggers/handlers/__init__.py +0 -0
  35. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/loggers/handlers/file.py +0 -0
  36. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/loggers/handlers/stderr.py +0 -0
  37. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/__init__.py +0 -0
  38. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/__init__.py +0 -0
  39. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/bill_teams.py +0 -0
  40. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/format.py +0 -0
  41. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/invoice.py +0 -0
  42. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/validator.py +0 -0
  43. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/commons/get_information.py +0 -0
  44. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/commons/get_location_from_headers.py +0 -0
  45. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/experiments/__init__.py +0 -0
  46. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/experiments/experiments.py +0 -0
  47. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/experiments/formats.py +0 -0
  48. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/show_config.py +0 -0
  49. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/version.py +0 -0
  50. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/__init__.py +0 -0
  51. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/_markdown_doc.py +0 -0
  52. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/_missing.py +0 -0
  53. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/base.py +0 -0
  54. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/colors.py +0 -0
  55. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/format.py +0 -0
  56. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/highlight.py +0 -0
  57. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/validators/identity.py +0 -0
  58. {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/validators/permission.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: elapi
3
- Version: 1.0.7.dev2
3
+ Version: 1.0.7.dev3
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
7
7
  Keywords: elabftw,api
8
- Author: Alexander Haller
9
- Author-email: alexander.haller@urz.uni-heidelberg.de
8
+ Author: Alexander Haller, Mahadi Xion
9
+ Author-email: elabftw@uni-heidelberg.de
10
10
  Maintainer: Mahadi Xion
11
11
  Maintainer-email: mahadi.xion@urz.uni-heidelberg.de
12
12
  Requires-Python: >=3.9.0,<4.0.0
@@ -1,10 +1,9 @@
1
1
  [tool.poetry]
2
2
  name = "elapi"
3
- version = "1.0.7-dev2"
3
+ version = "1.0.7.dev3"
4
4
  description = "elAPI is a powerful, extensible API client for eLabFTW."
5
5
  authors = [
6
- "Alexander Haller <alexander.haller@urz.uni-heidelberg.de>",
7
- "Mahadi Xion <mahadi.xion@urz.uni-heidelberg.de>",
6
+ "Alexander Haller, Mahadi Xion <elabftw@uni-heidelberg.de>",
8
7
  ]
9
8
  maintainers = [
10
9
  "Mahadi Xion <mahadi.xion@urz.uni-heidelberg.de>",
@@ -0,0 +1 @@
1
+ 1.0.7.dev3
@@ -71,6 +71,10 @@ __PARAMETERS__doc__ = {
71
71
  f"If _--format/-F_ is absent, then {APP_NAME} can use the file extension as the data format. "
72
72
  f"If _--format/-F_ is also present, then file extension is ignored, "
73
73
  f"and --format value takes precedence.\n",
74
+ "export_overwrite": f"If given --export/-e path is a file, but it already **exists**, "
75
+ f"{APP_NAME} will not overwrite the file by default, and will instead use the "
76
+ f"fallback location. _--overwrite_ needs to be passed if {APP_NAME} should overwrite "
77
+ f"an existing file when exporting.",
74
78
  "data_format": f"Format style for the output. Supported values are: {supported_highlighting_formats}. "
75
79
  f"The values are case insensitive. The default format is `{DEFAULT_EXPORT_DATA_FORMAT.upper()}`. "
76
80
  "When 'txt' is used, the response will be sent in *original*, un-formatted (almost), "
@@ -195,6 +195,10 @@ def get(
195
195
  ),
196
196
  ] = False,
197
197
  _export_dest: Annotated[Optional[str], typer.Argument(hidden=True)] = None,
198
+ export_overwrite: Annotated[
199
+ bool,
200
+ typer.Option("--overwrite", help=docs["export_overwrite"], show_default=False),
201
+ ] = False,
198
202
  ) -> dict:
199
203
  """
200
204
  Make `GET` requests to eLabFTW endpoints as documented in
@@ -235,7 +239,9 @@ def get(
235
239
  style="red",
236
240
  )
237
241
  raise typer.Exit(1)
238
- data_format, export_dest, export_file_ext = CLIExport(data_format, _export_dest)
242
+ data_format, export_dest, export_file_ext = CLIExport(
243
+ data_format, _export_dest, export_overwrite
244
+ )
239
245
  if not query:
240
246
  format = CLIFormat(data_format, export_file_ext)
241
247
  else:
@@ -1,10 +1,12 @@
1
+ import errno
1
2
  import os
2
3
  from pathlib import Path
3
4
 
4
5
  from dynaconf import Dynaconf
5
6
 
6
7
  from ._config_history import ConfigHistory, InspectConfigHistory
7
- from .log_file import LOG_FILE_PATH, XDG_DATA_HOME
8
+ from .log_file import LOG_FILE_PATH, _XDG_DATA_HOME
9
+
8
10
  # noinspection PyUnresolvedReferences
9
11
  from .._names import (
10
12
  APP_NAME,
@@ -25,11 +27,13 @@ from .._names import (
25
27
  )
26
28
  from ..loggers import Logger
27
29
  from ..path import ProperPath
30
+ from ..styles import stdin_console, NoteText
28
31
  from ..validators import (
29
32
  Validate,
30
33
  ValidationError,
31
34
  CriticalValidationError,
32
35
  PathValidator,
36
+ PathValidationError,
33
37
  )
34
38
 
35
39
  logger = Logger()
@@ -116,19 +120,24 @@ TOKEN_BEARER: str = "Authorization"
116
120
  # Export location
117
121
  CONFIG_EXPORT_DIR = ProperPath(
118
122
  (_CONFIG_EXPORT_DIR_ORIGINAL := settings.get(KEY_EXPORT_DIR)) or os.devnull,
123
+ kind=(_CONFIG_EXPORT_DIR_ORIGINAL and "dir") or "file",
119
124
  err_logger=logger,
120
125
  ) # the default "os.devnull" saves ProperPath from TypeError, ValueError
121
126
  # for when settings.get(KEY_EXPORT_DIR) is None/"".
122
- if _CONFIG_EXPORT_DIR_ORIGINAL and CONFIG_EXPORT_DIR.kind != "dir":
123
- logger.warning(
124
- f"{KEY_EXPORT_DIR}: {_CONFIG_EXPORT_DIR_ORIGINAL} is not a directory!"
125
- )
126
- logger.debug("If you want to export to a file use '--export <path-to-file>'.")
127
- CONFIG_EXPORT_DIR = None
128
127
  try:
129
128
  EXPORT_DIR = Validate(PathValidator(CONFIG_EXPORT_DIR)).get()
130
- except ValidationError:
129
+ except PathValidationError as e:
131
130
  if _CONFIG_EXPORT_DIR_ORIGINAL:
131
+ if e.errno == errno.EEXIST:
132
+ logger.warning(
133
+ f"{KEY_EXPORT_DIR}: {_CONFIG_EXPORT_DIR_ORIGINAL} from configuration file is not a directory!"
134
+ )
135
+ stdin_console.print(
136
+ NoteText(
137
+ "If you want to export to a file use '--export <path-to-file>'.\n",
138
+ stem="Note",
139
+ )
140
+ )
132
141
  logger.warning(
133
142
  f"{KEY_EXPORT_DIR}: {_CONFIG_EXPORT_DIR_ORIGINAL} from configuration file couldn't be validated! "
134
143
  )
@@ -149,7 +158,7 @@ except ValidationError:
149
158
  logger.critical(
150
159
  f"{APP_NAME} couldn't validate {FALLBACK_EXPORT_DIR} to store exported data. "
151
160
  f"This is a fatal error. To quickly fix this error define an export directory "
152
- f"with 'export_dir' in configuration file. {APP_NAME} will not run!"
161
+ f"with '{KEY_EXPORT_DIR}' in configuration file. {APP_NAME} will not run!"
153
162
  )
154
163
  raise CriticalValidationError
155
164
  # Falls back to ~/Downloads if $XDG_DOWNLOAD_DIR isn't found
@@ -159,7 +168,7 @@ if LOG_FILE_PATH.parent != LOG_DIR_ROOT:
159
168
  APP_DATA_DIR = LOG_FILE_PATH.parent
160
169
  else:
161
170
  validate_app_dir = Validate(
162
- PathValidator([XDG_DATA_HOME / APP_NAME, FALLBACK_DIR / APP_NAME])
171
+ PathValidator([_XDG_DATA_HOME / APP_NAME, FALLBACK_DIR / APP_NAME])
163
172
  )
164
173
  try:
165
174
  APP_DATA_DIR = validate_app_dir.get()
@@ -23,7 +23,7 @@ validate_path = Validate(
23
23
  PathValidator(
24
24
  [
25
25
  LOG_DIR_ROOT / LOG_FILE_NAME,
26
- (XDG_DATA_HOME := ProperPath(os.getenv(ENV_XDG_DATA_HOME, os.devnull)))
26
+ (_XDG_DATA_HOME := ProperPath(os.getenv(ENV_XDG_DATA_HOME, os.devnull)))
27
27
  / APP_NAME
28
28
  / LOG_FILE_NAME,
29
29
  FALLBACK_DIR / LOG_FILE_NAME,
@@ -5,7 +5,7 @@ import re
5
5
  from contextlib import contextmanager
6
6
  from pathlib import Path
7
7
  from shutil import rmtree
8
- from typing import Union, TextIO, Generator
8
+ from typing import Union, TextIO, Generator, Optional
9
9
 
10
10
  from ._exceptions import NoException
11
11
  from .loggers import SimpleLogger
@@ -16,7 +16,7 @@ class ProperPath:
16
16
  self,
17
17
  name: Union[str, Path, None, "ProperPath"],
18
18
  env_var: bool = False,
19
- kind: Union[str, None] = "",
19
+ kind: Optional[str] = None, # Here, None => Undefined/unknown
20
20
  err_logger: logging.Logger = SimpleLogger(),
21
21
  ):
22
22
  self.name = name
@@ -103,7 +103,7 @@ class ProperPath:
103
103
 
104
104
  @kind.setter
105
105
  def kind(self, value) -> None:
106
- if not value:
106
+ if value is None:
107
107
  self._kind = (
108
108
  "dir"
109
109
  if self.expanded.is_dir()
@@ -139,20 +139,24 @@ class ProperPath:
139
139
  else f"PATH={target}"
140
140
  )
141
141
 
142
- def create(self) -> Union[Path, None]:
143
- if not (path := self.expanded.resolve(strict=False)).exists():
144
- # except FileNotFoundError:
145
- message = (
146
- f"{self._error_helper_compare_path_source(self.name, path)} could not be found. "
147
- f"An attempt to create PATH={path} will be made."
148
- )
149
- self.err_logger.warning(message)
142
+ def create(self, verbose: bool = True) -> Union[Path, None]:
143
+ path = self.expanded.resolve(strict=False)
150
144
  try:
151
145
  if self.kind == "file":
152
146
  path_parent, path_file = path.parent, path.name
147
+ if not path_parent.exists() and verbose:
148
+ self.err_logger.info(
149
+ f"Directory {self._error_helper_compare_path_source(self.name, path_parent)} could not be "
150
+ f"found. An attempt to create directory {path_parent} will be made."
151
+ )
153
152
  path_parent.mkdir(parents=True, exist_ok=True)
154
153
  (path_parent / path_file).touch(exist_ok=True)
155
154
  elif self.kind == "dir":
155
+ if not path.exists() and verbose:
156
+ self.err_logger.info(
157
+ f"Directory {self._error_helper_compare_path_source(self.name, path)} could not be found. "
158
+ f"An attempt to create directory {path} will be made."
159
+ )
156
160
  path.mkdir(parents=True, exist_ok=True)
157
161
  except (exception := PermissionError) as e:
158
162
  message = f"Permission to create {self._error_helper_compare_path_source(self.name, path)} is denied."
@@ -1,3 +1,4 @@
1
+ from ... import APP_NAME
1
2
  from ...configuration import DEFAULT_EXPORT_DATA_FORMAT
2
3
 
3
4
  _UNIQUE_SUPPORTED_FORMATS = ["json", "yaml", "txt"]
@@ -27,4 +28,17 @@ __PARAMETERS__doc__ = {
27
28
  " to some external formatting program like `less`. "
28
29
  "If an unsupported format value is provided then the output "
29
30
  f"format falls back to `{DEFAULT_EXPORT_DATA_FORMAT.upper()}`.",
31
+ "export_details": f"- If _'--export'_ is passed without any following value, then it acts as a flag, and "
32
+ f"`export_dir` value from configuration file is used. "
33
+ f"It should be clear that `export_dir` in a configuration file only accepts a directory path.\n"
34
+ f"- If a directory path is provided as a value, "
35
+ f"i.e., _'--export \<path/to/directory\>'_, then that path is used instead. "
36
+ f"When the path is a directory, "
37
+ f"the file name is auto-generated using the following scheme: *'DATE_HHMMSS_\<FUNCTION\>.EXT'*.\n"
38
+ f"- If a file path is passed, i.e., _'--export <path/to/file.json>'_, "
39
+ f"then data is simply exported to that file. This allows custom file name scheme. "
40
+ f"If _--format/-F_ is absent, then {APP_NAME} can use the file extension as the data format. "
41
+ f"If _--format/-F_ is also present, then file extension is ignored, "
42
+ f"and --format value takes precedence.\n"
43
+
30
44
  }
@@ -42,13 +42,19 @@ def get_teams(
42
42
  typer.Option(
43
43
  "--export",
44
44
  "-e",
45
- help=elapi_docs["export"] + elapi_docs["export_details"],
45
+ help=elapi_docs["export"] + docs["export_details"],
46
46
  is_flag=True,
47
47
  is_eager=True,
48
48
  show_default=False,
49
49
  ),
50
50
  ] = False,
51
51
  _export_dest: Annotated[Optional[str], typer.Argument(hidden=True)] = None,
52
+ export_overwrite: Annotated[
53
+ bool,
54
+ typer.Option(
55
+ "--overwrite", help=elapi_docs["export_overwrite"], show_default=False
56
+ ),
57
+ ] = False,
52
58
  ) -> dict:
53
59
  """Get billable teams data."""
54
60
  from .format import remove_csv_formatter_support
@@ -72,7 +78,9 @@ def get_teams(
72
78
  if sort_json_format:
73
79
  from .format import JSONSortedFormat # noqa: F401
74
80
 
75
- data_format, export_dest, export_file_ext = CLIExport(data_format, _export_dest)
81
+ data_format, export_dest, export_file_ext = CLIExport(
82
+ data_format, _export_dest, export_overwrite
83
+ )
76
84
  format = CLIFormat(data_format, export_file_ext)
77
85
 
78
86
  import asyncio
@@ -128,13 +136,19 @@ def get_owners(
128
136
  typer.Option(
129
137
  "--export",
130
138
  "-e",
131
- help=elapi_docs["export"] + elapi_docs["export_details"],
139
+ help=elapi_docs["export"] + docs["export_details"],
132
140
  is_flag=True,
133
141
  is_eager=True,
134
142
  show_default=False,
135
143
  ),
136
144
  ] = False,
137
145
  _export_dest: Annotated[Optional[str], typer.Argument(hidden=True)] = None,
146
+ export_overwrite: Annotated[
147
+ bool,
148
+ typer.Option(
149
+ "--overwrite", help=elapi_docs["export_overwrite"], show_default=False
150
+ ),
151
+ ] = False,
138
152
  ) -> dict:
139
153
  """Get billable team owners data."""
140
154
  from .format import remove_csv_formatter_support
@@ -163,7 +177,7 @@ def get_owners(
163
177
  if sort_json_format:
164
178
  from .format import JSONSortedFormat # noqa: F401
165
179
 
166
- data_format, export_dest, export_file_ext = CLIExport(data_format, _export_dest)
180
+ data_format, export_dest, export_file_ext = CLIExport(data_format, _export_dest, export_overwrite)
167
181
  format = CLIFormat(data_format, export_file_ext)
168
182
 
169
183
  from .bill_teams import (
@@ -214,7 +228,7 @@ def generate_invoice(
214
228
  typer.Option(
215
229
  "--export",
216
230
  "-e",
217
- help=docs["invoice_export"] + elapi_docs["export_details"],
231
+ help=docs["invoice_export"] + docs["export_details"],
218
232
  is_flag=True,
219
233
  is_eager=True,
220
234
  show_default=False,
@@ -1,3 +1,3 @@
1
- from .export import Export, ExportValidator # noqa: F401
1
+ from .export import Export, ExportPathValidator # noqa: F401
2
2
  from .get_information import Information, RecursiveInformation # noqa: F401
3
3
  from .get_location_from_headers import get_location_from_headers # noqa: F401
@@ -7,19 +7,29 @@ from typer.core import TyperGroup
7
7
  from ...configuration import APP_NAME, DEFAULT_EXPORT_DATA_FORMAT
8
8
  from ...loggers import Logger
9
9
  from ...path import ProperPath
10
+ from ...validators import Exit
10
11
 
11
12
  logger = Logger()
12
13
 
13
14
 
14
15
  class CLIExport:
15
16
  def __new__(
16
- cls, data_format: Optional[str] = None, export_dest: Optional[str] = None
17
+ cls,
18
+ data_format: Optional[str] = None,
19
+ export_dest: Optional[str] = None,
20
+ can_overwrite: bool = False,
17
21
  ):
18
22
  from collections import namedtuple
19
23
  from ...validators import Validate
20
- from .export import ExportValidator
24
+ from .export import ExportPathValidator
21
25
 
22
- validate_export = Validate(ExportValidator(export_dest))
26
+ try:
27
+ validate_export = Validate(
28
+ ExportPathValidator(export_dest, can_overwrite=can_overwrite)
29
+ )
30
+ except ValueError as e:
31
+ logger.error(e)
32
+ raise Exit(1)
23
33
  export_dest: ProperPath = validate_export.get()
24
34
 
25
35
  _export_file_ext: str = (
@@ -74,22 +74,56 @@ class Export:
74
74
  )
75
75
 
76
76
 
77
- class ExportValidator(PathValidator):
77
+ class ExportPathValidator(PathValidator):
78
78
  def __init__(
79
- self, /, export_path: Union[Iterable, Union[None, str, ProperPath, Path]]
79
+ self,
80
+ /,
81
+ export_path: Union[Iterable, Union[None, str, ProperPath, Path]],
82
+ can_overwrite: bool = False,
80
83
  ):
81
84
  self.export_path = export_path
85
+ self.can_overwrite = can_overwrite
82
86
  super().__init__(export_path)
83
87
 
88
+ @property
89
+ def can_overwrite(self) -> bool:
90
+ return self._can_overwrite
91
+
92
+ @can_overwrite.setter
93
+ def can_overwrite(self, value: bool):
94
+ if not isinstance(value, bool):
95
+ raise ValueError("can_overwrite attribute must be a boolean!")
96
+ self._can_overwrite = value
97
+
84
98
  def validate(self) -> ProperPath:
85
99
  from ...configuration import APP_NAME, EXPORT_DIR
100
+ from ...styles import stdin_console, NoteText
86
101
 
87
102
  if self.export_path is not None:
88
103
  try:
89
- return ProperPath(super().validate(), err_logger=logger)
104
+ path = ProperPath(super().validate(), err_logger=logger)
90
105
  except ValidationError:
91
106
  logger.warning(
92
107
  f"--export path '{self.export_path}' couldn't be validated! "
93
108
  f"{APP_NAME} will use fallback export location."
94
109
  )
110
+ else:
111
+ if (
112
+ path.kind == "file"
113
+ and path.expanded.exists()
114
+ and path.expanded not in super()._self_created_files
115
+ and not self.can_overwrite
116
+ ):
117
+ logger.warning(
118
+ f"--export path '{self.export_path}' already exists! "
119
+ f"{APP_NAME} will use fallback export location."
120
+ )
121
+ stdin_console.print(
122
+ NoteText(
123
+ "Use '--overwrite' to force '--export' to write to an existing file.\n",
124
+ stem="Note",
125
+ )
126
+ )
127
+ return ProperPath(EXPORT_DIR, err_logger=logger)
128
+ return path
95
129
  return ProperPath(EXPORT_DIR, err_logger=logger)
@@ -48,6 +48,18 @@ __PARAMETERS__doc__ = {
48
48
  "to some external formatting program like `less`. "
49
49
  "If an unsupported format value is provided then the output format "
50
50
  f"falls back to `{DEFAULT_EXPORT_DATA_FORMAT.upper()}`.",
51
+ "export_details": f"- If _'--export'_ is passed without any following value, then it acts as a flag, and "
52
+ f"`export_dir` value from configuration file is used. "
53
+ f"It should be clear that `export_dir` in a configuration file only accepts a directory path.\n"
54
+ f"- If a directory path is provided as a value, "
55
+ f"i.e., _'--export \<path/to/directory\>'_, then that path is used instead. "
56
+ f"When the path is a directory, "
57
+ f"the file name is auto-generated using the following scheme: *'DATE_HHMMSS_\<FUNCTION\>.EXT'*.\n"
58
+ f"- If a file path is passed, i.e., _'--export <path/to/file.json>'_, "
59
+ f"then data is simply exported to that file. This allows custom file name scheme. "
60
+ f"If _--format/-F_ is absent, then {APP_NAME} can use the file extension as the data format. "
61
+ f"If _--format/-F_ is also present, then file extension is ignored, "
62
+ f"and --format value takes precedence.\n",
51
63
 
52
64
 
53
65
  }
@@ -42,13 +42,19 @@ def get(
42
42
  typer.Option(
43
43
  "--export",
44
44
  "-e",
45
- help=elapi_docs["export"] + elapi_docs["export_details"],
45
+ help=elapi_docs["export"] + docs["export_details"],
46
46
  is_flag=True,
47
47
  is_eager=True,
48
48
  show_default=False,
49
49
  ),
50
50
  ] = False,
51
51
  _export_dest: Annotated[Optional[str], typer.Argument(hidden=True)] = None,
52
+ export_overwrite: Annotated[
53
+ bool,
54
+ typer.Option(
55
+ "--overwrite", help=elapi_docs["export_overwrite"], show_default=False
56
+ ),
57
+ ] = False,
52
58
  ) -> dict:
53
59
  """
54
60
  Read or download an experiment.
@@ -71,7 +77,9 @@ def get(
71
77
  logger.error(e)
72
78
  raise typer.Exit(1)
73
79
  else:
74
- data_format, export_dest, export_file_ext = CLIExport(data_format, _export_dest)
80
+ data_format, export_dest, export_file_ext = CLIExport(
81
+ data_format, _export_dest, export_overwrite
82
+ )
75
83
  format = CLIFormat(data_format, export_file_ext)
76
84
 
77
85
  if isinstance(format, formats.BinaryFormat):
@@ -81,7 +89,7 @@ def get(
81
89
  f"Data will be exported."
82
90
  )
83
91
  response = FixedExperimentEndpoint().get(
84
- experiment_id, query={"format": data_format}
92
+ experiment_id, query={"format": data_format.lower()}
85
93
  )
86
94
  formatted_data = format(response_data := response.content)
87
95
  else:
@@ -248,13 +256,19 @@ def download_attachment(
248
256
  typer.Option(
249
257
  "--export",
250
258
  "-e",
251
- help=elapi_docs["export"] + elapi_docs["export_details"],
259
+ help=elapi_docs["export"] + docs["export_details"],
252
260
  is_flag=True,
253
261
  is_eager=True,
254
262
  show_default=False,
255
263
  ),
256
264
  ] = False,
257
265
  _export_dest: Annotated[Optional[str], typer.Argument(hidden=True)] = None,
266
+ export_overwrite: Annotated[
267
+ bool,
268
+ typer.Option(
269
+ "--overwrite", help=elapi_docs["export_overwrite"], show_default=False
270
+ ),
271
+ ] = False,
258
272
  ) -> None:
259
273
  """
260
274
  Download an attachment from an experiment.
@@ -289,7 +303,7 @@ def download_attachment(
289
303
  raise typer.Exit(1)
290
304
  else:
291
305
  data_format, export_dest, export_file_ext = CLIExport(
292
- attachment_extension, _export_dest
306
+ attachment_extension, _export_dest, export_overwrite
293
307
  )
294
308
  _is_real_id = attachment_id == attachment_real_id
295
309
  if export:
@@ -8,5 +8,5 @@ from .base import (
8
8
  CriticalValidationError,
9
9
  )
10
10
  from .identity import HostIdentityValidator
11
- from .path import PathValidator
11
+ from .path import PathValidator, PathValidationError
12
12
  from .permission import PermissionValidator, APITokenRWValidator
@@ -2,17 +2,10 @@ import sys
2
2
  from abc import abstractmethod, ABC
3
3
  from typing import Any
4
4
 
5
- import typer
6
-
7
5
 
8
6
  class ValidationError(Exception): ...
9
7
 
10
8
 
11
- class RuntimeValidationError(typer.Exit):
12
- def __init__(self, *args) -> None:
13
- super().__init__(*args or (1,)) # default error code is always 1
14
-
15
-
16
9
  class Exit(BaseException):
17
10
  if (
18
11
  hasattr(sys, "ps1")
@@ -32,7 +25,12 @@ class Exit(BaseException):
32
25
  return super().__new__(cls, *args, **kwargs) # cls == CriticalValidationError
33
26
 
34
27
 
35
- class CriticalValidationError(Exit): ...
28
+ class RuntimeValidationError(Exit, ValidationError):
29
+ def __init__(self, *args) -> None:
30
+ super().__init__(*args or (1,)) # default error code is always 1
31
+
32
+
33
+ class CriticalValidationError(Exit, ValidationError): ...
36
34
 
37
35
 
38
36
  class Validator(ABC):
@@ -1,16 +1,29 @@
1
1
  import string
2
2
  from pathlib import Path
3
3
  from random import choices
4
- from typing import Union, Iterable
4
+ from typing import Union, Iterable, Optional
5
5
 
6
6
  from .base import Validator, ValidationError
7
7
  from ..path import ProperPath
8
8
 
9
9
 
10
+ class PathValidationError(ValidationError):
11
+ def __init__(self, *args) -> None:
12
+ super().__init__(*args)
13
+ self.errno: Optional[int] = None
14
+ # Unlike OSError errno here is an instance attribute instead a class attribute.
15
+ # This ensures broad use of errno with PathValidationError in the future.
16
+
17
+ def __call__(self, *args):
18
+ super().__init__(*args)
19
+ return self
20
+
21
+
10
22
  class PathValidator(Validator):
11
23
  def __init__(
12
24
  self,
13
25
  path: Union[Iterable, Union[None, str, ProperPath, Path]],
26
+ retain_created_file: bool = True,
14
27
  **kwargs,
15
28
  ):
16
29
  from ..loggers import Logger
@@ -20,6 +33,8 @@ class PathValidator(Validator):
20
33
  self.TMP_FILE = (
21
34
  f".tmp_{''.join(choices(string.ascii_lowercase + string.digits, k=16))}"
22
35
  )
36
+ self.retain_created_file = retain_created_file
37
+ self.__self_created_files: list = []
23
38
 
24
39
  @property
25
40
  def path(self):
@@ -38,7 +53,13 @@ class PathValidator(Validator):
38
53
  else:
39
54
  self._path = (value,) if isinstance(value, str) else value
40
55
 
56
+ @property
57
+ def _self_created_files(self):
58
+ return self.__self_created_files
59
+
41
60
  def validate(self):
61
+ errno: Optional[int] = None
62
+ _self_created_file: bool = False
42
63
  for p in self.path:
43
64
  if not isinstance(p, ProperPath):
44
65
  try:
@@ -51,7 +72,10 @@ class PathValidator(Validator):
51
72
  else p
52
73
  )
53
74
  try:
54
- p.create()
75
+ if not p.expanded.exists():
76
+ p.create()
77
+ self._self_created_files.append(p.expanded)
78
+ _self_created_file = True
55
79
  with p_child.open(mode="ba+") as f:
56
80
  f.write(
57
81
  b"\x06"
@@ -63,9 +87,25 @@ class PathValidator(Validator):
63
87
  continue # It'd not be possible to read from those files.
64
88
  f.seek(f.tell() - 1)
65
89
  f.truncate()
66
- except (p.PathException, p_child.PathException, ValueError, AttributeError):
90
+ except (
91
+ p.PathException,
92
+ p_child.PathException,
93
+ ValueError,
94
+ AttributeError,
95
+ ) as e:
96
+ errno = getattr(e, "errno", None)
67
97
  continue
68
98
  else:
69
- p_child.remove() if p.kind == "dir" else ...
99
+ if p.kind == "dir":
100
+ p_child.remove()
101
+ if (
102
+ not self.retain_created_file
103
+ and _self_created_file
104
+ and p.kind == "file"
105
+ and p.expanded.stat().st_size == 0
106
+ ):
107
+ p.remove()
70
108
  return p.expanded
71
- raise ValidationError("Given path(s) could not be validated!")
109
+ validation_error = PathValidationError()
110
+ validation_error.errno = errno
111
+ raise validation_error("Given path(s) could not be validated!")
@@ -1 +0,0 @@
1
- 1.0.7-dev2
File without changes
File without changes
File without changes