icat-sync-parametertype 1.0.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.
@@ -0,0 +1,20 @@
1
+ # MIT License
2
+
3
+ **Copyright (c) 2024 European Synchrotron Radiation Facility**
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: icat-sync-parametertype
3
+ Version: 1.0.0
4
+ Summary: Python client to synchronize parameterType from the icat-esrf-definitions with the ICAT database
5
+ Author-email: ESRF <marjolaine.bodin@esrf.fr>
6
+ License: # MIT License
7
+
8
+ **Copyright (c) 2024 European Synchrotron Radiation Facility**
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
11
+ this software and associated documentation files (the "Software"), to deal in
12
+ the Software without restriction, including without limitation the rights to
13
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14
+ the Software, and to permit persons to whom the Software is furnished to do so,
15
+ subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
24
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
25
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+
27
+ Project-URL: Homepage, https://gitlab.esrf.fr/icat/data-sync/parametertype
28
+ Project-URL: Documentation, https://gitlab.esrf.fr/icat/data-sync/parametertype
29
+ Project-URL: Repository, https://gitlab.esrf.fr/icat/data-sync/parametertype
30
+ Project-URL: Issues, https://gitlab.esrf.fr/icat/data-sync/parametertype/issues
31
+ Project-URL: Changelog, https://gitlab.esrf.fr/icat/data-sync/parametertype/-/blob/main/CHANGELOG.md
32
+ Classifier: Intended Audience :: Science/Research
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Requires-Python: >=3.8
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE.md
38
+ Requires-Dist: python-icat
39
+ Requires-Dist: icat-esrf-definitions
40
+ Provides-Extra: test
41
+ Requires-Dist: pytest; extra == "test"
42
+ Provides-Extra: dev
43
+ Requires-Dist: icat-sync-parametertype[test]; extra == "dev"
44
+ Requires-Dist: ruff; extra == "dev"
45
+ Dynamic: license-file
46
+
47
+ # icat-sync-parametertype
48
+
49
+ A python client to synchronize the metadata defined in the [ICAT ESRF definitions](https://gitlab.esrf.fr/icat/hdf5-master-config) with the ICAT database.
50
+
51
+ ```bash
52
+ icat-sync-parametertype -w http://ovm-icat-test.esrf.fr:8080 -a db -u admin -P
53
+ ```
54
+
55
+ `-P` prompts for the password. The connection can also be read from a
56
+ [python-icat configuration file](https://python-icat.readthedocs.io/en/stable/config.html)
57
+ (`icat.cfg`, or the file given by the `ICAT_CFG` environment variable), selecting a section with `-s`.
58
+ Copy [icat.cfg.example](icat.cfg.example) to `icat.cfg` and fill in the credentials:
59
+
60
+ ```bash
61
+ icat-sync-parametertype -s test
62
+ ```
63
+
64
+ ## Options
65
+
66
+ - `--dry-run`: only show the changes, nothing is registered in ICAT.
67
+ - `--yes`: register the changes without asking for confirmation (for CI).
68
+
69
+ ## Exit codes
70
+
71
+ | Code | Meaning |
72
+ |------|---------|
73
+ | 0 | ICAT is up to date, or the changes were registered (or declined) |
74
+ | 1 | At least one change failed to be registered |
75
+ | 2 | `--dry-run` detected changes |
@@ -0,0 +1,29 @@
1
+ # icat-sync-parametertype
2
+
3
+ A python client to synchronize the metadata defined in the [ICAT ESRF definitions](https://gitlab.esrf.fr/icat/hdf5-master-config) with the ICAT database.
4
+
5
+ ```bash
6
+ icat-sync-parametertype -w http://ovm-icat-test.esrf.fr:8080 -a db -u admin -P
7
+ ```
8
+
9
+ `-P` prompts for the password. The connection can also be read from a
10
+ [python-icat configuration file](https://python-icat.readthedocs.io/en/stable/config.html)
11
+ (`icat.cfg`, or the file given by the `ICAT_CFG` environment variable), selecting a section with `-s`.
12
+ Copy [icat.cfg.example](icat.cfg.example) to `icat.cfg` and fill in the credentials:
13
+
14
+ ```bash
15
+ icat-sync-parametertype -s test
16
+ ```
17
+
18
+ ## Options
19
+
20
+ - `--dry-run`: only show the changes, nothing is registered in ICAT.
21
+ - `--yes`: register the changes without asking for confirmation (for CI).
22
+
23
+ ## Exit codes
24
+
25
+ | Code | Meaning |
26
+ |------|---------|
27
+ | 0 | ICAT is up to date, or the changes were registered (or declined) |
28
+ | 1 | At least one change failed to be registered |
29
+ | 2 | `--dry-run` detected changes |
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["setuptools>=46.4"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "icat-sync-parametertype"
7
+ version = "1.0.0"
8
+ authors = [{name = "ESRF", email = "marjolaine.bodin@esrf.fr"}]
9
+ description = "Python client to synchronize parameterType from the icat-esrf-definitions with the ICAT database"
10
+ readme = {file = "README.md", content-type = "text/markdown"}
11
+ license = {file = "LICENSE.md"}
12
+ classifiers = [
13
+ "Intended Audience :: Science/Research",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Programming Language :: Python :: 3",
16
+ ]
17
+ requires-python = ">=3.8"
18
+ dependencies = [
19
+ "python-icat",
20
+ "icat-esrf-definitions",
21
+ ]
22
+
23
+
24
+ [project.optional-dependencies]
25
+ test = [
26
+ "pytest"
27
+ ]
28
+ dev = [
29
+ "icat-sync-parametertype[test]",
30
+ "ruff"
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://gitlab.esrf.fr/icat/data-sync/parametertype"
35
+ Documentation = "https://gitlab.esrf.fr/icat/data-sync/parametertype"
36
+ Repository = "https://gitlab.esrf.fr/icat/data-sync/parametertype"
37
+ Issues = "https://gitlab.esrf.fr/icat/data-sync/parametertype/issues"
38
+ Changelog = "https://gitlab.esrf.fr/icat/data-sync/parametertype/-/blob/main/CHANGELOG.md"
39
+
40
+ [tool.setuptools]
41
+ package-dir = { "" = "src" }
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.setuptools.package-data]
47
+ "*"= ['*.txt', '*.json']
48
+
49
+ [tool.coverage.run]
50
+ omit = ['*/tests/*']
51
+
52
+ [tool.ruff]
53
+ line-length = 88
54
+
55
+ [tool.ruff.lint]
56
+ select = [
57
+ "E", # pycodestyle errors
58
+ "F", # pyflakes
59
+ "I", # isort
60
+ "N", # pep8-naming
61
+ "S", # flake8-bandit
62
+ ]
63
+ ignore = ["E501"]
64
+
65
+ [tool.ruff.lint.per-file-ignores]
66
+ "**/tests/**" = [
67
+ "S101", # allow asserts
68
+ ]
69
+
70
+ [tool.ruff.lint.isort]
71
+ force-single-line = true
72
+ known-first-party = ["icat_sync_parametertype"]
73
+
74
+
75
+ [project.scripts]
76
+ icat-sync-parametertype = "icat_sync_parametertype.apps.sync:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,37 @@
1
+ import logging
2
+ import sys
3
+
4
+ from icat_sync_parametertype.definition import get_icat_esrf_definition
5
+ from icat_sync_parametertype.icat_client import get_icat_parametertype
6
+ from icat_sync_parametertype.icat_client import setup_icat_client
7
+ from icat_sync_parametertype.icat_client import sync_parametertypes
8
+ from icat_sync_parametertype.logger import setup_logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def main() -> int:
14
+ setup_logging(level=logging.DEBUG)
15
+ client, conf = setup_icat_client()
16
+
17
+ logger.info("=" * 60)
18
+ logger.info("ICAT environment: %s", conf.configSection or "(command line)")
19
+ logger.info("ICAT server: %s", client.url)
20
+ logger.info("ICAT user: %s/%s", conf.auth, getattr(conf, "username", "-"))
21
+ logger.info("=" * 60)
22
+
23
+ logger.info("Updating ICAT parameterTypes using the ESRF icat definitions...")
24
+ icat_parametertypes = get_icat_parametertype(client)
25
+
26
+ metadata = get_icat_esrf_definition()
27
+ logger.info(f"Retrieved {len(metadata)} parameter types in ICAT ESRF definition")
28
+
29
+ exit_code = sync_parametertypes(
30
+ icat_parametertypes, metadata, client, dry_run=conf.dry_run, yes=conf.yes
31
+ )
32
+ logger.info("Synchronization finished.")
33
+ return exit_code
34
+
35
+
36
+ if __name__ == "__main__":
37
+ sys.exit(main())
@@ -0,0 +1,17 @@
1
+ from typing import Dict
2
+
3
+ from icat_esrf_definitions.models import IcatDatasetParameters
4
+
5
+
6
+ def get_icat_esrf_definition() -> Dict[str, Dict[str, str]]:
7
+ fields = IcatDatasetParameters.icat_fields()
8
+ metadata = {}
9
+ for field in fields.values():
10
+ units = str(field.unit_info.units) if field.unit_info else ""
11
+
12
+ metadata[field.icat_name] = {
13
+ "description": field.description or "",
14
+ "units": units,
15
+ "rename_to": field.new_icat_name,
16
+ }
17
+ return metadata
@@ -0,0 +1,288 @@
1
+ import logging
2
+ from typing import Any
3
+ from typing import Dict
4
+ from typing import List
5
+ from typing import Tuple
6
+
7
+ import icat
8
+ import icat.config
9
+ from icat.entity import Entity
10
+ from icat.query import Query
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def setup_icat_client() -> Tuple[icat.client.Client, icat.config.Configuration]:
16
+ config = icat.config.Config(ids=False)
17
+ config.add_variable(
18
+ "dry_run",
19
+ ("--dry-run",),
20
+ dict(help="only show the changes, exit with code 2 if there are any"),
21
+ type=icat.config.flag,
22
+ default=False,
23
+ )
24
+ config.add_variable(
25
+ "yes",
26
+ ("--yes",),
27
+ dict(help="register the changes without asking for confirmation"),
28
+ type=icat.config.flag,
29
+ default=False,
30
+ )
31
+ client, conf = config.getconfig()
32
+ client.login(conf.auth, conf.credentials)
33
+ return client, conf
34
+
35
+
36
+ def get_icat_parametertype(icat_client: icat.client.Client) -> List[Dict]:
37
+ try:
38
+ query = Query(icat_client, "ParameterType")
39
+ icat_parametertype = icat_client.search(query)
40
+ logger.info(
41
+ "Retrieved " + str(len(icat_parametertype)) + " parameterTypes in ICAT"
42
+ )
43
+ return icat_parametertype
44
+ except icat.SearchResultError as e:
45
+ logger.error(e)
46
+ return []
47
+
48
+
49
+ def is_parametertype_changed(
50
+ parametertype: Entity, new_description: str, new_units: str
51
+ ) -> bool:
52
+ current_description = parametertype.description or ""
53
+ current_units = getattr(parametertype, "units", "") or ""
54
+ return current_description != new_description or current_units != new_units
55
+
56
+
57
+ def update_parametertype(
58
+ client: icat.client.Client,
59
+ esrf_facility: Entity,
60
+ name: str,
61
+ description: str,
62
+ units: str,
63
+ register: bool,
64
+ ) -> bool:
65
+ try:
66
+ query = Query(client, "ParameterType", conditions={"name": "= '%s'" % name})
67
+ parametertype = client.assertedSearch(query)[0]
68
+ except icat.SearchResultError as e:
69
+ logger.error(e)
70
+ return False
71
+
72
+ if is_parametertype_changed(parametertype, description, units):
73
+ try:
74
+ parametertype.facility = esrf_facility
75
+ parametertype.applicableToDataCollection = (
76
+ parametertype.applicableToDataCollection
77
+ )
78
+ parametertype.applicableToDatafile = parametertype.applicableToDatafile
79
+ parametertype.applicableToDataset = parametertype.applicableToDataset
80
+ parametertype.applicableToInvestigation = (
81
+ parametertype.applicableToInvestigation
82
+ )
83
+ parametertype.applicableToSample = parametertype.applicableToSample
84
+ parametertype.applicableToSample = parametertype.applicableToSample
85
+ parametertype.maximumNumericValue = parametertype.maximumNumericValue
86
+ parametertype.minimumNumericValue = parametertype.minimumNumericValue
87
+ parametertype.unitsFullName = parametertype.unitsFullName
88
+ parametertype.valueType = parametertype.valueType
89
+
90
+ parametertype.description = description
91
+ parametertype.units = units
92
+
93
+ if register:
94
+ parametertype.update()
95
+ logger.info(
96
+ "ParameterType %s updated with description=%s unit=%s",
97
+ name,
98
+ description,
99
+ units,
100
+ )
101
+ except Exception as e:
102
+ logger.error("Failed to update ParameterType %s: %s", name, e)
103
+ return False
104
+ return True
105
+
106
+
107
+ def rename_parametertype(
108
+ client: icat.client.Client,
109
+ esrf_facility: Entity,
110
+ old_name: str,
111
+ new_name: str,
112
+ register: bool,
113
+ ) -> bool:
114
+ try:
115
+ query = Query(client, "ParameterType", conditions={"name": "= '%s'" % old_name})
116
+ parametertype = client.assertedSearch(query)[0]
117
+ except icat.SearchResultError as e:
118
+ logger.error(e)
119
+ return False
120
+ try:
121
+ parametertype.facility = esrf_facility
122
+ parametertype.applicableToDataCollection = (
123
+ parametertype.applicableToDataCollection
124
+ )
125
+ parametertype.applicableToDatafile = parametertype.applicableToDatafile
126
+ parametertype.applicableToDataset = parametertype.applicableToDataset
127
+ parametertype.applicableToInvestigation = (
128
+ parametertype.applicableToInvestigation
129
+ )
130
+ parametertype.applicableToSample = parametertype.applicableToSample
131
+ parametertype.applicableToSample = parametertype.applicableToSample
132
+ parametertype.maximumNumericValue = parametertype.maximumNumericValue
133
+ parametertype.minimumNumericValue = parametertype.minimumNumericValue
134
+ parametertype.unitsFullName = parametertype.unitsFullName
135
+ parametertype.valueType = parametertype.valueType
136
+
137
+ parametertype.description = parametertype.description
138
+ parametertype.units = parametertype.units
139
+
140
+ parametertype.name = new_name
141
+
142
+ if register:
143
+ parametertype.update()
144
+ logger.info("ParameterType %s renamed with name=%s", old_name, new_name)
145
+ except Exception as e:
146
+ logger.error("Failed to rename ParameterType %s: %s", old_name, e)
147
+ return False
148
+ return True
149
+
150
+
151
+ def create_parametertype(
152
+ client: icat.client.Client,
153
+ esrf_facility: Entity,
154
+ name: str,
155
+ description: str,
156
+ units: str,
157
+ register: bool,
158
+ ) -> bool:
159
+ try:
160
+ parametertype = client.new(
161
+ "ParameterType",
162
+ name=name,
163
+ facility=esrf_facility,
164
+ description=description,
165
+ units=units,
166
+ applicableToDataset=True,
167
+ valueType="STRING",
168
+ )
169
+ if register:
170
+ parametertype.create()
171
+ logger.info(
172
+ "ParameterType %s created with description=%s units=%s",
173
+ name,
174
+ description,
175
+ units,
176
+ )
177
+ except icat.ICATObjectExistsError:
178
+ logger.error("Error while creating the parametertype %s", name)
179
+ return False
180
+ return True
181
+
182
+
183
+ def get_facility(client: icat.client.Client) -> Entity:
184
+ return client.assertedSearch("Facility[name='ESRF']")[0]
185
+
186
+
187
+ def truncate_for_oracle(
188
+ value: str, max_bytes: int = 255, encoding: str = "utf-8"
189
+ ) -> str:
190
+ if value is None:
191
+ return ""
192
+ encoded = value.encode(encoding)
193
+ if len(encoded) <= max_bytes:
194
+ return value
195
+ return encoded[:max_bytes].decode(encoding, errors="ignore")
196
+
197
+
198
+ def sync_parametertypes(
199
+ icat_parametertypes: List[Any],
200
+ metadata: Dict[str, Dict[str, str]],
201
+ icat_client: icat.client.Client,
202
+ dry_run: bool = False,
203
+ yes: bool = False,
204
+ ) -> int:
205
+ """Return the exit code: 0 on success or nothing to do, 1 if an operation
206
+ failed, 2 if changes are detected in dry-run mode."""
207
+ icat_by_name = {p.name: p for p in icat_parametertypes}
208
+ esrf_facility = get_facility(icat_client)
209
+ operations = []
210
+
211
+ for name, field in metadata.items():
212
+ new_name = field.get("rename_to")
213
+ if new_name and name in icat_by_name:
214
+ operations.append(("rename", name, new_name))
215
+ icat_by_name[new_name] = icat_by_name.pop(name)
216
+
217
+ for name, field in metadata.items():
218
+ if field.get("rename_to") in metadata:
219
+ continue
220
+ description = field.get("description") or ""
221
+ description = truncate_for_oracle(description)
222
+ units = field.get("units") or "NA"
223
+ target_name = field.get("rename_to") or name
224
+
225
+ if target_name in icat_by_name:
226
+ param = icat_by_name[target_name]
227
+ if is_parametertype_changed(param, description, units):
228
+ operations.append(("update", target_name, description, units))
229
+ else:
230
+ operations.append(("create", target_name, description, units))
231
+
232
+ if not operations:
233
+ logger.info("ICAT is up to date, no changes detected.")
234
+ return 0
235
+
236
+ logger.info("The following changes are detected:")
237
+ for op in operations:
238
+ if op[0] == "rename":
239
+ logger.info(f" - Rename '{op[1]}' -> '{op[2]}'")
240
+ elif op[0] == "update":
241
+ logger.info(f" - Update '{op[1]}' (description/units changed)")
242
+ elif op[0] == "create":
243
+ logger.info(f" - Create '{op[1]}'")
244
+
245
+ if dry_run:
246
+ logger.warning("Dry run: no changes will be registered into ICAT!")
247
+ return 2
248
+ if yes:
249
+ register = True
250
+ else:
251
+ response = (
252
+ input(
253
+ f"Do you want to register these changes into ICAT {icat_client.url}? [y/N]: "
254
+ )
255
+ .strip()
256
+ .lower()
257
+ )
258
+ register = response == "y"
259
+ if not register:
260
+ logger.warning("No changes will be registered into ICAT!")
261
+ return 0
262
+
263
+ failures = 0
264
+
265
+ for op in operations:
266
+ if op[0] == "rename":
267
+ _, old, new = op
268
+ ok = rename_parametertype(icat_client, esrf_facility, old, new, register)
269
+
270
+ elif op[0] == "update":
271
+ _, name, description, units = op
272
+ ok = update_parametertype(
273
+ icat_client, esrf_facility, name, description, units, register
274
+ )
275
+
276
+ elif op[0] == "create":
277
+ _, name, description, units = op
278
+ ok = create_parametertype(
279
+ icat_client, esrf_facility, name, description, units, register
280
+ )
281
+
282
+ if not ok:
283
+ failures += 1
284
+
285
+ if failures:
286
+ logger.error("%d of %d changes failed", failures, len(operations))
287
+ return 1
288
+ return 0
@@ -0,0 +1,19 @@
1
+ import logging
2
+ import sys
3
+
4
+
5
+ def setup_logging(level=logging.INFO) -> None:
6
+ formatter = logging.Formatter(
7
+ "[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s",
8
+ datefmt="%Y-%m-%d %H:%M:%S",
9
+ )
10
+
11
+ handler = logging.StreamHandler(sys.stdout)
12
+ handler.setFormatter(formatter)
13
+
14
+ root_logger = logging.getLogger()
15
+ root_logger.setLevel(logging.WARNING)
16
+ root_logger.handlers.clear()
17
+ root_logger.addHandler(handler)
18
+
19
+ logging.getLogger("icat_sync_parametertype").setLevel(level)
@@ -0,0 +1,86 @@
1
+ from types import SimpleNamespace
2
+
3
+ import pytest
4
+
5
+ from icat_sync_parametertype import icat_client
6
+
7
+
8
+ def param(name, description="desc", units="NA"):
9
+ return SimpleNamespace(name=name, description=description, units=units)
10
+
11
+
12
+ def field(description="desc", units="", rename_to=None):
13
+ return {"description": description, "units": units, "rename_to": rename_to}
14
+
15
+
16
+ @pytest.fixture
17
+ def calls(monkeypatch):
18
+ """Record the ICAT operations instead of sending them."""
19
+ calls = []
20
+ monkeypatch.setattr(icat_client, "get_facility", lambda client: None)
21
+
22
+ def recorder(op):
23
+ def record(client, facility, *args):
24
+ calls.append((op, *args[:-1])) # drop the register flag
25
+ return True
26
+
27
+ return record
28
+
29
+ for op in ("create", "update", "rename"):
30
+ monkeypatch.setattr(icat_client, f"{op}_parametertype", recorder(op))
31
+ return calls
32
+
33
+
34
+ def sync(icat_params, metadata, **kwargs):
35
+ client = SimpleNamespace(url="https://icat.test")
36
+ return icat_client.sync_parametertypes(icat_params, metadata, client, **kwargs)
37
+
38
+
39
+ def test_up_to_date(calls):
40
+ assert sync([param("A")], {"A": field()}, yes=True) == 0
41
+ assert calls == []
42
+
43
+
44
+ def test_create_and_update(calls):
45
+ metadata = {"A": field(description="new"), "B": field(units="mm")}
46
+ assert sync([param("A")], metadata, yes=True) == 0
47
+ assert calls == [("update", "A", "new", "NA"), ("create", "B", "desc", "mm")]
48
+
49
+
50
+ def test_rename_uses_new_entry_values(calls):
51
+ metadata = {
52
+ "old": field(description="old desc", rename_to="new"),
53
+ "new": field(description="new desc"),
54
+ }
55
+ assert sync([param("old", description="new desc")], metadata, yes=True) == 0
56
+ assert calls == [("rename", "old", "new")]
57
+
58
+
59
+ def test_renamed_field_is_stable(calls):
60
+ metadata = {
61
+ "old": field(description="old desc", rename_to="new"),
62
+ "new": field(description="new desc"),
63
+ }
64
+ assert sync([param("new", description="new desc")], metadata, yes=True) == 0
65
+ assert calls == []
66
+
67
+
68
+ def test_quotes_are_not_doubled(calls):
69
+ sync([], {"A": field(description="it's")}, yes=True)
70
+ assert calls == [("create", "A", "it's", "NA")]
71
+
72
+
73
+ def test_dry_run(calls):
74
+ assert sync([], {"A": field()}, dry_run=True) == 2
75
+ assert calls == []
76
+
77
+
78
+ def test_failure(calls, monkeypatch):
79
+ monkeypatch.setattr(icat_client, "create_parametertype", lambda *args: False)
80
+ assert sync([], {"A": field()}, yes=True) == 1
81
+
82
+
83
+ def test_declined(calls, monkeypatch):
84
+ monkeypatch.setattr("builtins.input", lambda prompt: "n")
85
+ assert sync([], {"A": field()}) == 0
86
+ assert calls == []
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: icat-sync-parametertype
3
+ Version: 1.0.0
4
+ Summary: Python client to synchronize parameterType from the icat-esrf-definitions with the ICAT database
5
+ Author-email: ESRF <marjolaine.bodin@esrf.fr>
6
+ License: # MIT License
7
+
8
+ **Copyright (c) 2024 European Synchrotron Radiation Facility**
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
11
+ this software and associated documentation files (the "Software"), to deal in
12
+ the Software without restriction, including without limitation the rights to
13
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
14
+ the Software, and to permit persons to whom the Software is furnished to do so,
15
+ subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
22
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
23
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
24
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
25
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
26
+
27
+ Project-URL: Homepage, https://gitlab.esrf.fr/icat/data-sync/parametertype
28
+ Project-URL: Documentation, https://gitlab.esrf.fr/icat/data-sync/parametertype
29
+ Project-URL: Repository, https://gitlab.esrf.fr/icat/data-sync/parametertype
30
+ Project-URL: Issues, https://gitlab.esrf.fr/icat/data-sync/parametertype/issues
31
+ Project-URL: Changelog, https://gitlab.esrf.fr/icat/data-sync/parametertype/-/blob/main/CHANGELOG.md
32
+ Classifier: Intended Audience :: Science/Research
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3
35
+ Requires-Python: >=3.8
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE.md
38
+ Requires-Dist: python-icat
39
+ Requires-Dist: icat-esrf-definitions
40
+ Provides-Extra: test
41
+ Requires-Dist: pytest; extra == "test"
42
+ Provides-Extra: dev
43
+ Requires-Dist: icat-sync-parametertype[test]; extra == "dev"
44
+ Requires-Dist: ruff; extra == "dev"
45
+ Dynamic: license-file
46
+
47
+ # icat-sync-parametertype
48
+
49
+ A python client to synchronize the metadata defined in the [ICAT ESRF definitions](https://gitlab.esrf.fr/icat/hdf5-master-config) with the ICAT database.
50
+
51
+ ```bash
52
+ icat-sync-parametertype -w http://ovm-icat-test.esrf.fr:8080 -a db -u admin -P
53
+ ```
54
+
55
+ `-P` prompts for the password. The connection can also be read from a
56
+ [python-icat configuration file](https://python-icat.readthedocs.io/en/stable/config.html)
57
+ (`icat.cfg`, or the file given by the `ICAT_CFG` environment variable), selecting a section with `-s`.
58
+ Copy [icat.cfg.example](icat.cfg.example) to `icat.cfg` and fill in the credentials:
59
+
60
+ ```bash
61
+ icat-sync-parametertype -s test
62
+ ```
63
+
64
+ ## Options
65
+
66
+ - `--dry-run`: only show the changes, nothing is registered in ICAT.
67
+ - `--yes`: register the changes without asking for confirmation (for CI).
68
+
69
+ ## Exit codes
70
+
71
+ | Code | Meaning |
72
+ |------|---------|
73
+ | 0 | ICAT is up to date, or the changes were registered (or declined) |
74
+ | 1 | At least one change failed to be registered |
75
+ | 2 | `--dry-run` detected changes |
@@ -0,0 +1,17 @@
1
+ LICENSE.md
2
+ README.md
3
+ pyproject.toml
4
+ src/icat_sync_parametertype/__init__.py
5
+ src/icat_sync_parametertype/definition.py
6
+ src/icat_sync_parametertype/icat_client.py
7
+ src/icat_sync_parametertype/logger.py
8
+ src/icat_sync_parametertype.egg-info/PKG-INFO
9
+ src/icat_sync_parametertype.egg-info/SOURCES.txt
10
+ src/icat_sync_parametertype.egg-info/dependency_links.txt
11
+ src/icat_sync_parametertype.egg-info/entry_points.txt
12
+ src/icat_sync_parametertype.egg-info/requires.txt
13
+ src/icat_sync_parametertype.egg-info/top_level.txt
14
+ src/icat_sync_parametertype/apps/__init__.py
15
+ src/icat_sync_parametertype/apps/sync.py
16
+ src/icat_sync_parametertype/tests/__init__.py
17
+ src/icat_sync_parametertype/tests/test_sync.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ icat-sync-parametertype = icat_sync_parametertype.apps.sync:main
@@ -0,0 +1,9 @@
1
+ python-icat
2
+ icat-esrf-definitions
3
+
4
+ [dev]
5
+ icat-sync-parametertype[test]
6
+ ruff
7
+
8
+ [test]
9
+ pytest