fair-cli 0.9.9__tar.gz → 0.10.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.
- fair_cli-0.10.0/CHANGELOG.md +86 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/CITATION.cff +2 -2
- {fair_cli-0.9.9 → fair_cli-0.10.0}/PKG-INFO +3 -6
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/cli.py +13 -4
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/common.py +1 -1
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/identifiers.py +21 -6
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/register.py +1 -1
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/registry/requests.py +9 -1
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/registry/sync.py +53 -17
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/session.py +29 -20
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/staging.py +4 -6
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/user_config/__init__.py +106 -45
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/user_config/globbing.py +24 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/utilities.py +13 -3
- {fair_cli-0.9.9 → fair_cli-0.10.0}/pyproject.toml +9 -3
- fair_cli-0.9.9/CHANGELOG.md +0 -33
- {fair_cli-0.9.9 → fair_cli-0.10.0}/LICENSE +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/README.md +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/__init__.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/configuration/__init__.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/configuration/validation.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/exceptions.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/files.txt +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/history.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/logging.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/registry/__init__.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/registry/file_formats.json +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/registry/file_types.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/registry/server.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/registry/storage.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/registry/versioning.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/run.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/templates/__init__.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/templates/config.jinja +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/templates/hist.jinja +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/testing.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/user_config/validation.py +0 -0
- {fair_cli-0.9.9 → fair_cli-0.10.0}/fair/virtualenv.py +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# 2026-10-03 [v0.10.0](https://github.com/FAIRDataPipeline/FAIR-CLI/releases/tag/v0.10.0)
|
|
2
|
+
|
|
3
|
+
## Changed behaviour
|
|
4
|
+
- `fair remote add` takes the remote's token file with `--token FILE`, and prompts for it if omitted.
|
|
5
|
+
The command never worked before: the label was stored as the token path.
|
|
6
|
+
- `fair run --ci` prepares the job directory and prints it without running the script, as documented.
|
|
7
|
+
- `fair init --ci` leaves an existing `.fair/` alone, as `fair init` does; it used to delete it, data
|
|
8
|
+
store included. Use `fair purge` to start afresh.
|
|
9
|
+
- Wildcards: each `*` matches one segment of a name (not `/`), anchored at both ends, in `read:` and
|
|
10
|
+
`write:` alike, matching DataPipeline.jl. `data_product/*` no longer matches `data_product/1/2`.
|
|
11
|
+
- `${{GIT_TAG}}` is the nearest tag in the history of the current commit (`git describe`), not the
|
|
12
|
+
last tag by name; with no such tag it is an error rather than a crash.
|
|
13
|
+
- `run_metadata.shell` now chooses the interpreter the script runs with.
|
|
14
|
+
|
|
15
|
+
## Fixed
|
|
16
|
+
- `write:` entries with a wildcard were dropped from the working config (#140). The pattern is now kept
|
|
17
|
+
for new names, and existing matches are written as the pattern entry describes them (its `use: version`,
|
|
18
|
+
`file_type`, `description`).
|
|
19
|
+
- `fair push` from a project whose data store was not the local registry's first storage root recorded
|
|
20
|
+
its files on the remote under the pusher's `file://` path (data-registry #229).
|
|
21
|
+
- `fair pull` of a `read:` entry from a remote registry failed.
|
|
22
|
+
- A DOI whose publisher refuses automated requests (e.g. Wiley, HTTP 403) was rejected as an invalid
|
|
23
|
+
identifier. An identifier that resolves is now accepted; one that does not (404) is still refused.
|
|
24
|
+
- `${{DATETIME-<format>}}` always failed with "Failed to parse formatted datetime variable".
|
|
25
|
+
- Substituting a path with backslashes, such as `${{CONFIG_DIR}}` on Windows, failed `fair pull` and `fair run`
|
|
26
|
+
(#267). Variables are now substituted into the configuration's values, so any text is inserted as it is.
|
|
27
|
+
- `fair remote remove` crashed.
|
|
28
|
+
- A ROR or GRID ID the ROR API could not parse (e.g. one containing `!`) crashed with `KeyError`
|
|
29
|
+
instead of being reported as not found.
|
|
30
|
+
- Run from a subdirectory of a project, commands lost configuration changes and left a stale session
|
|
31
|
+
file, so the next command did not start the registry and `fair registry stop` needed `--force`.
|
|
32
|
+
- Staging ignored the configured local registry and always used port 8000.
|
|
33
|
+
- A download that failed with an HTTP error registered the error page as the data.
|
|
34
|
+
- `fair run` could crash on Windows when the model printed a character the console cannot encode.
|
|
35
|
+
- `fair run` in a git repository without the configured remote gave a bare `IndexError`; it now says
|
|
36
|
+
which remote is missing, or reads `run_metadata: remote_repo:` if given.
|
|
37
|
+
- `fair init` in a subdirectory of a FAIR repository named it as `'True'`.
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
- CI ran no tests: an unused dev dependency (`pylama`) crashed `pytest --markers`, leaving the test loop
|
|
41
|
+
empty. It is removed, and a failure to list the tests now fails the job; all 8 jobs run the full suite.
|
|
42
|
+
- numpy is chosen per Python version (>= 2.3 from 3.11), so Python 3.14 gets a wheel, not a source build.
|
|
43
|
+
- Download tests use a local HTTP server rather than github.com.
|
|
44
|
+
- `test_configuration.py::test_local_config_query` is marked `xfail`: it has never passed, and its intent
|
|
45
|
+
is unknown.
|
|
46
|
+
|
|
47
|
+
# 2026-09-22 [v0.9.9](https://github.com/FAIRDataPipeline/FAIR-CLI/releases/tag/v0.9.9)
|
|
48
|
+
- Requires Python >= 3.10, in step with data-registry v1.2.2.
|
|
49
|
+
- `${{RUN_ID}}` in a `write:` name is left for the language API to fill in at `finalise`.
|
|
50
|
+
- ROR/GRID organisation lookups failed after the ROR API moved to its v2 schema.
|
|
51
|
+
|
|
52
|
+
# Between v0.2.3 and v0.9.8
|
|
53
|
+
Not recorded release by release; see the [releases](https://github.com/FAIRDataPipeline/FAIR-CLI/releases)
|
|
54
|
+
and the git log.
|
|
55
|
+
- Fix virtual environment bug in `fair` binaries where `venv` assumes the main executable is `python`.
|
|
56
|
+
- Registration of `author` and `namespace` objects.
|
|
57
|
+
- Allow specification of tag to install registry from during `fair registry install`.
|
|
58
|
+
- Wildcard '*' parsing introduced for data products.
|
|
59
|
+
- Ability to `push` to a registry added.
|
|
60
|
+
- Added `--dirty` option to `fair run` to allow running with uncommitted changes.
|
|
61
|
+
- Added `config.yaml` file validation.
|
|
62
|
+
- Added initialisation from existing registry.
|
|
63
|
+
- Switch to setting port not local URI during initialisation.
|
|
64
|
+
- Added option to specify port on `fair registry start`.
|
|
65
|
+
- Added `cli-config.yaml` file validation during initialisation.
|
|
66
|
+
- Added `fair reset` to list of commands.
|
|
67
|
+
- Add tab completion for staging data products.
|
|
68
|
+
|
|
69
|
+
# 2021-11-17 [v0.2.3](https://github.com/FAIRDataPipeline/FAIR-CLI/releases/tag/v0.2.3)
|
|
70
|
+
- Move handling of the user `config.yaml` file to a separate class `JobConfiguration`.
|
|
71
|
+
- Added various fixes to improve functionality within Windows.
|
|
72
|
+
- Move registry installation from script execution to internal function which sets up virtual environment etc.
|
|
73
|
+
- Added a test suite to the project.
|
|
74
|
+
- Added additional recognised identifiers for author setup from an organisation GRID and ROR.
|
|
75
|
+
|
|
76
|
+
# 2021-10-06 [v0.2.2](https://github.com/FAIRDataPipeline/FAIR-CLI/releases/tag/v0.2.2)
|
|
77
|
+
- Update to package metadata for PyPi
|
|
78
|
+
|
|
79
|
+
# 2021-10-06 [v0.2.1](https://github.com/FAIRDataPipeline/FAIR-CLI/releases/tag/v0.2.1)
|
|
80
|
+
- Automatic starter `config.yaml` generation.
|
|
81
|
+
- Local and Global CLI configurations.
|
|
82
|
+
- Start/stop local registry either explicitly or during synchronisations.
|
|
83
|
+
- Run logs available via git-like interface.
|
|
84
|
+
- Added ability to add/remove files.
|
|
85
|
+
- Repository style handling, acts like another git-like tool per project.
|
|
86
|
+
- Creation of an interface for `fair` using `click`.
|
|
@@ -26,7 +26,7 @@ authors:
|
|
|
26
26
|
affiliation: University of Glasgow
|
|
27
27
|
orcid: https://orcid.org/0000-0002-4424-9890
|
|
28
28
|
|
|
29
|
-
date-released:
|
|
29
|
+
date-released: 2026-10-03
|
|
30
30
|
keywords:
|
|
31
31
|
- FAIR Data Pipeline
|
|
32
32
|
- FAIR
|
|
@@ -36,4 +36,4 @@ license: BSD-2-Clause
|
|
|
36
36
|
message: If you use this software, please cite it using these metadata.
|
|
37
37
|
repository-code: https://github.com/FAIRDataPipeline/FAIR-CLI/
|
|
38
38
|
title: "The FAIR Data Pipeline command line tool"
|
|
39
|
-
version: 0.
|
|
39
|
+
version: 0.10.0
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: fair-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Synchronization interface for the SCRC FAIR Data Pipeline registry
|
|
5
|
+
Home-page: https://www.fairdatapipeline.org/
|
|
5
6
|
License: BSD-2-Clause
|
|
6
|
-
License-File: LICENSE
|
|
7
7
|
Keywords: FAIR Data Pipeline,FAIR,Data Management,Provenance
|
|
8
8
|
Author: Richard Reeve
|
|
9
9
|
Author-email: richard.reeve@glasgow.ac.uk
|
|
@@ -20,8 +20,6 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.15
|
|
25
23
|
Classifier: Topic :: Database :: Front-Ends
|
|
26
24
|
Classifier: Topic :: Scientific/Engineering
|
|
27
25
|
Provides-Extra: all
|
|
@@ -40,7 +38,6 @@ Requires-Dist: simplejson (>=3.17.5,<4.0.0)
|
|
|
40
38
|
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
41
39
|
Requires-Dist: validators (>=0.18.2,<0.19.0)
|
|
42
40
|
Project-URL: Documentation, https://www.fairdatapipeline.org/docs/interface/fdp/
|
|
43
|
-
Project-URL: Homepage, https://www.fairdatapipeline.org/
|
|
44
41
|
Project-URL: Issue Tracker, https://github.com/FAIRDataPipeline/FAIR-CLI/issues
|
|
45
42
|
Project-URL: Repository, https://github.com/FAIRDataPipeline/FAIR-CLI
|
|
46
43
|
Description-Content-Type: text/markdown
|
|
@@ -616,9 +616,16 @@ def remote(ctx, verbose: bool = False, debug: bool = False):
|
|
|
616
616
|
|
|
617
617
|
|
|
618
618
|
@remote.command()
|
|
619
|
-
@click.argument("options", nargs=-1)
|
|
619
|
+
@click.argument("options", nargs=-1, required=True)
|
|
620
|
+
@click.option(
|
|
621
|
+
"--token",
|
|
622
|
+
"token_file",
|
|
623
|
+
help="File containing the API token for the remote registry",
|
|
624
|
+
prompt="Remote API Token File",
|
|
625
|
+
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
|
626
|
+
)
|
|
620
627
|
@click.option("--debug/--no-debug", help="Run in debug mode", default=False)
|
|
621
|
-
def add(options: typing.List[str], debug: bool) -> None:
|
|
628
|
+
def add(options: typing.List[str], token_file: str, debug: bool) -> None:
|
|
622
629
|
"""Add a remote registry URL with option to give it a label if multiple
|
|
623
630
|
remotes may be used.
|
|
624
631
|
|
|
@@ -628,13 +635,15 @@ def add(options: typing.List[str], debug: bool) -> None:
|
|
|
628
635
|
size 1 or 2 list containing either:
|
|
629
636
|
- label, url
|
|
630
637
|
- url
|
|
638
|
+
token_file : str
|
|
639
|
+
file containing the API token for the remote registry
|
|
631
640
|
"""
|
|
632
641
|
_url = options[1] if len(options) > 1 else options[0]
|
|
633
642
|
_label = options[0] if len(options) > 1 else "origin"
|
|
634
643
|
|
|
635
644
|
try:
|
|
636
645
|
with fdp_session.FAIR(os.getcwd(), debug=debug) as fair_session:
|
|
637
|
-
fair_session.add_remote(_url, _label)
|
|
646
|
+
fair_session.add_remote(_url, token_file, _label)
|
|
638
647
|
except fdp_exc.FAIRCLIException as e:
|
|
639
648
|
e.err_print()
|
|
640
649
|
if e.level.lower() == "error":
|
|
@@ -654,7 +663,7 @@ def remove(label: str, debug: bool) -> None:
|
|
|
654
663
|
"""
|
|
655
664
|
try:
|
|
656
665
|
with fdp_session.FAIR(os.getcwd(), debug=debug) as fair_session:
|
|
657
|
-
fair_session.
|
|
666
|
+
fair_session.remove_remote(label)
|
|
658
667
|
except fdp_exc.FAIRCLIException as e:
|
|
659
668
|
e.err_print()
|
|
660
669
|
if e.level.lower() == "error":
|
|
@@ -19,7 +19,7 @@ Members
|
|
|
19
19
|
FAIR_FOLDER - name for FAIR local repository directory
|
|
20
20
|
|
|
21
21
|
Functions
|
|
22
|
-
|
|
22
|
+
---------
|
|
23
23
|
registry_home - returns the location of the local data registry
|
|
24
24
|
find_fair_root - returns the closest '.fair' directory in the upper hierarchy
|
|
25
25
|
find_git_root - returns the closest '.git' directory
|
|
@@ -192,10 +192,12 @@ def check_ror(ror: str) -> typing.Dict:
|
|
|
192
192
|
|
|
193
193
|
def check_grid(grid_id: str) -> typing.Dict:
|
|
194
194
|
"""Checks if valid GRID ID using ROR (https://ror.org/) public api
|
|
195
|
+
|
|
195
196
|
Parameters
|
|
196
197
|
----------
|
|
197
198
|
grid_id : str
|
|
198
199
|
GRID ID to be checked
|
|
200
|
+
|
|
199
201
|
Returns
|
|
200
202
|
-------
|
|
201
203
|
typing.Dict
|
|
@@ -230,12 +232,16 @@ def _check_generic_ror(id: str) -> typing.Dict:
|
|
|
230
232
|
logger.debug(f"{_url} Responded with {_response.status_code}")
|
|
231
233
|
return _result_dict
|
|
232
234
|
|
|
233
|
-
|
|
235
|
+
# A query ROR cannot parse (e.g. one containing "!") still returns 200, with
|
|
236
|
+
# "errors" in place of results
|
|
237
|
+
_items = _response.json().get("items")
|
|
238
|
+
if not _items:
|
|
239
|
+
logger.debug(f"{_url} returned no results: {_response.json().get('errors')}")
|
|
234
240
|
return _result_dict
|
|
235
241
|
|
|
236
|
-
_id =
|
|
242
|
+
_id = _items[0]["id"]
|
|
237
243
|
# The ROR API lists every name of an organisation, marking the one to show as "ror_display"
|
|
238
|
-
_names =
|
|
244
|
+
_names = _items[0]["names"]
|
|
239
245
|
_name = next(
|
|
240
246
|
(n["value"] for n in _names if "ror_display" in n["types"]),
|
|
241
247
|
_names[0]["value"],
|
|
@@ -251,7 +257,9 @@ def _check_generic_ror(id: str) -> typing.Dict:
|
|
|
251
257
|
def check_id_permitted(identifier: str, retries: int = 5) -> bool:
|
|
252
258
|
"""Check a user provided identifier is permitted
|
|
253
259
|
|
|
254
|
-
This ID is expected to be a valid URL
|
|
260
|
+
This ID is expected to be a valid URL. Permitted means that it
|
|
261
|
+
resolves: the URL, after any redirects, is served, or it redirects to a
|
|
262
|
+
page that refuses access (401/403), as a publisher behind a DOI may.
|
|
255
263
|
|
|
256
264
|
Parameters
|
|
257
265
|
----------
|
|
@@ -274,9 +282,16 @@ def check_id_permitted(identifier: str, retries: int = 5) -> bool:
|
|
|
274
282
|
headers = {}
|
|
275
283
|
if fake_agent:
|
|
276
284
|
headers = {"User-Agent": str(UserAgent().chrome)}
|
|
277
|
-
requests.get(
|
|
285
|
+
_response = requests.get(
|
|
278
286
|
identifier, verify=False, allow_redirects=True, headers=headers
|
|
279
|
-
)
|
|
287
|
+
)
|
|
288
|
+
# Some publishers answer 403 to any automated request whatever
|
|
289
|
+
# the user agent, so a refusal reached through a redirect (e.g.
|
|
290
|
+
# from doi.org) still means the identifier resolved. A 404
|
|
291
|
+
# anywhere is still rejected.
|
|
292
|
+
if _response.history and _response.status_code in (401, 403):
|
|
293
|
+
return True
|
|
294
|
+
_response.raise_for_status()
|
|
280
295
|
return True
|
|
281
296
|
except (
|
|
282
297
|
requests.exceptions.MissingSchema,
|
|
@@ -506,6 +506,11 @@ def download_file(url: str, chunk_size: int = 8192) -> str:
|
|
|
506
506
|
-------
|
|
507
507
|
str
|
|
508
508
|
path of downloaded temporary file
|
|
509
|
+
|
|
510
|
+
Raises
|
|
511
|
+
------
|
|
512
|
+
requests.HTTPError
|
|
513
|
+
if the server answers with an error status
|
|
509
514
|
"""
|
|
510
515
|
# Save the data to a temporary file so we can calculate the hash
|
|
511
516
|
_file = tempfile.NamedTemporaryFile(delete=False)
|
|
@@ -528,12 +533,15 @@ def download_file(url: str, chunk_size: int = 8192) -> str:
|
|
|
528
533
|
response = requests.get(
|
|
529
534
|
url, allow_redirects=True, verify=False, headers=headers
|
|
530
535
|
)
|
|
531
|
-
open(_fname, mode="wb").write(response.content)
|
|
532
536
|
except Exception as e:
|
|
533
537
|
raise fdp_exc.FAIRCLIException(
|
|
534
538
|
f"Failed to download file '{url}'"
|
|
535
539
|
f" due to connection error: {traceback.format_exc()}"
|
|
536
540
|
) from e
|
|
541
|
+
# Outside the try, so that callers can tell an HTTP error from a
|
|
542
|
+
# failure to connect, and an error page is never saved as the data
|
|
543
|
+
response.raise_for_status()
|
|
544
|
+
open(_fname, mode="wb").write(response.content)
|
|
537
545
|
|
|
538
546
|
return _fname
|
|
539
547
|
|
|
@@ -226,6 +226,35 @@ def sync_dependency_chain(
|
|
|
226
226
|
return _new_urls
|
|
227
227
|
|
|
228
228
|
|
|
229
|
+
# Whether a storage_root record is on the pushing machine's file system
|
|
230
|
+
def _is_local_root(storage_root: typing.Dict) -> bool:
|
|
231
|
+
return storage_root.get("root", "").startswith("file://")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# URL of a registry's own data store: the storage_root whose root is the
|
|
235
|
+
# registry's data/ path, beside its api/, from which it serves stored files.
|
|
236
|
+
# A registry whose site URL differs from its API's address records that root
|
|
237
|
+
# under another name; its first storage_root is then taken, as set_site_info
|
|
238
|
+
# creates the data store first.
|
|
239
|
+
def _remote_data_store_url(registry_uri: str, token: str) -> str:
|
|
240
|
+
_registry_uri = fdp_util.check_trailing_slash(registry_uri)
|
|
241
|
+
_root = urllib.parse.urljoin(_registry_uri, "../data/")
|
|
242
|
+
_data_store = fdp_req.get(
|
|
243
|
+
registry_uri, "storage_root", token, params={"root": _root}
|
|
244
|
+
)
|
|
245
|
+
if _data_store:
|
|
246
|
+
return _data_store[0]["url"]
|
|
247
|
+
_fallback = urllib.parse.urljoin(_registry_uri, "storage_root/1/")
|
|
248
|
+
logger.warning(
|
|
249
|
+
"Registry '%s' has no storage root '%s'; assuming its data store is "
|
|
250
|
+
"'%s'. Its site URL may not match its address.",
|
|
251
|
+
registry_uri,
|
|
252
|
+
_root,
|
|
253
|
+
_fallback,
|
|
254
|
+
)
|
|
255
|
+
return _fallback
|
|
256
|
+
|
|
257
|
+
|
|
229
258
|
def _get_new_url(
|
|
230
259
|
origin_uri: str,
|
|
231
260
|
origin_token: str,
|
|
@@ -281,8 +310,9 @@ def _get_new_url(
|
|
|
281
310
|
# Make sure that a URL for the component does exist
|
|
282
311
|
if value not in new_urls:
|
|
283
312
|
raise fdp_exc.RegistryError(
|
|
284
|
-
f"Expected URL
|
|
285
|
-
f"'{
|
|
313
|
+
f"Expected a URL on '{dest_uri}' for component '{key}', "
|
|
314
|
+
f"whose value '{value}' should have been synchronised "
|
|
315
|
+
"before it."
|
|
286
316
|
)
|
|
287
317
|
|
|
288
318
|
# Retrieve from the new URLs the correct value and substitute
|
|
@@ -293,7 +323,6 @@ def _get_new_url(
|
|
|
293
323
|
# (as remote URL will never match local)
|
|
294
324
|
|
|
295
325
|
_obj_type = fdp_req.get_obj_type_from_url(object_url, token=origin_token)
|
|
296
|
-
_obj_id = fdp_req.get_obj_id_from_url(object_url)
|
|
297
326
|
|
|
298
327
|
_filters = {
|
|
299
328
|
k: v
|
|
@@ -311,20 +340,21 @@ def _get_new_url(
|
|
|
311
340
|
# If Public is true then any files will be uploaded to the remote registry
|
|
312
341
|
# If local_data_store is set we're pulling to a local registry
|
|
313
342
|
if public and not local_data_store:
|
|
314
|
-
#
|
|
315
|
-
#
|
|
316
|
-
|
|
317
|
-
#
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return _remote_storage_root_url
|
|
343
|
+
# A local data store is any root on the pusher's own file system,
|
|
344
|
+
# however many projects share the local registry; it is replaced by
|
|
345
|
+
# the remote data store. Other roots (e.g. https://github.com/) are
|
|
346
|
+
# pushed as they are.
|
|
347
|
+
if _obj_type == "storage_root" and _is_local_root(object_data):
|
|
348
|
+
return _remote_data_store_url(dest_uri, dest_token)
|
|
321
349
|
# If the current object is a storage_location
|
|
322
350
|
elif _obj_type == "storage_location":
|
|
323
|
-
# Again if the storage_root is
|
|
324
|
-
if (
|
|
325
|
-
fdp_req.
|
|
326
|
-
== "1"
|
|
351
|
+
# Again if the storage_root is a local data store
|
|
352
|
+
if _is_local_root(
|
|
353
|
+
fdp_req.url_get(object_data["storage_root"], origin_token)
|
|
327
354
|
):
|
|
355
|
+
_remote_storage_root_url = _remote_data_store_url(
|
|
356
|
+
dest_uri, dest_token
|
|
357
|
+
)
|
|
328
358
|
# Update the new object path and filter path
|
|
329
359
|
_new_obj_data["path"] = _filters["path"] = _new_obj_data["hash"]
|
|
330
360
|
# Update the new object storage_root
|
|
@@ -522,6 +552,11 @@ def sync_data_products(
|
|
|
522
552
|
)
|
|
523
553
|
result = result[0]
|
|
524
554
|
|
|
555
|
+
# Held separately because `result` becomes the external object below,
|
|
556
|
+
# and the file is fetched by the data product's own namespace, name
|
|
557
|
+
# and version
|
|
558
|
+
_data_product = result
|
|
559
|
+
|
|
525
560
|
result_object = fdp_req.url_get(result["object"], token=origin_token)
|
|
526
561
|
result_storage_location = fdp_req.url_get(
|
|
527
562
|
result_object["storage_location"], token=origin_token
|
|
@@ -544,7 +579,7 @@ def sync_data_products(
|
|
|
544
579
|
# If local_data_store assume we're syncing from remote to local
|
|
545
580
|
if local_data_store:
|
|
546
581
|
logger.debug("Retrieving files from remote registry data storage")
|
|
547
|
-
fetch_data_product(origin_token, local_data_store,
|
|
582
|
+
fetch_data_product(origin_token, local_data_store, _data_product)
|
|
548
583
|
# Else going from local to remote
|
|
549
584
|
else:
|
|
550
585
|
# If the storage location is public upload the files to object storage
|
|
@@ -1142,7 +1177,7 @@ def fetch_data_product(
|
|
|
1142
1177
|
_file_type = ""
|
|
1143
1178
|
|
|
1144
1179
|
_local_dir = os.path.join(
|
|
1145
|
-
local_data_store, _namespace["name"], data_product["
|
|
1180
|
+
local_data_store, _namespace["name"], data_product["name"]
|
|
1146
1181
|
)
|
|
1147
1182
|
|
|
1148
1183
|
os.makedirs(_local_dir, exist_ok=True)
|
|
@@ -1201,7 +1236,8 @@ def download_from_registry(registry_url: str, root: str, path: str) -> str:
|
|
|
1201
1236
|
logger.debug("Downloaded file from '%s' to temporary file", _download_url)
|
|
1202
1237
|
except requests.HTTPError as r_in:
|
|
1203
1238
|
raise fdp_exc.UserConfigError(
|
|
1204
|
-
f"Failed to fetch item '{_download_url}' with
|
|
1239
|
+
f"Failed to fetch item '{_download_url}' with status code "
|
|
1240
|
+
f"{r_in.response.status_code}"
|
|
1205
1241
|
) from r_in
|
|
1206
1242
|
|
|
1207
1243
|
return _temp_data_file
|
|
@@ -542,7 +542,10 @@ class FAIR:
|
|
|
542
542
|
|
|
543
543
|
self._session_config.write()
|
|
544
544
|
|
|
545
|
-
|
|
545
|
+
# A passive run only prepares the job directory, for the model to be
|
|
546
|
+
# run from it separately
|
|
547
|
+
if not passive:
|
|
548
|
+
self._session_config.execute()
|
|
546
549
|
|
|
547
550
|
self._post_job_breakdown(add_run=True)
|
|
548
551
|
|
|
@@ -754,11 +757,14 @@ class FAIR:
|
|
|
754
757
|
def remove_remote(self, label: str) -> None:
|
|
755
758
|
"""Remove a remote URL from the list of remotes by label"""
|
|
756
759
|
self.check_is_repo()
|
|
757
|
-
if
|
|
760
|
+
if (
|
|
761
|
+
"registries" not in self._local_config
|
|
762
|
+
or label not in self._local_config["registries"]
|
|
763
|
+
):
|
|
758
764
|
raise fdp_exc.CLIConfigurationError(
|
|
759
765
|
f"No such entry '{label}' in available remotes"
|
|
760
766
|
)
|
|
761
|
-
del self._local_config[label]
|
|
767
|
+
del self._local_config["registries"][label]
|
|
762
768
|
|
|
763
769
|
def modify_remote(self, label: str, url: str) -> None:
|
|
764
770
|
"""Update a remote URL for a given remote"""
|
|
@@ -1089,7 +1095,8 @@ class FAIR:
|
|
|
1089
1095
|
raise fdp_exc.CLIConfigurationError(
|
|
1090
1096
|
"Cannot generate user 'config.yaml'",
|
|
1091
1097
|
hint="You need to set the remote URL"
|
|
1092
|
-
" by running: \n\n\tfair remote add <url
|
|
1098
|
+
" by running: \n\n\tfair remote add <url>"
|
|
1099
|
+
" --token <token-file>\n",
|
|
1093
1100
|
)
|
|
1094
1101
|
|
|
1095
1102
|
with open(output_file_name, encoding="utf-8", mode="w") as f:
|
|
@@ -1132,11 +1139,17 @@ class FAIR:
|
|
|
1132
1139
|
|
|
1133
1140
|
_first_time = not os.path.exists(fdp_com.global_fdpconfig())
|
|
1134
1141
|
|
|
1142
|
+
# Also with --ci: the data store lives in .fair/, and the registry
|
|
1143
|
+
# still holds records pointing into it. 'fair purge' starts afresh.
|
|
1144
|
+
if os.path.exists(_fair_dir):
|
|
1145
|
+
if export_as:
|
|
1146
|
+
self._export_cli_configuration(export_as)
|
|
1147
|
+
return
|
|
1148
|
+
else:
|
|
1149
|
+
click.echo("FAIR repository is already initialised.")
|
|
1150
|
+
return
|
|
1151
|
+
|
|
1135
1152
|
if self._testing:
|
|
1136
|
-
if os.path.exists(_fair_dir):
|
|
1137
|
-
if platform.system() == "Windows":
|
|
1138
|
-
fdp_com.set_file_permissions(_fair_dir)
|
|
1139
|
-
shutil.rmtree(_fair_dir, onerror=fdp_com.remove_readonly)
|
|
1140
1153
|
using = fdp_test.create_configurations(
|
|
1141
1154
|
registry,
|
|
1142
1155
|
fdp_com.find_git_root(os.getcwd()),
|
|
@@ -1144,15 +1157,9 @@ class FAIR:
|
|
|
1144
1157
|
os.path.join(os.getcwd(), ".fair"),
|
|
1145
1158
|
)
|
|
1146
1159
|
|
|
1147
|
-
if
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
return
|
|
1151
|
-
else:
|
|
1152
|
-
click.echo("FAIR repository is already initialised.")
|
|
1153
|
-
return
|
|
1154
|
-
|
|
1155
|
-
if _existing := fdp_com.find_fair_root(self._session_loc) and not self._testing:
|
|
1160
|
+
if (
|
|
1161
|
+
_existing := fdp_com.find_fair_root(self._session_loc)
|
|
1162
|
+
) and not self._testing:
|
|
1156
1163
|
click.echo(
|
|
1157
1164
|
"A FAIR repository was initialised for this location at"
|
|
1158
1165
|
f" '{_existing}'"
|
|
@@ -1256,7 +1263,7 @@ class FAIR:
|
|
|
1256
1263
|
|
|
1257
1264
|
def close_session(self) -> None:
|
|
1258
1265
|
"""Upon exiting, dump all configurations to file"""
|
|
1259
|
-
if not
|
|
1266
|
+
if not fdp_com.find_fair_root(self._session_loc):
|
|
1260
1267
|
return
|
|
1261
1268
|
|
|
1262
1269
|
if self._session_id:
|
|
@@ -1266,10 +1273,12 @@ class FAIR:
|
|
|
1266
1273
|
)
|
|
1267
1274
|
os.remove(_cache_addr)
|
|
1268
1275
|
|
|
1269
|
-
if os.path.exists(fdp_com.
|
|
1276
|
+
if os.path.exists(os.path.dirname(fdp_com.global_fdpconfig())):
|
|
1270
1277
|
with open(fdp_com.global_fdpconfig(), encoding="utf-8", mode="w") as f:
|
|
1271
1278
|
yaml.dump(self._global_config, f)
|
|
1272
|
-
if os.path.exists(
|
|
1279
|
+
if os.path.exists(
|
|
1280
|
+
os.path.dirname(fdp_com.local_fdpconfig(self._session_loc))
|
|
1281
|
+
):
|
|
1273
1282
|
with open(
|
|
1274
1283
|
fdp_com.local_fdpconfig(self._session_loc), encoding="utf-8", mode="w"
|
|
1275
1284
|
) as f:
|
|
@@ -418,9 +418,8 @@ class Stager:
|
|
|
418
418
|
with open(self._staging_file, encoding="utf-8") as f:
|
|
419
419
|
_staging_dict = yaml.safe_load(f)
|
|
420
420
|
|
|
421
|
-
result = fdp_req.
|
|
422
|
-
|
|
423
|
-
fdp_req.local_token(),
|
|
421
|
+
result = fdp_req.get(
|
|
422
|
+
fdp_conf.get_local_uri(), "data_product", fdp_req.local_token()
|
|
424
423
|
)
|
|
425
424
|
|
|
426
425
|
for data_product in result:
|
|
@@ -441,9 +440,8 @@ class Stager:
|
|
|
441
440
|
with open(self._staging_file, encoding="utf-8") as f:
|
|
442
441
|
_staging_dict = yaml.safe_load(f)
|
|
443
442
|
|
|
444
|
-
result = fdp_req.
|
|
445
|
-
|
|
446
|
-
fdp_req.local_token(),
|
|
443
|
+
result = fdp_req.get(
|
|
444
|
+
fdp_conf.get_local_uri(), "code_run", fdp_req.local_token()
|
|
447
445
|
)
|
|
448
446
|
|
|
449
447
|
for code_run in result:
|
|
@@ -93,6 +93,28 @@ SHELLS: typing.Dict[str, str] = {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
|
|
96
|
+
# Apply a function to every string in a configuration's nested dicts and lists,
|
|
97
|
+
# keys included, returning the new structure
|
|
98
|
+
def _map_strings(item: typing.Any, func: typing.Callable) -> typing.Any:
|
|
99
|
+
if isinstance(item, str):
|
|
100
|
+
return func(item)
|
|
101
|
+
if isinstance(item, dict):
|
|
102
|
+
return {
|
|
103
|
+
_map_strings(k, func): _map_strings(v, func)
|
|
104
|
+
for k, v in item.items()
|
|
105
|
+
}
|
|
106
|
+
if isinstance(item, list):
|
|
107
|
+
return [_map_strings(v, func) for v in item]
|
|
108
|
+
return item
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Every string in a configuration's nested dicts and lists, keys included
|
|
112
|
+
def _strings_in(item: typing.Any) -> typing.List[str]:
|
|
113
|
+
_strings: typing.List[str] = []
|
|
114
|
+
_map_strings(item, lambda s: _strings.append(s) or s)
|
|
115
|
+
return _strings
|
|
116
|
+
|
|
117
|
+
|
|
96
118
|
class JobConfiguration(MutableMapping):
|
|
97
119
|
_logger = logging.getLogger("FAIRDataPipeline.ConfigYAML")
|
|
98
120
|
_block_types = ("register", "write", "read")
|
|
@@ -408,6 +430,15 @@ class JobConfiguration(MutableMapping):
|
|
|
408
430
|
f" wildcard '{block_entry[_obj_type]}'"
|
|
409
431
|
) from e
|
|
410
432
|
|
|
433
|
+
# The registry's own filter lets '*' match across '/'
|
|
434
|
+
_results_local = [
|
|
435
|
+
result
|
|
436
|
+
for result in _results_local
|
|
437
|
+
if fdp_glob.matches_wildcard(
|
|
438
|
+
block_entry[_obj_type], result[_search_key]
|
|
439
|
+
)
|
|
440
|
+
]
|
|
441
|
+
|
|
411
442
|
if _obj_type in ("namespace", "author"):
|
|
412
443
|
# If the object is a namespace or an author then there is no
|
|
413
444
|
# additional info in the registry so we can just add the entries
|
|
@@ -425,12 +456,26 @@ class JobConfiguration(MutableMapping):
|
|
|
425
456
|
)
|
|
426
457
|
|
|
427
458
|
elif _obj_type == "data_product":
|
|
428
|
-
_version = block_entry.get("
|
|
459
|
+
_version = block_entry.get("use", {}).get(
|
|
460
|
+
"version", block_entry.get("version", None)
|
|
461
|
+
)
|
|
429
462
|
|
|
430
463
|
_new_entries = fdp_glob.get_data_product_objects(
|
|
431
464
|
registry_token, _results_local, block_type, _version
|
|
432
465
|
)
|
|
433
466
|
|
|
467
|
+
# A match is written again as the pattern entry describes it
|
|
468
|
+
# (file_type, description, public), not as its registry row
|
|
469
|
+
if block_type == "write":
|
|
470
|
+
for _new_entry in _new_entries:
|
|
471
|
+
_new_entry.update(
|
|
472
|
+
{
|
|
473
|
+
k: v
|
|
474
|
+
for k, v in block_entry.items()
|
|
475
|
+
if k not in ("data_product", "use")
|
|
476
|
+
}
|
|
477
|
+
)
|
|
478
|
+
|
|
434
479
|
if block_type == "write":
|
|
435
480
|
_new_entries.append(block_entry)
|
|
436
481
|
|
|
@@ -637,7 +682,16 @@ class JobConfiguration(MutableMapping):
|
|
|
637
682
|
"not a valid git repository."
|
|
638
683
|
) from e
|
|
639
684
|
|
|
640
|
-
|
|
685
|
+
try:
|
|
686
|
+
_url = _git_repo.remotes[_remote].url
|
|
687
|
+
except IndexError as e:
|
|
688
|
+
raise fdp_exc.FDPRepositoryError(
|
|
689
|
+
f"Failed to update job configuration from location '{fair_repo_dir}', "
|
|
690
|
+
f"git repository '{_local_repo}' has no remote '{_remote}'.",
|
|
691
|
+
hint="Add the remote to the repository, or give "
|
|
692
|
+
"'run_metadata: remote_repo:' in the configuration.",
|
|
693
|
+
) from e
|
|
694
|
+
|
|
641
695
|
self["run_metadata.remote_repo"] = _url
|
|
642
696
|
|
|
643
697
|
def pop(self, key: str, default: typing.Optional[typing.Any] = None) -> typing.Any:
|
|
@@ -828,11 +882,14 @@ class JobConfiguration(MutableMapping):
|
|
|
828
882
|
|
|
829
883
|
def _tag_check():
|
|
830
884
|
_repo = git.Repo(fdp_conf.local_git_repo(self.local_repository))
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
885
|
+
# The nearest tag in the history of HEAD, not the last by name
|
|
886
|
+
try:
|
|
887
|
+
return _repo.git.describe("--tags", "--abbrev=0")
|
|
888
|
+
except git.GitCommandError as e:
|
|
889
|
+
raise fdp_exc.UserConfigError(
|
|
890
|
+
"Cannot use GIT_TAG variable, no git tags found "
|
|
891
|
+
"in the history of the current commit."
|
|
892
|
+
) from e
|
|
836
893
|
|
|
837
894
|
_substitutes: typing.Dict[str, typing.Callable] = {
|
|
838
895
|
"DATE": lambda: job_time.strftime("%Y%m%d"),
|
|
@@ -848,32 +905,20 @@ class JobConfiguration(MutableMapping):
|
|
|
848
905
|
"GIT_TAG": _tag_check,
|
|
849
906
|
}
|
|
850
907
|
|
|
908
|
+
# Substitutions are made in the parsed configuration, never in its
|
|
909
|
+
# YAML text, so that a value is inserted as it is: a Windows path's
|
|
910
|
+
# backslashes are neither regular-expression nor YAML escapes
|
|
911
|
+
|
|
851
912
|
# Additional parser for formatted datetime
|
|
852
|
-
_regex_dt_fmt = re.compile(r"\$\{\{\s*DATETIME\-[^}${\s]]+\s*\}\}")
|
|
853
913
|
_regex_fmt = re.compile(r"\$\{\{\s*DATETIME\-([^}${\s]+)\s*\}\}")
|
|
854
914
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
_fmt_res: typing.Optional[typing.List[str]] = _regex_fmt.findall(_config_str)
|
|
861
|
-
|
|
862
|
-
self._logger.debug(
|
|
863
|
-
"Found datetime substitutions: %s %s",
|
|
864
|
-
_dt_fmt_res or "",
|
|
865
|
-
_fmt_res or "",
|
|
915
|
+
self._config = _map_strings(
|
|
916
|
+
self._config,
|
|
917
|
+
lambda s: _regex_fmt.sub(
|
|
918
|
+
lambda m: job_time.strftime(m.group(1).strip()), s
|
|
919
|
+
),
|
|
866
920
|
)
|
|
867
921
|
|
|
868
|
-
# The two regex searches should match lengths
|
|
869
|
-
if len(_dt_fmt_res) != len(_fmt_res):
|
|
870
|
-
raise fdp_exc.UserConfigError("Failed to parse formatted datetime variable")
|
|
871
|
-
|
|
872
|
-
if _dt_fmt_res:
|
|
873
|
-
for i, _ in enumerate(_dt_fmt_res):
|
|
874
|
-
_time_str = job_time.strftime(_fmt_res[i].strip())
|
|
875
|
-
_config_str = _config_str.replace(_dt_fmt_res[i], _time_str)
|
|
876
|
-
|
|
877
922
|
_regex_dict = {
|
|
878
923
|
var: r"\$\{\{\s*" + f"{var}" + r"\s*\}\}" for var in _substitutes
|
|
879
924
|
}
|
|
@@ -881,16 +926,16 @@ class JobConfiguration(MutableMapping):
|
|
|
881
926
|
# Perform string substitutions
|
|
882
927
|
for var, subst in _regex_dict.items():
|
|
883
928
|
# Only execute functions in var substitutions that are required
|
|
884
|
-
if re.
|
|
885
|
-
_value = _substitutes[var]()
|
|
929
|
+
if any(re.search(subst, s) for s in _strings_in(self._config)):
|
|
930
|
+
_value = str(_substitutes[var]() or "")
|
|
886
931
|
if not _value:
|
|
887
932
|
raise fdp_exc.InternalError(
|
|
888
933
|
f"Expected value for substitution of '{var}' but returned None",
|
|
889
934
|
)
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
935
|
+
self._config = _map_strings(
|
|
936
|
+
self._config, lambda s: re.sub(subst, lambda _: _value, s)
|
|
937
|
+
)
|
|
938
|
+
self._logger.debug("Substituting %s: %s", var, _value)
|
|
894
939
|
|
|
895
940
|
def _register_to_read(
|
|
896
941
|
self, register_block: typing.List[typing.Dict]
|
|
@@ -1016,16 +1061,18 @@ class JobConfiguration(MutableMapping):
|
|
|
1016
1061
|
|
|
1017
1062
|
_version = item["use"]["version"]
|
|
1018
1063
|
|
|
1019
|
-
if "data_product"
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1064
|
+
if "data_product" in item["use"]:
|
|
1065
|
+
_name = item["use"]["data_product"]
|
|
1066
|
+
elif "external_object" in item and "*" in item["external_object"]:
|
|
1067
|
+
_name = item["external_object"]
|
|
1068
|
+
elif "data_product" in item and "*" in item["data_product"]:
|
|
1069
|
+
_name = item["data_product"]
|
|
1070
|
+
else:
|
|
1071
|
+
self._logger.warning(f"Missing use:data_product in {item}")
|
|
1026
1072
|
continue
|
|
1027
|
-
|
|
1028
|
-
|
|
1073
|
+
# Only a wildcard entry lacks this: kept in the write block as the
|
|
1074
|
+
# template for names the model writes that match it
|
|
1075
|
+
_new_item["use"]["data_product"] = _name
|
|
1029
1076
|
_namespace = item["use"]["namespace"]
|
|
1030
1077
|
|
|
1031
1078
|
# If no ID exists for the namespace then this object has not yet
|
|
@@ -1061,6 +1108,13 @@ class JobConfiguration(MutableMapping):
|
|
|
1061
1108
|
except fdp_exc.RegistryError:
|
|
1062
1109
|
_results = []
|
|
1063
1110
|
|
|
1111
|
+
if "*" in _name:
|
|
1112
|
+
_results = [
|
|
1113
|
+
result
|
|
1114
|
+
for result in _results
|
|
1115
|
+
if fdp_glob.matches_wildcard(_name, result["name"])
|
|
1116
|
+
]
|
|
1117
|
+
|
|
1064
1118
|
try:
|
|
1065
1119
|
_version = fdp_ver.get_correct_version(
|
|
1066
1120
|
version=_version,
|
|
@@ -1186,7 +1240,7 @@ class JobConfiguration(MutableMapping):
|
|
|
1186
1240
|
def shell(self) -> str:
|
|
1187
1241
|
"""Retrieve the shell choice"""
|
|
1188
1242
|
_shell_default = "batch" if platform.system() == "Windows" else "sh"
|
|
1189
|
-
return self.get("shell", _shell_default)
|
|
1243
|
+
return self.get("run_metadata.shell", _shell_default)
|
|
1190
1244
|
|
|
1191
1245
|
@property
|
|
1192
1246
|
def local_repository(self) -> str:
|
|
@@ -1318,11 +1372,18 @@ class JobConfiguration(MutableMapping):
|
|
|
1318
1372
|
encoding="utf-8",
|
|
1319
1373
|
)
|
|
1320
1374
|
|
|
1375
|
+
# The job log is UTF-8, but the console may not be (cp1252 on a
|
|
1376
|
+
# Windows runner), and one unencodable character must not stop the run
|
|
1377
|
+
_encoding = sys.stdout.encoding or "utf-8"
|
|
1378
|
+
|
|
1321
1379
|
# Write any stdout to the job log
|
|
1322
1380
|
for line in iter(_process.stdout.readline, ""):
|
|
1323
1381
|
self._log_file.writelines([line])
|
|
1324
1382
|
_log_tail.append(line)
|
|
1325
|
-
click.echo(
|
|
1383
|
+
click.echo(
|
|
1384
|
+
line.encode(_encoding, errors="replace").decode(_encoding),
|
|
1385
|
+
nl=False,
|
|
1386
|
+
)
|
|
1326
1387
|
sys.stdout.flush()
|
|
1327
1388
|
|
|
1328
1389
|
_process.wait()
|
|
@@ -14,6 +14,7 @@ Constants
|
|
|
14
14
|
- DISPOSABLES: tuple of keys to be removed before adding to config.yaml
|
|
15
15
|
|
|
16
16
|
"""
|
|
17
|
+
import re
|
|
17
18
|
import typing
|
|
18
19
|
|
|
19
20
|
import fair.exceptions as fdp_exc
|
|
@@ -38,6 +39,29 @@ DISPOSABLES = (
|
|
|
38
39
|
)
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
def matches_wildcard(pattern: str, name: str) -> bool:
|
|
43
|
+
"""Return whether a name matches a wildcard pattern
|
|
44
|
+
|
|
45
|
+
Each '*' matches one segment of the name: one or more characters other
|
|
46
|
+
than '/'. The pattern is anchored at both ends, and every other character
|
|
47
|
+
matches itself.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
pattern : str
|
|
52
|
+
name containing one or more '*' wildcards
|
|
53
|
+
name : str
|
|
54
|
+
name to test against the pattern
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
bool
|
|
59
|
+
whether the name matches
|
|
60
|
+
"""
|
|
61
|
+
_regex = "[^/]+".join(re.escape(part) for part in pattern.split("*"))
|
|
62
|
+
return re.fullmatch(_regex, name) is not None
|
|
63
|
+
|
|
64
|
+
|
|
41
65
|
def get_single_layer_objects(
|
|
42
66
|
results_list: typing.List[typing.Dict], object_type: str
|
|
43
67
|
) -> typing.List[typing.Dict]:
|
|
@@ -202,9 +202,19 @@ def is_api_url(uri: str, string: str) -> bool:
|
|
|
202
202
|
_uri = urllib.parse.urlparse(uri)
|
|
203
203
|
|
|
204
204
|
logger.debug(
|
|
205
|
-
"Checking if '%s' is a valid API URL against
|
|
205
|
+
"Checking if '%s' is a valid API URL against endpoint '%s'",
|
|
206
206
|
string,
|
|
207
|
-
|
|
207
|
+
uri,
|
|
208
208
|
)
|
|
209
209
|
|
|
210
|
-
|
|
210
|
+
if _url.netloc != _uri.netloc:
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
# The API is served under a path, and a registry serves other things from
|
|
214
|
+
# the same host - a remote registry's own data store is <domain>/data/ -
|
|
215
|
+
# so the path has to match too, or a StorageRoot's root is taken for a
|
|
216
|
+
# reference to another object. An endpoint given as a bare host has no
|
|
217
|
+
# path to compare, and then the net location is all there is to go on.
|
|
218
|
+
_api_path = check_trailing_slash(_uri.path) if _uri.path else "/"
|
|
219
|
+
|
|
220
|
+
return _url.path.startswith(_api_path)
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
]
|
|
24
24
|
description = "Synchronization interface for the SCRC FAIR Data Pipeline registry"
|
|
25
25
|
name = "fair-cli"
|
|
26
|
-
version = "0.
|
|
26
|
+
version = "0.10.0"
|
|
27
27
|
|
|
28
28
|
homepage = "https://www.fairdatapipeline.org/"
|
|
29
29
|
|
|
@@ -64,7 +64,7 @@ toml = "^0.10.2"
|
|
|
64
64
|
validators = "^0.18.2"
|
|
65
65
|
fake-useragent = "^1"
|
|
66
66
|
|
|
67
|
-
[tool.poetry.dev
|
|
67
|
+
[tool.poetry.group.dev.dependencies]
|
|
68
68
|
bandit = "*"
|
|
69
69
|
black = "*"
|
|
70
70
|
coverage = "*"
|
|
@@ -76,7 +76,6 @@ poetry = "^1.1.12"
|
|
|
76
76
|
pre-commit = "*"
|
|
77
77
|
pycodestyle = "^2.7.0"
|
|
78
78
|
pydocstyle = "^6.0.0"
|
|
79
|
-
pylama = "^8"
|
|
80
79
|
pytest = "*"
|
|
81
80
|
pytest-shutil = "*"
|
|
82
81
|
pytest-cov = "*"
|
|
@@ -88,6 +87,13 @@ moto = "^5"
|
|
|
88
87
|
Flask = "^3"
|
|
89
88
|
Flask-Cors = "^4"
|
|
90
89
|
matplotlib = "*"
|
|
90
|
+
# Brought in by matplotlib. Without a split, one numpy must serve the whole
|
|
91
|
+
# Python range, and numpy >= 2.3 needs Python >= 3.11, so every version got
|
|
92
|
+
# 2.2.6, which has no wheels for 3.14. Delete when the floor reaches 3.11.
|
|
93
|
+
numpy = [
|
|
94
|
+
{ version = ">=2.3", python = ">=3.11" },
|
|
95
|
+
{ version = "*", python = "<3.11" },
|
|
96
|
+
]
|
|
91
97
|
|
|
92
98
|
[tool.poetry.scripts]
|
|
93
99
|
fair = 'fair.cli:cli'
|
fair_cli-0.9.9/CHANGELOG.md
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# Unreleased
|
|
2
|
-
- Fix virtual environment bug in `fair` binaries where `venv` assumes the main executable is `python`.
|
|
3
|
-
- Registration of `author` and `namespace` objects.
|
|
4
|
-
- Allow specification of tag to install registry from during `fair registry install`.
|
|
5
|
-
- Wildcard '*' parsing introduced for data products.
|
|
6
|
-
- Ability to `push` to a registry added.
|
|
7
|
-
- Added `--dirty` option to `fair run` to allow running with uncommitted changes.
|
|
8
|
-
- Added `config.yaml` file validation.
|
|
9
|
-
- Added initialisation from existing registry.
|
|
10
|
-
- Switch to setting port not local URI during initialisation.
|
|
11
|
-
- Added option to specify port on `fair registry start`.
|
|
12
|
-
- Added `cli-config.yaml` file validation during initialisation.
|
|
13
|
-
- Added `fair reset` to list of commands.
|
|
14
|
-
- Add tab completion for staging data products.
|
|
15
|
-
|
|
16
|
-
# 2021-11-17 [v0.2.3](https://github.com/FAIRDataPipeline/FAIR-CLI/releases/tag/v0.2.3)
|
|
17
|
-
- Move handling of the user `config.yaml` file to a separate class `JobConfiguration`.
|
|
18
|
-
- Added various fixes to improve functionality within Windows.
|
|
19
|
-
- Move registry installation from script execution to internal function which sets up virtual environment etc.
|
|
20
|
-
- Added a test suite to the project.
|
|
21
|
-
- Added additional recognised identifiers for author setup from an organisation GRID and ROR.
|
|
22
|
-
|
|
23
|
-
# 2021-10-06 [v0.2.2](https://github.com/FAIRDataPipeline/FAIR-CLI/releases/tag/v0.2.2)
|
|
24
|
-
- Update to package metadata for PyPi
|
|
25
|
-
|
|
26
|
-
# 2021-10-06 [v0.2.1](https://github.com/FAIRDataPipeline/FAIR-CLI/releases/tag/v0.2.1)
|
|
27
|
-
- Automatic starter `config.yaml` generation.
|
|
28
|
-
- Local and Global CLI configurations.
|
|
29
|
-
- Start/stop local registry either explicitly or during synchronisations.
|
|
30
|
-
- Run logs available via git-like interface.
|
|
31
|
-
- Added ability to add/remove files.
|
|
32
|
-
- Repository style handling, acts like another git-like tool per project.
|
|
33
|
-
- Creation of an interface for `fair` using `click`.
|
|
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
|