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.
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/PKG-INFO +3 -3
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/pyproject.toml +2 -3
- elapi-1.0.7.dev3/src/elapi/VERSION +1 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/cli/doc.py +4 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/cli/elapi.py +7 -1
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/configuration/config.py +19 -10
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/configuration/log_file.py +1 -1
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/path.py +15 -11
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/_doc.py +14 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/cli.py +19 -5
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/commons/__init__.py +1 -1
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/commons/cli_helpers.py +13 -3
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/commons/export.py +37 -3
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/experiments/_doc.py +12 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/experiments/cli.py +19 -5
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/validators/__init__.py +1 -1
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/validators/base.py +6 -8
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/validators/path.py +45 -5
- elapi-1.0.7.dev2/src/elapi/VERSION +0 -1
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/LICENSE +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/README.md +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/__init__.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/_exceptions.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/_names.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/api.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/cli/__init__.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/cli/__main__.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/cli/_plugin_handler.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/configuration/__init__.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/configuration/_config_history.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/endpoint.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/loggers/__init__.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/loggers/base.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/loggers/handlers/__init__.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/loggers/handlers/file.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/loggers/handlers/stderr.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/__init__.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/__init__.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/bill_teams.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/format.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/invoice.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/bill_teams/validator.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/commons/get_information.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/commons/get_location_from_headers.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/experiments/__init__.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/experiments/experiments.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/experiments/formats.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/show_config.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/version.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/__init__.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/_markdown_doc.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/_missing.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/base.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/colors.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/format.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/styles/highlight.py +0 -0
- {elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/validators/identity.py +0 -0
- {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.
|
|
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:
|
|
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
|
|
3
|
+
version = "1.0.7.dev3"
|
|
4
4
|
description = "elAPI is a powerful, extensible API client for eLabFTW."
|
|
5
5
|
authors = [
|
|
6
|
-
"Alexander Haller <
|
|
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(
|
|
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,
|
|
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
|
|
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 '
|
|
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([
|
|
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
|
-
(
|
|
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:
|
|
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
|
|
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
|
-
|
|
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"] +
|
|
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(
|
|
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"] +
|
|
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"] +
|
|
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,
|
|
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,
|
|
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
|
|
24
|
+
from .export import ExportPathValidator
|
|
21
25
|
|
|
22
|
-
|
|
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
|
|
77
|
+
class ExportPathValidator(PathValidator):
|
|
78
78
|
def __init__(
|
|
79
|
-
self,
|
|
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
|
-
|
|
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"] +
|
|
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(
|
|
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"] +
|
|
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:
|
|
@@ -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
|
|
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.
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{elapi-1.0.7.dev2 → elapi-1.0.7.dev3}/src/elapi/plugins/commons/get_location_from_headers.py
RENAMED
|
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
|