fair-cli 0.10.1__tar.gz → 0.10.2__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.1 → fair_cli-0.10.2}/CHANGELOG.md +60 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/CITATION.cff +1 -1
- {fair_cli-0.10.1 → fair_cli-0.10.2}/PKG-INFO +83 -7
- {fair_cli-0.10.1 → fair_cli-0.10.2}/README.md +77 -3
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/identifiers.py +53 -3
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/register.py +50 -32
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/registry/server.py +31 -7
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/registry/storage.py +113 -5
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/registry/sync.py +221 -36
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/session.py +6 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/user_config/validation.py +5 -0
- fair_cli-0.10.2/fair/virtualenv.py +141 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/pyproject.toml +2 -5
- fair_cli-0.10.1/fair/virtualenv.py +0 -45
- {fair_cli-0.10.1 → fair_cli-0.10.2}/LICENSE +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/__init__.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/cli.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/common.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/configuration/__init__.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/configuration/validation.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/exceptions.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/files.txt +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/history.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/logging.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/registry/__init__.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/registry/file_formats.json +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/registry/file_types.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/registry/requests.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/registry/versioning.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/run.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/staging.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/templates/__init__.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/templates/config.jinja +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/templates/hist.jinja +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/testing.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/user_config/__init__.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/user_config/globbing.py +0 -0
- {fair_cli-0.10.1 → fair_cli-0.10.2}/fair/utilities.py +0 -0
|
@@ -1,3 +1,63 @@
|
|
|
1
|
+
# 2026-10-05 [v0.10.2](https://github.com/FAIRDataPipeline/FAIR-CLI/releases/tag/v0.10.2)
|
|
2
|
+
|
|
3
|
+
## Changed behaviour
|
|
4
|
+
- `fair pull` refuses a `register:` entry for an external object whose identifier or unique name, title and
|
|
5
|
+
release version are those of a source already registered from a different file: a source is one file. Give
|
|
6
|
+
the entry a `title` of its own, or its `release_version`; the same file under another name still registers.
|
|
7
|
+
With a local registry before data-registry v1.4.0, where each data product has its own source, nothing is
|
|
8
|
+
checked.
|
|
9
|
+
- `fair init` in a repository that is already initialised registers its user in the local registry, where it
|
|
10
|
+
did nothing. A registry reinstalled since the repository was initialised has no record of the user, and a
|
|
11
|
+
run failed until the repository was purged and initialised afresh. The repository itself is left as it is.
|
|
12
|
+
|
|
13
|
+
## Added
|
|
14
|
+
- A `register:` entry for an external object may give `release_version`, the version of the source it was taken
|
|
15
|
+
from, beside `release_date`. A local registry at data-registry v1.4.0 or later records it as the external
|
|
16
|
+
object's version, 1.0.0 without it; an earlier one records the data product's version there whatever is
|
|
17
|
+
given. It is not the entry's `version`, which is the data product's.
|
|
18
|
+
|
|
19
|
+
## Fixed
|
|
20
|
+
- A GitHub author or user was refused as "not a recognised github" once the GitHub API's limit for
|
|
21
|
+
anonymous requests (60 an hour per address) was spent. GitHub lookups now send the token in `GITHUB_TOKEN`
|
|
22
|
+
or `GITHUB_PAT` if set, retry without it if GitHub refuses it (expired or revoked), and say when the rate
|
|
23
|
+
limit is the problem.
|
|
24
|
+
- The FAIR-CLI binaries could not install a local registry, so `fair init` and `fair registry install` failed:
|
|
25
|
+
the registry's virtual environment was built from the binary in place of a Python. They now use a Python
|
|
26
|
+
>= 3.10 from `FAIR_PYTHON` or the `PATH`, or have `uv` fetch one, and say so if there is none. A missing
|
|
27
|
+
Python is found before anything is installed.
|
|
28
|
+
- `fair push` failed, with "Failed to access [] on remote registry", for a data product registered from a web
|
|
29
|
+
address once a code run had used it, when the local registry was data-registry v1.4.0. The remote records
|
|
30
|
+
such a file in two places with one hash - where it is stored and where it came from - and the wrong one
|
|
31
|
+
could be taken.
|
|
32
|
+
- `fair registry install --force` refused an existing install, as it does without `--force`, instead of
|
|
33
|
+
replacing it.
|
|
34
|
+
- A file that was already registered was not registered again under a second data product name, or in a
|
|
35
|
+
second namespace: `fair pull` passed over the entry without a word, and a run that read it then failed. An
|
|
36
|
+
entry is now passed over only when that data product, in that namespace and at that version, already holds
|
|
37
|
+
the file.
|
|
38
|
+
- `fair push` and `fair pull` left out a data product that shares its external object with another, as
|
|
39
|
+
data-registry v1.4.0 allows, while reporting it synchronised: its file was moved and its record was not made.
|
|
40
|
+
- Starting the local registry reported success when another registry already held its port, and the commands
|
|
41
|
+
that followed went to that one. The start now fails, naming the address: the server that answers there
|
|
42
|
+
must accept the installed registry's token.
|
|
43
|
+
- `fair push` and `fair pull` stopped with a Python error at a data product that has no file - one whose
|
|
44
|
+
object has no storage location, as an entry for a deposit as a whole may be - and so at a code run that
|
|
45
|
+
read one. Its records are now synchronised, and a `register:` entry of the same name is told it exists.
|
|
46
|
+
- `fair push` gave a code run an input it did not read when two data products held the same file, as one
|
|
47
|
+
file registered under two names does: a run that read one of them and wrote something new arrived on the
|
|
48
|
+
remote as having read both. A run pushed before this keeps the extra input.
|
|
49
|
+
- A file registered under a second data product name, or in a second namespace, was copied into the data store
|
|
50
|
+
again, though the registry records a file once there and nothing referred to the copy. The copy is no
|
|
51
|
+
longer kept: the data product's file is the one already in the store.
|
|
52
|
+
- A data product pulled from a remote could not be read through an API. `fair pull` copied the remote's record
|
|
53
|
+
of where the file is - the remote's own store - and fetched the file to a place no record named, so a model
|
|
54
|
+
was handed an address and no file. A pulled file is now recorded at its place in the local data store, and
|
|
55
|
+
a file that two data products share is fetched once. Data products pulled with an earlier version keep
|
|
56
|
+
the old record.
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
- The test of a data product's dependencies allows for any a registry adds; data-registry v1.4.0 adds one.
|
|
60
|
+
|
|
1
61
|
# 2026-10-05 [v0.10.1](https://github.com/FAIRDataPipeline/FAIR-CLI/releases/tag/v0.10.1)
|
|
2
62
|
|
|
3
63
|
## Changed behaviour
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: fair-cli
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.2
|
|
4
4
|
Summary: Synchronization interface for the SCRC FAIR Data Pipeline registry
|
|
5
|
-
Home-page: https://www.fairdatapipeline.org/
|
|
6
5
|
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,9 +20,10 @@ 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
|
|
23
25
|
Classifier: Topic :: Database :: Front-Ends
|
|
24
26
|
Classifier: Topic :: Scientific/Engineering
|
|
25
|
-
Provides-Extra: all
|
|
26
27
|
Requires-Dist: GitPython (>=3.1.18,<4.0.0)
|
|
27
28
|
Requires-Dist: Jinja2 (>=3.0.1,<4.0.0)
|
|
28
29
|
Requires-Dist: PyYAML (>=5.4.1,<7.0.0)
|
|
@@ -38,6 +39,7 @@ Requires-Dist: simplejson (>=3.17.5,<4.0.0)
|
|
|
38
39
|
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
39
40
|
Requires-Dist: validators (>=0.18.2,<0.19.0)
|
|
40
41
|
Project-URL: Documentation, https://www.fairdatapipeline.org/docs/interface/fdp/
|
|
42
|
+
Project-URL: Homepage, https://www.fairdatapipeline.org/
|
|
41
43
|
Project-URL: Issue Tracker, https://github.com/FAIRDataPipeline/FAIR-CLI/issues
|
|
42
44
|
Project-URL: Repository, https://github.com/FAIRDataPipeline/FAIR-CLI
|
|
43
45
|
Description-Content-Type: text/markdown
|
|
@@ -58,6 +60,8 @@ The package is installed using Pip:
|
|
|
58
60
|
pip install fair-cli
|
|
59
61
|
```
|
|
60
62
|
|
|
63
|
+
It needs Python 3.10 or later. A virtual environment is recommended: the local registry is installed with the same Python as the CLI, in an environment of its own.
|
|
64
|
+
|
|
61
65
|
To enable tab completion you need to modify your shell:
|
|
62
66
|
|
|
63
67
|
### Bash
|
|
@@ -78,12 +82,81 @@ _FAIR_COMPLETE=bash_source fair > ~/.config/fish/.fair-complete.fish
|
|
|
78
82
|
echo '. ~/.config/fish/.fair-complete.fish' >> ~/.bashrc
|
|
79
83
|
```
|
|
80
84
|
|
|
85
|
+
## Upgrading and reinstalling
|
|
86
|
+
|
|
87
|
+
The CLI and the local registry are installed separately - the CLI by `pip`, the registry by the CLI - so upgrading one does not upgrade the other.
|
|
88
|
+
|
|
89
|
+
### The CLI
|
|
90
|
+
|
|
91
|
+
To move to the newest release, or to a particular one:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
pip install --upgrade fair-cli
|
|
95
|
+
pip install fair-cli==0.10.1
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
To install a branch of this repository, give `pip` its name after an `@`. A tag or a commit works the same way:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
pip install git+https://github.com/FAIRDataPipeline/FAIR-CLI@<branch>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
or clone the repository, check the branch out and install from the clone:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
git clone -b <branch> https://github.com/FAIRDataPipeline/FAIR-CLI
|
|
108
|
+
pip install ./FAIR-CLI
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A branch keeps its version number from one commit to the next, and `pip` does not replace an installed package with another of the same version. To bring an installed branch up to date, or to install one branch over another of the same version, add `--force-reinstall --no-deps`:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
pip install --force-reinstall --no-deps git+https://github.com/FAIRDataPipeline/FAIR-CLI@<branch>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`fair --version` gives the version number alone, which a branch may share with a release. `pip freeze` says what is installed: `fair-cli==0.10.1` for a release, and `fair-cli @ git+https://github.com/FAIRDataPipeline/FAIR-CLI@<commit>` for a branch.
|
|
118
|
+
|
|
119
|
+
### The local registry
|
|
120
|
+
|
|
121
|
+
To replace the local registry with the newest release of it, stop it if it is running, install with `--force`, and start it again:
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
fair registry stop
|
|
125
|
+
fair registry install --force
|
|
126
|
+
fair registry start
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`--version` installs a tag or a branch of the [registry's repository](https://github.com/FAIRDataPipeline/data-registry) in place of the newest release:
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
fair registry install --force --version v1.3.0
|
|
133
|
+
fair registry install --force --version <branch>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Without `--force` an existing registry is left as it is and the command stops. `fair registry uninstall`, which asks first, followed by `fair registry install` does the same in two steps, and is the way with `fair-cli` 0.10.1 or earlier, where `--force` is refused.
|
|
137
|
+
|
|
138
|
+
Check the name given to `--version` before using it with `--force`: the old registry is removed before the name is looked for, so one that does not exist leaves no working registry until the command is run again with one that does.
|
|
139
|
+
|
|
140
|
+
The registry is installed in `~/.fair/registry`, or in the directory given with `--directory`, which has to be given again to replace a registry that was installed elsewhere. A reinstall replaces that directory:
|
|
141
|
+
|
|
142
|
+
- every record in the local registry goes with its database: the data products, the code runs, and what `fair init` put there;
|
|
143
|
+
- the local registry has a new token, written to `token` in its directory when it is next started, and anything else that was kept in that directory is gone;
|
|
144
|
+
- the data store, the projects and `~/.fair/cli` are not touched.
|
|
145
|
+
|
|
146
|
+
The new registry does not know the user of a project that was initialised before the reinstall, and a run in that project fails until it does. There are two ways to put that right:
|
|
147
|
+
|
|
148
|
+
- run `fair init` in the project again, which leaves the project as it is - its configuration and any data store kept inside it - and registers its user in the new registry. This needs `fair-cli` 0.10.2 or later: before that, `fair init` does nothing in a project that is already initialised;
|
|
149
|
+
- or start the project afresh: run `fair purge` in it, which removes its `.fair` folder and any data store kept inside it, and then `fair init`.
|
|
150
|
+
|
|
151
|
+
After either, `fair pull` registers again what the project registers or reads. What had been pushed to a remote registry can be pulled back from it. The records of anything that had not been pushed cannot be recovered, though its files are still in the data store unless that was removed with the project's `.fair` folder.
|
|
152
|
+
|
|
81
153
|
## Uninstallation
|
|
82
154
|
To uninstall the CLI run:
|
|
83
155
|
```
|
|
84
156
|
fair purge --all
|
|
85
|
-
pip uninstall fair
|
|
157
|
+
pip uninstall fair-cli
|
|
86
158
|
```
|
|
159
|
+
`fair purge --all` removes the `.fair` folder of the current project and the whole of `~/.fair`: the local registry, the default data store, the CLI's configuration and any token kept there. Leave it out to keep them.
|
|
87
160
|
|
|
88
161
|
## The User Configuration File
|
|
89
162
|
Job runs are configured via `config.yaml` files. Upon initialisation of a project, FAIR-CLI automatically generates a starter configuration file with all requirements in place. To execute a process (e.g. perform a model run from a compiled binary/script) an additional key of either `script` or `script_path` must be provided. Alternatively the command `fair run bash` can be used to append the key and run a command directly.
|
|
@@ -112,6 +185,8 @@ A full description of `config.yaml` files can be found [here](https://www.fairda
|
|
|
112
185
|
|
|
113
186
|
Initialises a new FAIR repository within the given directory. This should ideally be the same location as the `.git` folder for the current project, however during setup an option is given to specify an alternative. The command will ask the user a series of questions which will provide metadata for tracking run authors, and also allow for the creation of a starter `config.yaml` file. Initialisation will also configure the CLI itself.
|
|
114
187
|
|
|
188
|
+
In a repository that is already initialised the command asks nothing and leaves the repository as it is. It registers the repository's user in the local registry, which a registry [reinstalled](#upgrading-and-reinstalling) since the repository was initialised no longer holds.
|
|
189
|
+
|
|
115
190
|
#### Custom CLI Configuration
|
|
116
191
|
After setup is complete, the current CLI configuration can also be saved using the command:
|
|
117
192
|
```
|
|
@@ -270,10 +345,11 @@ The registry can be installed using the CLI as well by running:
|
|
|
270
345
|
```sh
|
|
271
346
|
fair registry install
|
|
272
347
|
```
|
|
273
|
-
with the additional options to specify the installation location, and the data registry repository tag to install from:
|
|
348
|
+
with the additional options to specify the installation location, and the data registry repository tag or branch to install from:
|
|
274
349
|
```sh
|
|
275
|
-
fair registry install --directory ~/.fair/my_registry --version v1.0
|
|
350
|
+
fair registry install --directory ~/.fair/my_registry --version v1.4.0
|
|
276
351
|
```
|
|
352
|
+
To replace a registry that is already installed, see [Upgrading and reinstalling](#upgrading-and-reinstalling).
|
|
277
353
|
|
|
278
354
|
### `log`
|
|
279
355
|
|
|
@@ -14,6 +14,8 @@ The package is installed using Pip:
|
|
|
14
14
|
pip install fair-cli
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
It needs Python 3.10 or later. A virtual environment is recommended: the local registry is installed with the same Python as the CLI, in an environment of its own.
|
|
18
|
+
|
|
17
19
|
To enable tab completion you need to modify your shell:
|
|
18
20
|
|
|
19
21
|
### Bash
|
|
@@ -34,12 +36,81 @@ _FAIR_COMPLETE=bash_source fair > ~/.config/fish/.fair-complete.fish
|
|
|
34
36
|
echo '. ~/.config/fish/.fair-complete.fish' >> ~/.bashrc
|
|
35
37
|
```
|
|
36
38
|
|
|
39
|
+
## Upgrading and reinstalling
|
|
40
|
+
|
|
41
|
+
The CLI and the local registry are installed separately - the CLI by `pip`, the registry by the CLI - so upgrading one does not upgrade the other.
|
|
42
|
+
|
|
43
|
+
### The CLI
|
|
44
|
+
|
|
45
|
+
To move to the newest release, or to a particular one:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pip install --upgrade fair-cli
|
|
49
|
+
pip install fair-cli==0.10.1
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
To install a branch of this repository, give `pip` its name after an `@`. A tag or a commit works the same way:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
pip install git+https://github.com/FAIRDataPipeline/FAIR-CLI@<branch>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
or clone the repository, check the branch out and install from the clone:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
git clone -b <branch> https://github.com/FAIRDataPipeline/FAIR-CLI
|
|
62
|
+
pip install ./FAIR-CLI
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
A branch keeps its version number from one commit to the next, and `pip` does not replace an installed package with another of the same version. To bring an installed branch up to date, or to install one branch over another of the same version, add `--force-reinstall --no-deps`:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
pip install --force-reinstall --no-deps git+https://github.com/FAIRDataPipeline/FAIR-CLI@<branch>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`fair --version` gives the version number alone, which a branch may share with a release. `pip freeze` says what is installed: `fair-cli==0.10.1` for a release, and `fair-cli @ git+https://github.com/FAIRDataPipeline/FAIR-CLI@<commit>` for a branch.
|
|
72
|
+
|
|
73
|
+
### The local registry
|
|
74
|
+
|
|
75
|
+
To replace the local registry with the newest release of it, stop it if it is running, install with `--force`, and start it again:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
fair registry stop
|
|
79
|
+
fair registry install --force
|
|
80
|
+
fair registry start
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`--version` installs a tag or a branch of the [registry's repository](https://github.com/FAIRDataPipeline/data-registry) in place of the newest release:
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
fair registry install --force --version v1.3.0
|
|
87
|
+
fair registry install --force --version <branch>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Without `--force` an existing registry is left as it is and the command stops. `fair registry uninstall`, which asks first, followed by `fair registry install` does the same in two steps, and is the way with `fair-cli` 0.10.1 or earlier, where `--force` is refused.
|
|
91
|
+
|
|
92
|
+
Check the name given to `--version` before using it with `--force`: the old registry is removed before the name is looked for, so one that does not exist leaves no working registry until the command is run again with one that does.
|
|
93
|
+
|
|
94
|
+
The registry is installed in `~/.fair/registry`, or in the directory given with `--directory`, which has to be given again to replace a registry that was installed elsewhere. A reinstall replaces that directory:
|
|
95
|
+
|
|
96
|
+
- every record in the local registry goes with its database: the data products, the code runs, and what `fair init` put there;
|
|
97
|
+
- the local registry has a new token, written to `token` in its directory when it is next started, and anything else that was kept in that directory is gone;
|
|
98
|
+
- the data store, the projects and `~/.fair/cli` are not touched.
|
|
99
|
+
|
|
100
|
+
The new registry does not know the user of a project that was initialised before the reinstall, and a run in that project fails until it does. There are two ways to put that right:
|
|
101
|
+
|
|
102
|
+
- run `fair init` in the project again, which leaves the project as it is - its configuration and any data store kept inside it - and registers its user in the new registry. This needs `fair-cli` 0.10.2 or later: before that, `fair init` does nothing in a project that is already initialised;
|
|
103
|
+
- or start the project afresh: run `fair purge` in it, which removes its `.fair` folder and any data store kept inside it, and then `fair init`.
|
|
104
|
+
|
|
105
|
+
After either, `fair pull` registers again what the project registers or reads. What had been pushed to a remote registry can be pulled back from it. The records of anything that had not been pushed cannot be recovered, though its files are still in the data store unless that was removed with the project's `.fair` folder.
|
|
106
|
+
|
|
37
107
|
## Uninstallation
|
|
38
108
|
To uninstall the CLI run:
|
|
39
109
|
```
|
|
40
110
|
fair purge --all
|
|
41
|
-
pip uninstall fair
|
|
111
|
+
pip uninstall fair-cli
|
|
42
112
|
```
|
|
113
|
+
`fair purge --all` removes the `.fair` folder of the current project and the whole of `~/.fair`: the local registry, the default data store, the CLI's configuration and any token kept there. Leave it out to keep them.
|
|
43
114
|
|
|
44
115
|
## The User Configuration File
|
|
45
116
|
Job runs are configured via `config.yaml` files. Upon initialisation of a project, FAIR-CLI automatically generates a starter configuration file with all requirements in place. To execute a process (e.g. perform a model run from a compiled binary/script) an additional key of either `script` or `script_path` must be provided. Alternatively the command `fair run bash` can be used to append the key and run a command directly.
|
|
@@ -68,6 +139,8 @@ A full description of `config.yaml` files can be found [here](https://www.fairda
|
|
|
68
139
|
|
|
69
140
|
Initialises a new FAIR repository within the given directory. This should ideally be the same location as the `.git` folder for the current project, however during setup an option is given to specify an alternative. The command will ask the user a series of questions which will provide metadata for tracking run authors, and also allow for the creation of a starter `config.yaml` file. Initialisation will also configure the CLI itself.
|
|
70
141
|
|
|
142
|
+
In a repository that is already initialised the command asks nothing and leaves the repository as it is. It registers the repository's user in the local registry, which a registry [reinstalled](#upgrading-and-reinstalling) since the repository was initialised no longer holds.
|
|
143
|
+
|
|
71
144
|
#### Custom CLI Configuration
|
|
72
145
|
After setup is complete, the current CLI configuration can also be saved using the command:
|
|
73
146
|
```
|
|
@@ -226,10 +299,11 @@ The registry can be installed using the CLI as well by running:
|
|
|
226
299
|
```sh
|
|
227
300
|
fair registry install
|
|
228
301
|
```
|
|
229
|
-
with the additional options to specify the installation location, and the data registry repository tag to install from:
|
|
302
|
+
with the additional options to specify the installation location, and the data registry repository tag or branch to install from:
|
|
230
303
|
```sh
|
|
231
|
-
fair registry install --directory ~/.fair/my_registry --version v1.0
|
|
304
|
+
fair registry install --directory ~/.fair/my_registry --version v1.4.0
|
|
232
305
|
```
|
|
306
|
+
To replace a registry that is already installed, see [Upgrading and reinstalling](#upgrading-and-reinstalling).
|
|
233
307
|
|
|
234
308
|
### `log`
|
|
235
309
|
|
|
@@ -18,6 +18,7 @@ Functions
|
|
|
18
18
|
|
|
19
19
|
__date__ = "2021-07-01"
|
|
20
20
|
|
|
21
|
+
import os
|
|
21
22
|
import time
|
|
22
23
|
import typing
|
|
23
24
|
import urllib.parse
|
|
@@ -28,6 +29,8 @@ import logging
|
|
|
28
29
|
from urllib3.exceptions import InsecureRequestWarning
|
|
29
30
|
from fake_useragent import UserAgent
|
|
30
31
|
|
|
32
|
+
import fair.exceptions as fdp_exc
|
|
33
|
+
|
|
31
34
|
logger = logging.getLogger("FAIRDataPipeline.Identifiers")
|
|
32
35
|
|
|
33
36
|
JSON_MIME_TYPE = "application/json"
|
|
@@ -98,10 +101,28 @@ def check_github(github: str) -> typing.Dict:
|
|
|
98
101
|
typing.Dict
|
|
99
102
|
metadata from the given ID
|
|
100
103
|
"""
|
|
101
|
-
_header = JSON_HEADERS
|
|
102
104
|
_url = urllib.parse.urljoin(QUERY_URLS["github"], github)
|
|
103
105
|
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
|
|
104
|
-
|
|
106
|
+
|
|
107
|
+
# Requests without a token share a limit of 60 an hour per IP address
|
|
108
|
+
_token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GITHUB_PAT")
|
|
109
|
+
_auth = {"Authorization": f"Bearer {_token}"} if _token else {}
|
|
110
|
+
_response = requests.get(
|
|
111
|
+
_url, headers={**JSON_HEADERS, **_auth}, verify=False, allow_redirects=True
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# An expired or revoked token is refused outright, where no token is not
|
|
115
|
+
if _response.status_code == 401 and _token:
|
|
116
|
+
logger.warning(
|
|
117
|
+
"GitHub refused the token in GITHUB_TOKEN/GITHUB_PAT, "
|
|
118
|
+
"retrying without it"
|
|
119
|
+
)
|
|
120
|
+
_auth = {}
|
|
121
|
+
_response = requests.get(
|
|
122
|
+
_url, headers=JSON_HEADERS, verify=False, allow_redirects=True
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
_check_github_rate_limit(_response, bool(_auth))
|
|
105
126
|
|
|
106
127
|
_result_dict: typing.Dict[str, typing.Any] = {}
|
|
107
128
|
|
|
@@ -109,8 +130,9 @@ def check_github(github: str) -> typing.Dict:
|
|
|
109
130
|
time.sleep(3)
|
|
110
131
|
_header = {"Accept": JSON_MIME_TYPE, "User-Agent": str(UserAgent().chrome)}
|
|
111
132
|
_response = requests.get(
|
|
112
|
-
_url, headers=_header, verify=False, allow_redirects=True
|
|
133
|
+
_url, headers={**_header, **_auth}, verify=False, allow_redirects=True
|
|
113
134
|
)
|
|
135
|
+
_check_github_rate_limit(_response, bool(_auth))
|
|
114
136
|
|
|
115
137
|
if _response.status_code != 200:
|
|
116
138
|
logger.debug(f"{_url} Responded with {_response.status_code}")
|
|
@@ -129,6 +151,34 @@ def check_github(github: str) -> typing.Dict:
|
|
|
129
151
|
return _result_dict
|
|
130
152
|
|
|
131
153
|
|
|
154
|
+
def _check_github_rate_limit(response: requests.Response, token: bool) -> None:
|
|
155
|
+
"""Raise if the GitHub API refused a request for exceeding its rate limit
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
response : requests.Response
|
|
160
|
+
response from the GitHub API
|
|
161
|
+
token : bool
|
|
162
|
+
whether the request was sent with a token GitHub accepted
|
|
163
|
+
"""
|
|
164
|
+
if response.status_code not in (403, 429):
|
|
165
|
+
return
|
|
166
|
+
if response.headers.get("X-RateLimit-Remaining") != "0":
|
|
167
|
+
return
|
|
168
|
+
_reset = response.headers.get("X-RateLimit-Reset")
|
|
169
|
+
_when = (
|
|
170
|
+
f" until {time.strftime('%H:%M:%S', time.localtime(int(_reset)))}"
|
|
171
|
+
if _reset and _reset.isdigit()
|
|
172
|
+
else ""
|
|
173
|
+
)
|
|
174
|
+
raise fdp_exc.FAIRCLIException(
|
|
175
|
+
f"The GitHub API rate limit is exhausted{_when}, so GitHub usernames "
|
|
176
|
+
"cannot be checked",
|
|
177
|
+
hint="Wait for the limit to reset"
|
|
178
|
+
+ ("" if token else ", or set GITHUB_TOKEN to a valid GitHub token"),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
132
182
|
def check_gitlab(gitlab: str, gitlab_url: str = "https://gitlab.com/") -> typing.Dict:
|
|
133
183
|
"""Checks if valid GitLab Username using Gitlab profile address
|
|
134
184
|
|
|
@@ -28,6 +28,8 @@ import typing
|
|
|
28
28
|
|
|
29
29
|
import urllib.parse
|
|
30
30
|
|
|
31
|
+
import semver
|
|
32
|
+
|
|
31
33
|
import fair.exceptions as fdp_exc
|
|
32
34
|
import fair.registry.requests as fdp_req
|
|
33
35
|
import fair.registry.storage as fdp_store
|
|
@@ -114,6 +116,17 @@ def fetch_registrations(
|
|
|
114
116
|
f"Expected key '{key}' in 'register' item"
|
|
115
117
|
)
|
|
116
118
|
|
|
119
|
+
# The version of the source itself, where an external object has one
|
|
120
|
+
if "release_version" in entry:
|
|
121
|
+
try:
|
|
122
|
+
semver.VersionInfo.parse(f"{entry['release_version']}")
|
|
123
|
+
except ValueError as e:
|
|
124
|
+
raise fdp_exc.UserConfigError(
|
|
125
|
+
"Expected a semantic version for 'release_version' in "
|
|
126
|
+
f"'register' item, but got '{entry['release_version']}'",
|
|
127
|
+
hint="Write it in quotes, with three parts, e.g. '2.1.0'",
|
|
128
|
+
) from e
|
|
129
|
+
|
|
117
130
|
_identifier: str = entry["identifier"] if "identifier" in entry else ""
|
|
118
131
|
_unique_name: str = entry["unique_name"] if "unique_name" in entry else ""
|
|
119
132
|
|
|
@@ -121,8 +134,6 @@ def fetch_registrations(
|
|
|
121
134
|
_external_object = None
|
|
122
135
|
_is_present = None
|
|
123
136
|
|
|
124
|
-
_search_data = {}
|
|
125
|
-
|
|
126
137
|
if "data_product" in entry:
|
|
127
138
|
_data_product: str = entry["use"]["data_product"]
|
|
128
139
|
elif "external_object" in entry:
|
|
@@ -139,39 +150,31 @@ def fetch_registrations(
|
|
|
139
150
|
)
|
|
140
151
|
elif _external_object:
|
|
141
152
|
_name = entry["use"]["data_product"]
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
# _search_data['alternate_identifier'] = entry['unique_name']
|
|
146
|
-
_search_data["alternate_identifier_type"] = entry[
|
|
147
|
-
"alternate_identifier_type"
|
|
148
|
-
]
|
|
149
|
-
elif "identifier" in entry:
|
|
150
|
-
_search_data["identifier"] = entry["identifier"]
|
|
151
|
-
else:
|
|
153
|
+
if "identifier" not in entry and not (
|
|
154
|
+
"unique_name" in entry and "alternate_identifier_type" in entry
|
|
155
|
+
):
|
|
152
156
|
raise fdp_exc.UserConfigError(
|
|
153
157
|
"Expected either 'identifier', or 'unique_name' and "
|
|
154
158
|
f"'alternate_identifier_type' in external object '{_name}'"
|
|
155
159
|
)
|
|
156
|
-
try:
|
|
157
|
-
_data_product_id = convert_key_value_to_id(
|
|
158
|
-
local_uri,
|
|
159
|
-
"data_product",
|
|
160
|
-
entry["use"]["data_product"],
|
|
161
|
-
fdp_req.local_token(),
|
|
162
|
-
)
|
|
163
|
-
_search_data["data_product"] = _data_product_id
|
|
164
|
-
except fdp_exc.RegistryError:
|
|
165
|
-
_is_present = "absent"
|
|
166
160
|
|
|
167
161
|
else:
|
|
168
162
|
_name = entry["use"]["data_product"]
|
|
169
|
-
_obj_type = "data_product"
|
|
170
|
-
_search_data = {"name": _name}
|
|
171
163
|
|
|
172
|
-
_search_data["version"] = entry["use"]["version"]
|
|
173
164
|
_namespace = entry["use"]["namespace"]
|
|
174
165
|
|
|
166
|
+
# What is looked for is the data product itself, by its namespace, name
|
|
167
|
+
# and version: the same file may be registered under another name or
|
|
168
|
+
# in another namespace, and an external object may be shared by several
|
|
169
|
+
# data products. A namespace the registry does not hold has nothing in it
|
|
170
|
+
_search_data = {"name": _name, "version": entry["use"]["version"]}
|
|
171
|
+
try:
|
|
172
|
+
_search_data["namespace"] = convert_key_value_to_id(
|
|
173
|
+
local_uri, "namespace", _namespace, fdp_req.local_token()
|
|
174
|
+
)
|
|
175
|
+
except fdp_exc.RegistryError:
|
|
176
|
+
_is_present = "absent"
|
|
177
|
+
|
|
175
178
|
if _external_object:
|
|
176
179
|
if not _identifier and not _unique_name:
|
|
177
180
|
raise fdp_exc.UserConfigError(
|
|
@@ -202,13 +205,14 @@ def fetch_registrations(
|
|
|
202
205
|
_local_dir = os.path.join(write_data_store, _namespace, _name)
|
|
203
206
|
|
|
204
207
|
# Check if the object is already present on the local registry
|
|
205
|
-
_is_present
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
if _is_present != "absent":
|
|
209
|
+
_is_present = fdp_store.check_if_object_exists(
|
|
210
|
+
local_uri=local_uri,
|
|
211
|
+
file_loc=_temp_data_file,
|
|
212
|
+
token=fdp_req.local_token(),
|
|
213
|
+
obj_type="data_product",
|
|
214
|
+
search_data=_search_data,
|
|
215
|
+
)
|
|
212
216
|
|
|
213
217
|
# Hash matched version already present
|
|
214
218
|
if _is_present == "hash_match":
|
|
@@ -238,6 +242,20 @@ def fetch_registrations(
|
|
|
238
242
|
)
|
|
239
243
|
logger.debug("No existing results found for %s", _search_data)
|
|
240
244
|
|
|
245
|
+
if _external_object:
|
|
246
|
+
try:
|
|
247
|
+
fdp_store.check_source_is_one_file(
|
|
248
|
+
local_uri=local_uri,
|
|
249
|
+
file_loc=_temp_data_file,
|
|
250
|
+
token=fdp_req.local_token(),
|
|
251
|
+
data=entry,
|
|
252
|
+
name=_name,
|
|
253
|
+
)
|
|
254
|
+
except fdp_exc.UserConfigError:
|
|
255
|
+
if _remove:
|
|
256
|
+
os.remove(_temp_data_file)
|
|
257
|
+
raise
|
|
258
|
+
|
|
241
259
|
# Create object location directory, ignoring if already present
|
|
242
260
|
# as multiple version files can exist
|
|
243
261
|
os.makedirs(_local_dir, exist_ok=True)
|
|
@@ -94,6 +94,15 @@ def check_server_running(local_uri: str = None) -> bool:
|
|
|
94
94
|
return False
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
# Whether the server at a registry endpoint is the registry a token belongs
|
|
98
|
+
# to: a registry refuses a token that is not its own
|
|
99
|
+
def _accepts_token(uri: str, token: str) -> bool:
|
|
100
|
+
_response = requests.get(
|
|
101
|
+
f"{uri}users/", headers={"Authorization": f"token {token}"}
|
|
102
|
+
)
|
|
103
|
+
return _response.status_code not in (401, 403)
|
|
104
|
+
|
|
105
|
+
|
|
97
106
|
def launch_server(
|
|
98
107
|
port: int = 8000,
|
|
99
108
|
registry_dir: str = None,
|
|
@@ -153,6 +162,18 @@ def launch_server(
|
|
|
153
162
|
"Failed to start local registry, no response from server"
|
|
154
163
|
)
|
|
155
164
|
|
|
165
|
+
# A response does not show that this registry started: another one that
|
|
166
|
+
# holds the port answers too, while this one stops for want of it, and
|
|
167
|
+
# the start script's status does not tell the two apart
|
|
168
|
+
if not _accepts_token(local_uri, fdp_req.local_token(registry_dir)):
|
|
169
|
+
raise fdp_exc.RegistryError(
|
|
170
|
+
f"Failed to start local registry: the server at '{local_uri}' "
|
|
171
|
+
f"is not the one installed in '{registry_dir}', as it refuses "
|
|
172
|
+
"that registry's token",
|
|
173
|
+
hint="Another server may hold the port: stop it, or start this "
|
|
174
|
+
"registry on another port with 'fair registry start --port'",
|
|
175
|
+
)
|
|
176
|
+
|
|
156
177
|
|
|
157
178
|
def stop_server(
|
|
158
179
|
registry_dir: str = None,
|
|
@@ -307,7 +328,13 @@ def install_registry(
|
|
|
307
328
|
if not install_dir:
|
|
308
329
|
install_dir = fdp_com.DEFAULT_REGISTRY_LOCATION
|
|
309
330
|
|
|
310
|
-
|
|
331
|
+
# Find a Python for the registry before changing anything on disk
|
|
332
|
+
_venv_cmd = None
|
|
333
|
+
if not venv_dir:
|
|
334
|
+
venv_dir = os.path.join(install_dir, "venv")
|
|
335
|
+
_venv_cmd = fdp_env.venv_command(venv_dir)
|
|
336
|
+
|
|
337
|
+
if os.path.exists(install_dir) and not force:
|
|
311
338
|
raise fdp_exc.RegistryError(
|
|
312
339
|
f"Local registry is already installed in {install_dir}"
|
|
313
340
|
)
|
|
@@ -361,12 +388,9 @@ def install_registry(
|
|
|
361
388
|
else:
|
|
362
389
|
_repo.git.checkout(reference)
|
|
363
390
|
|
|
364
|
-
if
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
_venv = fdp_env.FAIREnv(with_pip=True)
|
|
368
|
-
|
|
369
|
-
_venv.create(venv_dir)
|
|
391
|
+
if _venv_cmd:
|
|
392
|
+
logger.debug("Creating virtual environment: %s", _venv_cmd)
|
|
393
|
+
subprocess.check_call(_venv_cmd)
|
|
370
394
|
|
|
371
395
|
_python_exe = "python.exe" if platform.system() == "Windows" else "python"
|
|
372
396
|
_binary_loc = "Scripts" if platform.system() == "Windows" else "bin"
|