grz-cli 2.0.0__tar.gz → 3.0.1__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.
- {grz_cli-2.0.0 → grz_cli-3.0.1}/CHANGELOG.md +36 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/PKG-INFO +4 -11
- {grz_cli-2.0.0 → grz_cli-3.0.1}/README.md +2 -9
- grz_cli-3.0.1/docs/version-compatibility-check.md +63 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/pyproject.toml +2 -1
- grz_cli-3.0.1/src/grz_cli/__init__.py +1 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/cli.py +7 -1
- {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/encrypt.py +28 -24
- grz_cli-3.0.1/src/grz_cli/models/config.py +74 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/utils/version_check.py +2 -14
- grz_cli-3.0.1/tests/test_config.py +109 -0
- grz_cli-3.0.1/tests/test_encrypt.py +54 -0
- grz_cli-2.0.0/docs/version-compatibility-check.md +0 -56
- grz_cli-2.0.0/src/grz_cli/__init__.py +0 -1
- grz_cli-2.0.0/src/grz_cli/models/config.py +0 -19
- {grz_cli-2.0.0 → grz_cli-3.0.1}/.gitignore +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/LICENSE +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/MANIFEST.in +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/src/__init__.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/__init__.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/get_id.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/submit.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/upload.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/commands/validate.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/models/__init__.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/src/grz_cli/py.typed +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/tests/conftest.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/tests/test_smoke.py +0 -0
- {grz_cli-2.0.0 → grz_cli-3.0.1}/tests/test_submit.py +0 -0
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.0.1](https://github.com/BfArM-MVH/grz-tools/compare/grz-cli-v3.0.0...grz-cli-v3.0.1) (2026-10-22)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **grz-cli,grzctl:** require grz-common 4.0.2 ([#737](https://github.com/BfArM-MVH/grz-tools/issues/737)) ([6bc5d89](https://github.com/BfArM-MVH/grz-tools/commit/6bc5d8942bf3568ba1037a52f8fa0a97ace71b50))
|
|
9
|
+
|
|
10
|
+
## [3.0.0](https://github.com/BfArM-MVH/grz-tools/compare/grz-cli-v2.0.0...grz-cli-v3.0.0) (2026-09-26)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### ⚠ BREAKING CHANGES
|
|
14
|
+
|
|
15
|
+
* **grzctl:** the grzctl config has no `keys` section. Each inbox names its private key. An inbox's `private_key_passphrase` comes before `C4GH_PASSPHRASE`.
|
|
16
|
+
* **grz-cli:** `Crypt4GH.prepare_c4gh_keys`, `Crypt4GH.decrypt_file`, `Submission.encrypt`, `EncryptedSubmission.decrypt`, `Worker.encrypt` and `Worker.decrypt` take `X25519PrivateKey` and `X25519PublicKey` objects instead of paths. The `Crypt4GH` key loaders return these objects.
|
|
17
|
+
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **grz-cli,grz-common,grz-db,grzctl:** record why a submission failed and classify S3 errors ([#690](https://github.com/BfArM-MVH/grz-tools/issues/690)) ([f1d3fc8](https://github.com/BfArM-MVH/grz-tools/commit/f1d3fc804387c4fb6de861cccb6a99b521ac0056))
|
|
21
|
+
* **grz-cli:** accept the submitter private key inline ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
22
|
+
* **grz-cli:** add keys.submitter_private_key_passphrase ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
23
|
+
* **grz-common:** load crypt4gh keys in memory ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
24
|
+
* **grz-common:** recommend grz-cli 3.0.0 in the bundled version policy ([bf3e6bf](https://github.com/BfArM-MVH/grz-tools/commit/bf3e6bf4f2ed21cd1c7bdf4d6aa8710ece4f60d3))
|
|
25
|
+
* **grzctl:** accept every crypt4gh key in the config inline ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
26
|
+
* **grzctl:** Centralized version.json and its distribution using grzctl ([#667](https://github.com/BfArM-MVH/grz-tools/issues/667)) ([6ed9cb0](https://github.com/BfArM-MVH/grz-tools/commit/6ed9cb04bede9fc9b34b7ce6d3aa9a0df9d42ae4))
|
|
27
|
+
* **grzctl:** configure each crypt4gh key where grzctl uses it ([#694](https://github.com/BfArM-MVH/grz-tools/issues/694)) ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
|
|
32
|
+
* **grz-cli,grz-common,grz-db,grzctl:** require grz-common 4, grz-db 4 and grz-pydantic-models 4 ([#688](https://github.com/BfArM-MVH/grz-tools/issues/688)) ([bf3e6bf](https://github.com/BfArM-MVH/grz-tools/commit/bf3e6bf4f2ed21cd1c7bdf4d6aa8710ece4f60d3))
|
|
33
|
+
* **grz-common:** keep crypt4gh private keys out of DEBUG logs ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
34
|
+
* **grz-common:** leave the raw input out of config validation errors ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
35
|
+
* **grzctl,grz-common,grz-cli:** drop grzctl submit and grzctl's grz-cli dependency ([#675](https://github.com/BfArM-MVH/grz-tools/issues/675)) ([e6f27c1](https://github.com/BfArM-MVH/grz-tools/commit/e6f27c1714fc73d5159b29356914bc345a797c0a))
|
|
36
|
+
* **grzctl:** keep the stored upload date in db submission populate ([f1d3fc8](https://github.com/BfArM-MVH/grz-tools/commit/f1d3fc804387c4fb6de861cccb6a99b521ac0056))
|
|
37
|
+
* **grzctl:** mask db.author.private_key in dump-config ([06f12e6](https://github.com/BfArM-MVH/grz-tools/commit/06f12e625878c87413fbc1afa77ce8708c03a3df))
|
|
38
|
+
|
|
3
39
|
## [2.0.0](https://github.com/BfArM-MVH/grz-tools/compare/grz-cli-v1.8.0...grz-cli-v2.0.0) (2026-08-13)
|
|
4
40
|
|
|
5
41
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: grz-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.1
|
|
4
4
|
Summary: Tool for validation, encryption and upload of MV submissions to GDCs.
|
|
5
5
|
Project-URL: Homepage, https://github.com/BfArM-MVH/grz-tools
|
|
6
6
|
Project-URL: Repository, https://github.com/BfArM-MVH/grz-tools
|
|
@@ -13,7 +13,7 @@ Keywords: GDC,GRZ,S3
|
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Requires-Python: >=3.12
|
|
16
|
-
Requires-Dist: grz-common<4
|
|
16
|
+
Requires-Dist: grz-common<5,>=4.0.2
|
|
17
17
|
Requires-Dist: packaging<27,>=23
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -21,7 +21,6 @@ Description-Content-Type: text/markdown
|
|
|
21
21
|
|
|
22
22
|
A command-line tool for validating, encrypting, and uploading submissions to a genomDE Model Project GDC (Genome Data Center).
|
|
23
23
|
|
|
24
|
-
|
|
25
24
|
## Table of Contents
|
|
26
25
|
|
|
27
26
|
- [Installation](#installation)
|
|
@@ -38,7 +37,6 @@ A command-line tool for validating, encrypting, and uploading submissions to a g
|
|
|
38
37
|
- [License](#license)
|
|
39
38
|
- [Acknowledgements](#acknowledgements)
|
|
40
39
|
|
|
41
|
-
|
|
42
40
|
## Installation
|
|
43
41
|
|
|
44
42
|
### Requirements
|
|
@@ -53,7 +51,6 @@ For example:
|
|
|
53
51
|
- Docker container
|
|
54
52
|
- Windows Subsystem for Linux
|
|
55
53
|
|
|
56
|
-
|
|
57
54
|
### Using [Conda](https://conda.io) (recommended)
|
|
58
55
|
|
|
59
56
|
If Conda is not yet available on your system, we recommend to install it through the [Miniforge Conda installer](https://github.com/conda-forge/miniforge) by running the following commands:
|
|
@@ -79,7 +76,6 @@ Use the following command to update the tool:
|
|
|
79
76
|
conda update -n grz-tools -c conda-forge -c bioconda grz-cli
|
|
80
77
|
```
|
|
81
78
|
|
|
82
|
-
|
|
83
79
|
### Using pip (not recommended)
|
|
84
80
|
|
|
85
81
|
While installation via `pip` is possible, it is not recommended because users should create/manage a Python virtual environment and must ensure that the correct Python version is being used.
|
|
@@ -96,7 +92,6 @@ Use the following command to update the tool:
|
|
|
96
92
|
pip upgrade grz-cli
|
|
97
93
|
```
|
|
98
94
|
|
|
99
|
-
|
|
100
95
|
### Using Docker
|
|
101
96
|
|
|
102
97
|
Docker images are available via biocontainers at [https://biocontainers.pro/tools/grz-cli](https://biocontainers.pro/tools/grz-cli).
|
|
@@ -120,7 +115,7 @@ The S3 secrets can either be directly within the config file or as defined with
|
|
|
120
115
|
|
|
121
116
|
It is recommended to have the following folder structure for a single submission:
|
|
122
117
|
|
|
123
|
-
```
|
|
118
|
+
```text
|
|
124
119
|
EXAMPLE_SUBMISSION
|
|
125
120
|
├── files
|
|
126
121
|
│ ├── aaaaaaaa00000000aaaaaaaa00000000_blood_normal.read1.fastq.gz
|
|
@@ -138,7 +133,6 @@ The only requirements are that `metadata/metadata.json` exists and the `files/`
|
|
|
138
133
|
Data files may be nested under subfolders inside `files/` for better organization.
|
|
139
134
|
For example, each donor could have their own folder for files.
|
|
140
135
|
|
|
141
|
-
|
|
142
136
|
### Example submission procedure
|
|
143
137
|
|
|
144
138
|
After preparing your submission as outlined above, you can use the following command to validate, encrypt and upload the submission:
|
|
@@ -167,7 +161,6 @@ It combines the `validate`, `encrypt`, and `upload` commands (see below).
|
|
|
167
161
|
grz-cli submit --submission-dir foo
|
|
168
162
|
```
|
|
169
163
|
|
|
170
|
-
|
|
171
164
|
### validate
|
|
172
165
|
|
|
173
166
|
It is recommended to run this command before continuing with encryption and upload.
|
|
@@ -228,7 +221,7 @@ We recommend using Conda or [Pixi](https://pixi.sh/latest).
|
|
|
228
221
|
|
|
229
222
|
After cloning the desired branch of the `grz-tools` repo locally, you can run `grz-cli` directly from the repo using:
|
|
230
223
|
|
|
231
|
-
```
|
|
224
|
+
```text
|
|
232
225
|
uv run --project path/to/cloned/grz-tools grz-cli --help
|
|
233
226
|
```
|
|
234
227
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
A command-line tool for validating, encrypting, and uploading submissions to a genomDE Model Project GDC (Genome Data Center).
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
## Table of Contents
|
|
7
6
|
|
|
8
7
|
- [Installation](#installation)
|
|
@@ -19,7 +18,6 @@ A command-line tool for validating, encrypting, and uploading submissions to a g
|
|
|
19
18
|
- [License](#license)
|
|
20
19
|
- [Acknowledgements](#acknowledgements)
|
|
21
20
|
|
|
22
|
-
|
|
23
21
|
## Installation
|
|
24
22
|
|
|
25
23
|
### Requirements
|
|
@@ -34,7 +32,6 @@ For example:
|
|
|
34
32
|
- Docker container
|
|
35
33
|
- Windows Subsystem for Linux
|
|
36
34
|
|
|
37
|
-
|
|
38
35
|
### Using [Conda](https://conda.io) (recommended)
|
|
39
36
|
|
|
40
37
|
If Conda is not yet available on your system, we recommend to install it through the [Miniforge Conda installer](https://github.com/conda-forge/miniforge) by running the following commands:
|
|
@@ -60,7 +57,6 @@ Use the following command to update the tool:
|
|
|
60
57
|
conda update -n grz-tools -c conda-forge -c bioconda grz-cli
|
|
61
58
|
```
|
|
62
59
|
|
|
63
|
-
|
|
64
60
|
### Using pip (not recommended)
|
|
65
61
|
|
|
66
62
|
While installation via `pip` is possible, it is not recommended because users should create/manage a Python virtual environment and must ensure that the correct Python version is being used.
|
|
@@ -77,7 +73,6 @@ Use the following command to update the tool:
|
|
|
77
73
|
pip upgrade grz-cli
|
|
78
74
|
```
|
|
79
75
|
|
|
80
|
-
|
|
81
76
|
### Using Docker
|
|
82
77
|
|
|
83
78
|
Docker images are available via biocontainers at [https://biocontainers.pro/tools/grz-cli](https://biocontainers.pro/tools/grz-cli).
|
|
@@ -101,7 +96,7 @@ The S3 secrets can either be directly within the config file or as defined with
|
|
|
101
96
|
|
|
102
97
|
It is recommended to have the following folder structure for a single submission:
|
|
103
98
|
|
|
104
|
-
```
|
|
99
|
+
```text
|
|
105
100
|
EXAMPLE_SUBMISSION
|
|
106
101
|
├── files
|
|
107
102
|
│ ├── aaaaaaaa00000000aaaaaaaa00000000_blood_normal.read1.fastq.gz
|
|
@@ -119,7 +114,6 @@ The only requirements are that `metadata/metadata.json` exists and the `files/`
|
|
|
119
114
|
Data files may be nested under subfolders inside `files/` for better organization.
|
|
120
115
|
For example, each donor could have their own folder for files.
|
|
121
116
|
|
|
122
|
-
|
|
123
117
|
### Example submission procedure
|
|
124
118
|
|
|
125
119
|
After preparing your submission as outlined above, you can use the following command to validate, encrypt and upload the submission:
|
|
@@ -148,7 +142,6 @@ It combines the `validate`, `encrypt`, and `upload` commands (see below).
|
|
|
148
142
|
grz-cli submit --submission-dir foo
|
|
149
143
|
```
|
|
150
144
|
|
|
151
|
-
|
|
152
145
|
### validate
|
|
153
146
|
|
|
154
147
|
It is recommended to run this command before continuing with encryption and upload.
|
|
@@ -209,7 +202,7 @@ We recommend using Conda or [Pixi](https://pixi.sh/latest).
|
|
|
209
202
|
|
|
210
203
|
After cloning the desired branch of the `grz-tools` repo locally, you can run `grz-cli` directly from the repo using:
|
|
211
204
|
|
|
212
|
-
```
|
|
205
|
+
```text
|
|
213
206
|
uv run --project path/to/cloned/grz-tools grz-cli --help
|
|
214
207
|
```
|
|
215
208
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Version compatibility check
|
|
2
|
+
|
|
3
|
+
When you run `grz-cli`, it automatically checks whether your installed version is compatible with the current requirements.
|
|
4
|
+
If your version is too old, too new, or simply behind the recommended release, you will be informed before any submission processing tasks begin.
|
|
5
|
+
|
|
6
|
+
## What happens when you run grz-cli
|
|
7
|
+
|
|
8
|
+
On startup, the tool fetches a version policy from a central location and compares it against your installed version.
|
|
9
|
+
Depending on the result, one of four things will happen:
|
|
10
|
+
|
|
11
|
+
| Situation | What you'll see | Does execution continue? |
|
|
12
|
+
| --------------------------------------------------- | -------------------- | ------------------------ |
|
|
13
|
+
| Your version is too old | Error message | No |
|
|
14
|
+
| Your version works but is behind the recommendation | Warning message | Yes |
|
|
15
|
+
| Your version is newer than the supported maximum | Error message | No |
|
|
16
|
+
| Your version is within the supported range | Confirmation message | Yes |
|
|
17
|
+
|
|
18
|
+
If no policy is currently in effect, the check is silently skipped and execution continues normally.
|
|
19
|
+
|
|
20
|
+
## What the messages mean
|
|
21
|
+
|
|
22
|
+
**"Your grz-cli version is not supported"**
|
|
23
|
+
|
|
24
|
+
Your installed version is below the minimum required.
|
|
25
|
+
You must upgrade before you can continue.
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
Example message: Your grz-cli version (1.2.0) is not supported. Minimum required version is 1.5.0.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**"Upgrading is strongly recommended"**
|
|
32
|
+
|
|
33
|
+
Your version meets the minimum requirement but is behind the recommended release.
|
|
34
|
+
You can continue, but upgrading is advised.
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
Example message: You are using grz-cli 1.5.0, but the recommended version is 1.7.0.
|
|
38
|
+
Upgrading is strongly recommended.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**"grz-cli is within the supported and tested range"**
|
|
42
|
+
|
|
43
|
+
Your version is fully compatible.
|
|
44
|
+
No action is needed.
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
Example message: grz-cli 1.7.0 is within the supported and tested range.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**"Version is newer than the maximum supported version"**
|
|
51
|
+
|
|
52
|
+
Your version is ahead of what has been tested and approved.
|
|
53
|
+
You must downgrade before you can continue.
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
Example message: grz-cli version 2.1.0 is newer than the maximum supported version (2.0.0).
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Why is there a maximum version limit?
|
|
60
|
+
|
|
61
|
+
Most tools only enforce a minimum version.
|
|
62
|
+
`grz-cli` also enforces an upper bound to prevent untested releases from being used in regulated submission workflows, where the exact toolchain version may need to match what was validated.
|
|
63
|
+
If you have installed a pre-release or a newer version than your organisation currently supports, downgrade to the approved version before submitting.
|
|
@@ -14,7 +14,7 @@ authors = [
|
|
|
14
14
|
dynamic = ["version"]
|
|
15
15
|
dependencies = [
|
|
16
16
|
"packaging >=23,<27",
|
|
17
|
-
"grz-common >=
|
|
17
|
+
"grz-common >=4.0.2,<5",
|
|
18
18
|
]
|
|
19
19
|
classifiers = [
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
@@ -30,6 +30,7 @@ test = [
|
|
|
30
30
|
"pytest-cov",
|
|
31
31
|
"pytest-mock",
|
|
32
32
|
"pytest-sugar",
|
|
33
|
+
"pytest-xdist",
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.0.1"
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from textwrap import dedent
|
|
12
12
|
|
|
13
13
|
import click
|
|
14
|
+
import grz_common.exceptions as grzexc
|
|
14
15
|
import grz_pydantic_models.submission.metadata
|
|
15
16
|
from grz_common.cli import config_file
|
|
16
17
|
from grz_common.cli.dump_config import dump_config
|
|
@@ -95,7 +96,12 @@ def main():
|
|
|
95
96
|
Main entry point for the CLI application.
|
|
96
97
|
"""
|
|
97
98
|
cli = build_cli()
|
|
98
|
-
|
|
99
|
+
try:
|
|
100
|
+
cli()
|
|
101
|
+
except grzexc.GrzError as e:
|
|
102
|
+
# an expected failure, such as an invalid metadata.json: log its message, not a traceback
|
|
103
|
+
log.error(e)
|
|
104
|
+
sys.exit(1)
|
|
99
105
|
|
|
100
106
|
|
|
101
107
|
if __name__ == "__main__":
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from tempfile import NamedTemporaryFile
|
|
7
6
|
from typing import Any
|
|
8
7
|
|
|
9
8
|
import click
|
|
10
9
|
import grz_common.cli as grzcli
|
|
10
|
+
from grz_common.models.base import get_secret_value
|
|
11
|
+
from grz_common.utils.crypt import Crypt4GH
|
|
11
12
|
from grz_common.workers.worker import Worker
|
|
12
13
|
|
|
13
14
|
from ..models.config import EncryptConfig
|
|
@@ -34,9 +35,26 @@ def encrypt(configuration: dict[str, Any], submission_dir, force, check_validati
|
|
|
34
35
|
"""
|
|
35
36
|
config = EncryptConfig.model_validate(configuration)
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
if config.keys.grz_public_key is not None:
|
|
39
|
+
grz_public_key = Crypt4GH.load_public_key(config.keys.grz_public_key, key_name="keys.grz_public_key")
|
|
40
|
+
elif config.keys.grz_public_key_path is not None:
|
|
41
|
+
grz_public_key = Crypt4GH.retrieve_public_key(config.keys.grz_public_key_path)
|
|
42
|
+
else:
|
|
43
|
+
# This case cannot occur here, but an explicit check is needed for type-checking.
|
|
44
|
+
sys.exit("Either keys.grz_public_key or keys.grz_public_key_path must be set for encryption.")
|
|
45
|
+
|
|
46
|
+
submitter_private_key = None
|
|
47
|
+
passphrase = get_secret_value(config.keys.submitter_private_key_passphrase)
|
|
48
|
+
if config.keys.submitter_private_key is not None:
|
|
49
|
+
submitter_private_key = Crypt4GH.load_private_key(
|
|
50
|
+
config.keys.submitter_private_key.get_secret_value(),
|
|
51
|
+
passphrase=passphrase,
|
|
52
|
+
key_name="keys.submitter_private_key",
|
|
53
|
+
)
|
|
54
|
+
elif config.keys.submitter_private_key_path is not None:
|
|
55
|
+
submitter_private_key = Crypt4GH.retrieve_private_key(
|
|
56
|
+
config.keys.submitter_private_key_path, passphrase=passphrase
|
|
57
|
+
)
|
|
40
58
|
|
|
41
59
|
log.info("Starting encryption...")
|
|
42
60
|
|
|
@@ -48,25 +66,11 @@ def encrypt(configuration: dict[str, Any], submission_dir, force, check_validati
|
|
|
48
66
|
log_dir=submission_dir / "logs",
|
|
49
67
|
encrypted_files_dir=submission_dir / "encrypted_files",
|
|
50
68
|
)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
submitter_private_key_path=submitter_privkey_path,
|
|
58
|
-
force=force,
|
|
59
|
-
check_validation_logs=check_validation_logs,
|
|
60
|
-
)
|
|
61
|
-
else:
|
|
62
|
-
# This case cannot occur here, but an explicit check is needed for type-checking.
|
|
63
|
-
if config.keys.grz_public_key_path is None:
|
|
64
|
-
sys.exit("GRZ public key path is required for encryption.")
|
|
65
|
-
worker_inst.encrypt(
|
|
66
|
-
config.keys.grz_public_key_path,
|
|
67
|
-
submitter_private_key_path=submitter_privkey_path,
|
|
68
|
-
force=force,
|
|
69
|
-
check_validation_logs=check_validation_logs,
|
|
70
|
-
)
|
|
69
|
+
worker_inst.encrypt(
|
|
70
|
+
grz_public_key,
|
|
71
|
+
submitter_private_key=submitter_private_key,
|
|
72
|
+
force=force,
|
|
73
|
+
check_validation_logs=check_validation_logs,
|
|
74
|
+
)
|
|
71
75
|
|
|
72
76
|
log.info("Encryption successful!")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
from grz_common.models.base import Crypt4GHPublicKey, FilePath, IgnoringBaseModel
|
|
5
|
+
from grz_common.models.identifiers import IdentifiersConfigModel
|
|
6
|
+
from grz_common.models.s3 import S3ConfigModel
|
|
7
|
+
from pydantic import ConfigDict, SecretStr, model_validator
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class KeyModel(IgnoringBaseModel):
|
|
13
|
+
grz_public_key: Crypt4GHPublicKey | None = None
|
|
14
|
+
"""
|
|
15
|
+
The public key of the recipient (the associated GRZ).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
grz_public_key_path: FilePath | None = None
|
|
19
|
+
"""
|
|
20
|
+
Path to the crypt4gh public key of the recipient (the associated GRZ).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
submitter_private_key: SecretStr | None = None
|
|
24
|
+
"""
|
|
25
|
+
The submitter's crypt4gh private key (optional).
|
|
26
|
+
grz-cli signs the encrypted files with this key if it is set.
|
|
27
|
+
If neither this field nor ``submitter_private_key_path`` is set, grz-cli signs them with a random key.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
submitter_private_key_path: FilePath | None = None
|
|
31
|
+
"""
|
|
32
|
+
Path to the submitter's crypt4gh private key (optional).
|
|
33
|
+
grz-cli signs the encrypted files with this key if it is set.
|
|
34
|
+
If neither this field nor ``submitter_private_key`` is set, grz-cli signs them with a random key.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
submitter_private_key_passphrase: SecretStr | None = None
|
|
38
|
+
"""
|
|
39
|
+
Passphrase to the submitter's crypt4gh private key (optional).
|
|
40
|
+
grz-cli takes the passphrase from this field, else from ``C4GH_PASSPHRASE``, else from a prompt.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@model_validator(mode="after")
|
|
44
|
+
def validate_grz_public_key(self) -> Self:
|
|
45
|
+
if self.grz_public_key is None and self.grz_public_key_path is None:
|
|
46
|
+
raise ValueError("Either grz_public_key or grz_public_key_path must be set.")
|
|
47
|
+
if self.grz_public_key is not None and self.grz_public_key_path is not None:
|
|
48
|
+
raise ValueError("Only one of grz_public_key or grz_public_key_path must be set.")
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
@model_validator(mode="after")
|
|
52
|
+
def validate_submitter_private_key(self) -> Self:
|
|
53
|
+
if self.submitter_private_key is not None and self.submitter_private_key_path is not None:
|
|
54
|
+
raise ValueError("Only one of submitter_private_key or submitter_private_key_path must be set.")
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class KeyConfigModel(IgnoringBaseModel):
|
|
59
|
+
# errors would show the raw input, and SecretStr does not mask the submitter private key and its passphrase there
|
|
60
|
+
model_config = ConfigDict(hide_input_in_errors=True)
|
|
61
|
+
|
|
62
|
+
keys: KeyModel
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class UploadConfig(S3ConfigModel):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class EncryptConfig(KeyConfigModel):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ValidateConfig(IdentifiersConfigModel):
|
|
74
|
+
pass
|
|
@@ -4,7 +4,7 @@ from datetime import UTC, datetime
|
|
|
4
4
|
from importlib.metadata import version
|
|
5
5
|
|
|
6
6
|
from grz_common.models.s3 import S3Options
|
|
7
|
-
from grz_common.models.version import VersionFile, VersionInfo
|
|
7
|
+
from grz_common.models.version import VERSION_FILE_KEY, VersionFile, VersionInfo
|
|
8
8
|
from packaging import version as pkg_version
|
|
9
9
|
|
|
10
10
|
logger = logging.getLogger(__name__)
|
|
@@ -37,7 +37,7 @@ def _select_active_policy(
|
|
|
37
37
|
|
|
38
38
|
def check_version_and_exit_if_needed(
|
|
39
39
|
s3_options: S3Options,
|
|
40
|
-
version_file_key: str =
|
|
40
|
+
version_file_key: str = VERSION_FILE_KEY,
|
|
41
41
|
) -> None:
|
|
42
42
|
"""Validate the installed grz-cli version against the policy defined in version.json."""
|
|
43
43
|
version_file = VersionFile.from_s3(s3_options, version_file_key)
|
|
@@ -46,18 +46,6 @@ def check_version_and_exit_if_needed(
|
|
|
46
46
|
_check_policy_and_exit_if_needed("grz-cli", current_version, version_file.grzcli_version)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def check_metadata_version_and_exit_if_needed(
|
|
50
|
-
s3_options: S3Options,
|
|
51
|
-
metadata_schema_version: str,
|
|
52
|
-
version_file_key: str = "version.json",
|
|
53
|
-
) -> None:
|
|
54
|
-
"""Validate the metadata schema version against the policy defined in version.json."""
|
|
55
|
-
version_file = VersionFile.from_s3(s3_options, version_file_key)
|
|
56
|
-
|
|
57
|
-
current_version = pkg_version.Version(metadata_schema_version)
|
|
58
|
-
_check_policy_and_exit_if_needed("metadata", current_version, version_file.metadata_version)
|
|
59
|
-
|
|
60
|
-
|
|
61
49
|
def _check_policy_and_exit_if_needed(
|
|
62
50
|
subject: str,
|
|
63
51
|
current_version: pkg_version.Version,
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Tests for ``KeyModel``, the keys section of the grz-cli config, which moved here from grz-common."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from grz_cli.models.config import EncryptConfig, KeyModel
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
PUBLIC_KEY = "-----BEGIN CRYPT4GH PUBLIC KEY-----\n7JZ9eRjhOo1zB8HfoQK1ULCR3Wpnl91hF2K8FtpmeQ8=\n-----END CRYPT4GH PUBLIC KEY-----\n"
|
|
11
|
+
"""A crypt4gh public key as text. ``Crypt4GHPublicKey`` only checks for the markers, so it need not decode."""
|
|
12
|
+
|
|
13
|
+
SUBMITTER_PRIVATE_KEY = "submitter-private-key"
|
|
14
|
+
"""Stands for a private key given inline. The model does not parse it, so it need not be a key."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_grz_common_keeps_no_copy_of_the_key_models():
|
|
18
|
+
assert importlib.util.find_spec("grz_common.models.keys") is None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_encrypt_config_reads_the_keys_section(tmp_path: Path):
|
|
22
|
+
public_key_path = tmp_path / "grz.pub"
|
|
23
|
+
public_key_path.write_text(PUBLIC_KEY)
|
|
24
|
+
|
|
25
|
+
config = EncryptConfig.model_validate({"keys": {"grz_public_key_path": str(public_key_path)}})
|
|
26
|
+
|
|
27
|
+
assert config.keys.grz_public_key_path == public_key_path
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_a_config_with_the_removed_grz_private_key_path_still_loads():
|
|
31
|
+
"""grz-cli never read ``keys.grz_private_key_path``. Old configs still set it, so the model ignores it."""
|
|
32
|
+
keys = KeyModel.model_validate({"grz_public_key": PUBLIC_KEY, "grz_private_key_path": "/no/such/key.sec"})
|
|
33
|
+
|
|
34
|
+
assert "grz_private_key_path" not in keys.model_dump()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_neither_grz_public_key_nor_grz_public_key_path_fails():
|
|
38
|
+
with pytest.raises(ValidationError, match="Either grz_public_key or grz_public_key_path must be set"):
|
|
39
|
+
KeyModel()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_both_grz_public_key_and_grz_public_key_path_fails(tmp_path: Path):
|
|
43
|
+
public_key_path = tmp_path / "grz.pub"
|
|
44
|
+
public_key_path.write_text(PUBLIC_KEY)
|
|
45
|
+
|
|
46
|
+
with pytest.raises(ValidationError, match="Only one of grz_public_key or grz_public_key_path must be set"):
|
|
47
|
+
KeyModel(grz_public_key=PUBLIC_KEY, grz_public_key_path=public_key_path)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize(
|
|
51
|
+
"public_key",
|
|
52
|
+
[
|
|
53
|
+
PUBLIC_KEY,
|
|
54
|
+
"-----BEGIN CRYPT4GH PUBLIC KEY-----\n7JZ9eRjhOo1zB8HfoQK1ULCR3Wpnl91hF2K8FtpmeQ8=\n",
|
|
55
|
+
"7JZ9eRjhOo1zB8HfoQK1ULCR3Wpnl91hF2K8FtpmeQ8=\n-----END CRYPT4GH PUBLIC KEY-----\n",
|
|
56
|
+
],
|
|
57
|
+
ids=["both markers", "BEGIN marker only", "END marker only"],
|
|
58
|
+
)
|
|
59
|
+
def test_grz_public_key_with_a_marker_passes(public_key: str):
|
|
60
|
+
assert KeyModel(grz_public_key=public_key).grz_public_key == public_key
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_grz_public_key_without_markers_fails():
|
|
64
|
+
with pytest.raises(ValidationError, match="Invalid public key format"):
|
|
65
|
+
KeyModel(grz_public_key="7JZ9eRjhOo1zB8HfoQK1ULCR3Wpnl91hF2K8FtpmeQ8=")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_both_submitter_private_key_and_submitter_private_key_path_fails(tmp_path: Path):
|
|
69
|
+
private_key_path = tmp_path / "submitter.sec"
|
|
70
|
+
private_key_path.write_text(SUBMITTER_PRIVATE_KEY)
|
|
71
|
+
|
|
72
|
+
with pytest.raises(
|
|
73
|
+
ValidationError, match="Only one of submitter_private_key or submitter_private_key_path must be set"
|
|
74
|
+
):
|
|
75
|
+
KeyModel(
|
|
76
|
+
grz_public_key=PUBLIC_KEY,
|
|
77
|
+
submitter_private_key=SUBMITTER_PRIVATE_KEY,
|
|
78
|
+
submitter_private_key_path=private_key_path,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_the_submitter_private_key_is_masked_in_dumps():
|
|
83
|
+
keys = KeyModel(grz_public_key=PUBLIC_KEY, submitter_private_key=SUBMITTER_PRIVATE_KEY)
|
|
84
|
+
|
|
85
|
+
assert SUBMITTER_PRIVATE_KEY not in repr(keys)
|
|
86
|
+
assert SUBMITTER_PRIVATE_KEY not in keys.model_dump_json()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_the_submitter_private_key_passphrase_is_masked_in_dumps():
|
|
90
|
+
passphrase = "submitter-passphrase"
|
|
91
|
+
keys = KeyModel(grz_public_key=PUBLIC_KEY, submitter_private_key_passphrase=passphrase)
|
|
92
|
+
|
|
93
|
+
assert passphrase not in repr(keys)
|
|
94
|
+
assert passphrase not in keys.model_dump_json()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_a_config_error_does_not_show_the_submitter_private_key(tmp_path: Path):
|
|
98
|
+
private_key_path = tmp_path / "submitter.sec"
|
|
99
|
+
private_key_path.write_text(SUBMITTER_PRIVATE_KEY)
|
|
100
|
+
keys = {
|
|
101
|
+
"grz_public_key": PUBLIC_KEY,
|
|
102
|
+
"submitter_private_key": SUBMITTER_PRIVATE_KEY,
|
|
103
|
+
"submitter_private_key_path": str(private_key_path),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
with pytest.raises(ValidationError) as exc_info:
|
|
107
|
+
EncryptConfig.model_validate({"keys": keys})
|
|
108
|
+
|
|
109
|
+
assert SUBMITTER_PRIVATE_KEY not in str(exc_info.value)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Tests for ``grz-cli encrypt``, which loads the submitter private key with its configured passphrase."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click.testing
|
|
6
|
+
import crypt4gh.keys
|
|
7
|
+
import crypt4gh.keys.c4gh
|
|
8
|
+
import pytest
|
|
9
|
+
import yaml
|
|
10
|
+
from grz_cli.cli import build_cli
|
|
11
|
+
from pytest_mock import MockerFixture
|
|
12
|
+
|
|
13
|
+
PASSPHRASE = "right-passphrase"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def no_prompt(monkeypatch):
|
|
18
|
+
"""Fail the test if the passphrase prompt opens, and set a wrong ``C4GH_PASSPHRASE``.
|
|
19
|
+
|
|
20
|
+
The configured passphrase comes first, so the wrong one in the environment must not matter.
|
|
21
|
+
"""
|
|
22
|
+
monkeypatch.setenv("C4GH_PASSPHRASE", "wrong-passphrase")
|
|
23
|
+
|
|
24
|
+
def _fail(*args, **kwargs):
|
|
25
|
+
raise AssertionError("the passphrase prompt must not open")
|
|
26
|
+
|
|
27
|
+
monkeypatch.setattr("grz_common.utils.crypt.getpass", _fail)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.parametrize("inline", [False, True], ids=["path", "inline"])
|
|
31
|
+
def test_encrypt_signs_with_the_submitter_key_and_its_configured_passphrase(
|
|
32
|
+
tmp_path: Path, submission_dir: Path, mocker: MockerFixture, no_prompt, inline: bool
|
|
33
|
+
):
|
|
34
|
+
crypt4gh.keys.c4gh.generate(tmp_path / "grz.sec", tmp_path / "grz.pub", None, comment=None)
|
|
35
|
+
private_key_path = tmp_path / "le.sec"
|
|
36
|
+
crypt4gh.keys.c4gh.generate(private_key_path, tmp_path / "le.pub", PASSPHRASE.encode(), comment=None)
|
|
37
|
+
keys = {"grz_public_key_path": str(tmp_path / "grz.pub"), "submitter_private_key_passphrase": PASSPHRASE}
|
|
38
|
+
if inline:
|
|
39
|
+
keys["submitter_private_key"] = private_key_path.read_text()
|
|
40
|
+
else:
|
|
41
|
+
keys["submitter_private_key_path"] = str(private_key_path)
|
|
42
|
+
config_path = tmp_path / "config.yaml"
|
|
43
|
+
config_path.write_text(yaml.dump({"keys": keys}))
|
|
44
|
+
worker_encrypt = mocker.patch("grz_cli.commands.encrypt.Worker.encrypt")
|
|
45
|
+
|
|
46
|
+
result = click.testing.CliRunner().invoke(
|
|
47
|
+
build_cli(),
|
|
48
|
+
["encrypt", "--submission-dir", str(submission_dir), "--config-file", str(config_path)],
|
|
49
|
+
catch_exceptions=False,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert result.exit_code == 0, result.output
|
|
53
|
+
signing_key = worker_encrypt.call_args.kwargs["submitter_private_key"]
|
|
54
|
+
assert signing_key.private_bytes_raw() == crypt4gh.keys.get_private_key(private_key_path, lambda: PASSPHRASE)
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# Version compatibility check
|
|
2
|
-
|
|
3
|
-
When you run `grz-cli`, it automatically checks whether your installed version is compatible with the current requirements. If your version is too old, too new, or simply behind the recommended release, you will be informed before any submission processing tasks begin.
|
|
4
|
-
|
|
5
|
-
## What happens when you run grz-cli
|
|
6
|
-
|
|
7
|
-
On startup, the tool fetches a version policy from a central location and compares it against your installed version. Depending on the result, one of four things will happen:
|
|
8
|
-
|
|
9
|
-
| Situation | What you'll see | Does execution continue? |
|
|
10
|
-
|---|---|---|
|
|
11
|
-
| Your version is too old | Error message | No |
|
|
12
|
-
| Your version works but is behind the recommendation | Warning message | Yes |
|
|
13
|
-
| Your version is newer than the supported maximum | Error message | No |
|
|
14
|
-
| Your version is within the supported range | Confirmation message | Yes |
|
|
15
|
-
|
|
16
|
-
If no policy is currently in effect, the check is silently skipped and execution continues normally.
|
|
17
|
-
|
|
18
|
-
## What the messages mean
|
|
19
|
-
|
|
20
|
-
**"Your grz-cli version is not supported"**
|
|
21
|
-
|
|
22
|
-
Your installed version is below the minimum required. You must upgrade before you can continue.
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
Example message: Your grz-cli version (1.2.0) is not supported. Minimum required version is 1.5.0.
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
**"Upgrading is strongly recommended"**
|
|
29
|
-
|
|
30
|
-
Your version meets the minimum requirement but is behind the recommended release. You can continue, but upgrading is advised.
|
|
31
|
-
|
|
32
|
-
```
|
|
33
|
-
Example message: You are using grz-cli 1.5.0, but the recommended version is 1.7.0.
|
|
34
|
-
Upgrading is strongly recommended.
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
**"grz-cli is within the supported and tested range"**
|
|
38
|
-
|
|
39
|
-
Your version is fully compatible. No action is needed.
|
|
40
|
-
|
|
41
|
-
```
|
|
42
|
-
Example message: grz-cli 1.7.0 is within the supported and tested range.
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
**"Version is newer than the maximum supported version"**
|
|
46
|
-
|
|
47
|
-
Your version is ahead of what has been tested and approved. You must downgrade before you can continue.
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
Example message: grz-cli version 2.1.0 is newer than the maximum supported version (2.0.0).
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## Why is there a maximum version limit?
|
|
55
|
-
|
|
56
|
-
Most tools only enforce a minimum version. `grz-cli` also enforces an upper bound to prevent untested releases from being used in regulated submission workflows, where the exact toolchain version may need to match what was validated. If you have installed a pre-release or a newer version than your organisation currently supports, downgrade to the approved version before submitting.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.0.0"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
from grz_common.models.identifiers import IdentifiersConfigModel
|
|
4
|
-
from grz_common.models.keys import KeyConfigModel
|
|
5
|
-
from grz_common.models.s3 import S3ConfigModel
|
|
6
|
-
|
|
7
|
-
log = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class UploadConfig(S3ConfigModel):
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class EncryptConfig(KeyConfigModel):
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class ValidateConfig(IdentifiersConfigModel):
|
|
19
|
-
pass
|
|
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
|