imas-python 0.1.dev1618__py3-none-any.whl
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.
Potentially problematic release.
This version of imas-python might be problematic. Click here for more details.
- imas/__init__.py +34 -0
- imas/__main__.py +17 -0
- imas/_util.py +260 -0
- imas/_version.py +16 -0
- imas/assets/IDSDef2MDSpreTree.xsl +161 -0
- imas/assets/IDS_fake_toplevel.xml +318 -0
- imas/assets/IDS_minimal.xml +37 -0
- imas/assets/IDS_minimal_2.xml +37 -0
- imas/assets/IDS_minimal_struct_array.xml +43 -0
- imas/assets/IDS_minimal_types.xml +169 -0
- imas/assets/ITER_134173_106_core_profiles.ids +2438 -0
- imas/assets/ITER_134173_106_equilibrium.ids +3172 -0
- imas/assets/README.md +5 -0
- imas/assets/core_profiles.ids +2438 -0
- imas/assets/equilibrium.ids +3172 -0
- imas/backends/__init__.py +12 -0
- imas/backends/db_entry_impl.py +122 -0
- imas/backends/imas_core/__init__.py +4 -0
- imas/backends/imas_core/al_context.py +391 -0
- imas/backends/imas_core/db_entry_al.py +405 -0
- imas/backends/imas_core/db_entry_helpers.py +200 -0
- imas/backends/imas_core/imas_interface.py +216 -0
- imas/backends/imas_core/mdsplus_model.py +367 -0
- imas/backends/imas_core/uda_support.py +56 -0
- imas/backends/netcdf/__init__.py +4 -0
- imas/backends/netcdf/db_entry_nc.py +176 -0
- imas/backends/netcdf/ids2nc.py +306 -0
- imas/backends/netcdf/nc2ids.py +302 -0
- imas/backends/netcdf/nc_metadata.py +357 -0
- imas/backends/netcdf/nc_validate.py +53 -0
- imas/command/cli.py +239 -0
- imas/command/db_analysis.py +290 -0
- imas/command/helpers.py +45 -0
- imas/command/timer.py +77 -0
- imas/db_entry.py +802 -0
- imas/dd_helpers.py +300 -0
- imas/dd_zip.py +297 -0
- imas/exception.py +107 -0
- imas/ids_base.py +149 -0
- imas/ids_convert.py +920 -0
- imas/ids_coordinates.py +400 -0
- imas/ids_data_type.py +115 -0
- imas/ids_defs.py +171 -0
- imas/ids_factory.py +108 -0
- imas/ids_identifiers.py +125 -0
- imas/ids_metadata.py +307 -0
- imas/ids_path.py +254 -0
- imas/ids_primitive.py +510 -0
- imas/ids_struct_array.py +233 -0
- imas/ids_structure.py +331 -0
- imas/ids_toplevel.py +381 -0
- imas/setup_logging.py +64 -0
- imas/test/test_all_dd_versions.py +28 -0
- imas/test/test_cli.py +44 -0
- imas/test/test_dbentry.py +104 -0
- imas/test/test_dd_helpers.py +134 -0
- imas/test/test_dd_zip.py +29 -0
- imas/test/test_deepcopy.py +25 -0
- imas/test/test_exception.py +12 -0
- imas/test/test_get_sample.py +388 -0
- imas/test/test_hash.py +188 -0
- imas/test/test_helpers.py +354 -0
- imas/test/test_identifiers.py +105 -0
- imas/test/test_ids2nc.py +166 -0
- imas/test/test_ids_ascii_data.py +39 -0
- imas/test/test_ids_convert.py +415 -0
- imas/test/test_ids_coordinates.py +165 -0
- imas/test/test_ids_data_type.py +40 -0
- imas/test/test_ids_factory.py +35 -0
- imas/test/test_ids_metadata.py +66 -0
- imas/test/test_ids_mixin.py +20 -0
- imas/test/test_ids_path.py +192 -0
- imas/test/test_ids_primitive.py +109 -0
- imas/test/test_ids_struct_array.py +89 -0
- imas/test/test_ids_structure.py +70 -0
- imas/test/test_ids_toplevel.py +55 -0
- imas/test/test_ids_validate.py +343 -0
- imas/test/test_latest_dd_autofill.py +74 -0
- imas/test/test_latest_dd_manual.py +38 -0
- imas/test/test_latest_dd_resample.py +102 -0
- imas/test/test_lazy_loading.py +201 -0
- imas/test/test_list_occurrences.py +82 -0
- imas/test/test_minimal.py +37 -0
- imas/test/test_minimal_conversion.py +44 -0
- imas/test/test_minimal_io.py +21 -0
- imas/test/test_minimal_struct_array_io.py +60 -0
- imas/test/test_minimal_types.py +436 -0
- imas/test/test_minimal_types_io.py +70 -0
- imas/test/test_minimal_types_io_automatic.py +66 -0
- imas/test/test_nbc_change.py +408 -0
- imas/test/test_nc_autofill.py +13 -0
- imas/test/test_nc_entry.py +28 -0
- imas/test/test_nc_metadata.py +153 -0
- imas/test/test_nc_validation.py +197 -0
- imas/test/test_snippets.py +25 -0
- imas/test/test_static_ids.py +39 -0
- imas/test/test_str_1d.py +99 -0
- imas/test/test_time_slicing.py +182 -0
- imas/test/test_uda_support.py +12 -0
- imas/test/test_util.py +209 -0
- imas/training.py +45 -0
- imas/util.py +526 -0
- imas_python-0.1.dev1618.dist-info/LICENSE.txt +165 -0
- imas_python-0.1.dev1618.dist-info/METADATA +288 -0
- imas_python-0.1.dev1618.dist-info/RECORD +108 -0
- imas_python-0.1.dev1618.dist-info/WHEEL +5 -0
- imas_python-0.1.dev1618.dist-info/entry_points.txt +3 -0
- imas_python-0.1.dev1618.dist-info/top_level.txt +1 -0
imas/dd_helpers.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# This file is part of imas-python.
|
|
2
|
+
# You should have received the imas-python LICENSE file with this project.
|
|
3
|
+
"""Helper functions to build IDSDef.xml"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
from io import BytesIO
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Sequence, Tuple, Union
|
|
13
|
+
from urllib.request import urlopen
|
|
14
|
+
from zipfile import ZIP_DEFLATED, ZipFile
|
|
15
|
+
|
|
16
|
+
from packaging.version import Version as V
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
_idsdef_zip_relpath = Path("imas/assets/IDSDef.zip")
|
|
21
|
+
_build_dir = Path("build")
|
|
22
|
+
_saxon_local_default_name = "saxon9he.jar" # For pre-3.30.0 builds
|
|
23
|
+
_saxon_regex = "saxon((.(?!test|xqj))*).jar" # Can be used in re.match
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def prepare_data_dictionaries():
|
|
27
|
+
"""Build IMAS IDSDef.xml files for each tagged version in the DD repository
|
|
28
|
+
1. Search for saxon or download it
|
|
29
|
+
2. Clone the DD repository (ask for user/pass unless ssh key access is available)
|
|
30
|
+
3. Generate IDSDef.xml and rename to IDSDef_${version}.xml
|
|
31
|
+
4. Zip all these IDSDefs together and include in wheel
|
|
32
|
+
"""
|
|
33
|
+
from git import Repo
|
|
34
|
+
|
|
35
|
+
saxon_jar_path = get_saxon()
|
|
36
|
+
repo: Repo = get_data_dictionary_repo()
|
|
37
|
+
if repo:
|
|
38
|
+
newest_version_and_tag = (V("0"), None)
|
|
39
|
+
for tag in repo.tags:
|
|
40
|
+
version_and_tag = (V(str(tag)), tag)
|
|
41
|
+
if V(str(tag)) > V("3.21.1"):
|
|
42
|
+
newest_version_and_tag = max(newest_version_and_tag, version_and_tag)
|
|
43
|
+
logger.debug("Building data dictionary version %s", tag)
|
|
44
|
+
build_data_dictionary(repo, tag, saxon_jar_path)
|
|
45
|
+
|
|
46
|
+
logger.info("Creating zip file of DD versions")
|
|
47
|
+
|
|
48
|
+
if _idsdef_zip_relpath.is_file():
|
|
49
|
+
logger.warning("Overwriting '%s'", _idsdef_zip_relpath)
|
|
50
|
+
|
|
51
|
+
with ZipFile(
|
|
52
|
+
_idsdef_zip_relpath,
|
|
53
|
+
mode="w", # this needs w, since zip can have multiple same entries
|
|
54
|
+
compression=ZIP_DEFLATED,
|
|
55
|
+
) as dd_zip:
|
|
56
|
+
for filename in _build_dir.glob("[0-9]*.xml"):
|
|
57
|
+
arcname = Path("data-dictionary").joinpath(*filename.parts[1:])
|
|
58
|
+
dd_zip.write(filename, arcname=arcname)
|
|
59
|
+
# Include identifiers from latest tag in zip file
|
|
60
|
+
repo.git.checkout(newest_version_and_tag[1], force=True)
|
|
61
|
+
# DD layout <= 4.0.0
|
|
62
|
+
for filename in Path("data-dictionary").glob("*/*identifier.xml"):
|
|
63
|
+
arcname = Path("identifiers").joinpath(*filename.parts[1:])
|
|
64
|
+
dd_zip.write(filename, arcname=arcname)
|
|
65
|
+
# DD layout > 4.0.0
|
|
66
|
+
for filename in Path("data-dictionary").glob("schemas/*/*identifier.xml"):
|
|
67
|
+
arcname = Path("identifiers").joinpath(*filename.parts[2:])
|
|
68
|
+
dd_zip.write(filename, arcname=arcname)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# pre 3.30.0 versions of the DD have the `saxon9he.jar` file path hardcoded
|
|
72
|
+
# in their makefiles. To be sure we can build everything, we link whatever
|
|
73
|
+
# saxon we can find to a local file called saxon9he.jar
|
|
74
|
+
def get_saxon() -> Path:
|
|
75
|
+
"""Search for saxon*.jar and return the path or download it.
|
|
76
|
+
The DD build works by having Saxon in the CLASSPATH, called saxon9he.jar
|
|
77
|
+
until DD version 3.30.0. After 3.30.0 Saxon is found by the SAXONJARFILE env
|
|
78
|
+
variable. We will 'cheat' a little bit later by symlinking saxon9he.jar to
|
|
79
|
+
any version of saxon we found.
|
|
80
|
+
|
|
81
|
+
Check:
|
|
82
|
+
1. CLASSPATH
|
|
83
|
+
2. `which saxon`
|
|
84
|
+
3. /usr/share/java/*
|
|
85
|
+
4. or download it
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
local_saxon_path = Path.cwd() / _saxon_local_default_name
|
|
89
|
+
if local_saxon_path.exists():
|
|
90
|
+
logger.debug("Something already at '%s' not creating anew", local_saxon_path)
|
|
91
|
+
return local_saxon_path
|
|
92
|
+
|
|
93
|
+
saxon_jar_origin = Path(
|
|
94
|
+
find_saxon_classpath()
|
|
95
|
+
or find_saxon_bin()
|
|
96
|
+
or find_saxon_jar()
|
|
97
|
+
or download_saxon()
|
|
98
|
+
)
|
|
99
|
+
logger.info("Found Saxon JAR '%s'", saxon_jar_origin)
|
|
100
|
+
if saxon_jar_origin.name != _saxon_local_default_name:
|
|
101
|
+
try:
|
|
102
|
+
os.symlink(saxon_jar_origin, local_saxon_path)
|
|
103
|
+
except FileExistsError:
|
|
104
|
+
# Another process could have created the symlink while we were searching
|
|
105
|
+
logger.debug(
|
|
106
|
+
"Link '%s' exists, parallel process might've created it",
|
|
107
|
+
local_saxon_path,
|
|
108
|
+
)
|
|
109
|
+
return local_saxon_path
|
|
110
|
+
return saxon_jar_origin
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def find_saxon_jar():
|
|
114
|
+
# This finds multiple versions on my system, but they are symlinked together.
|
|
115
|
+
# take the shortest one.
|
|
116
|
+
jars = [
|
|
117
|
+
path
|
|
118
|
+
for path in Path("/usr/share/java").rglob("*")
|
|
119
|
+
if re.match(_saxon_regex, path.name, flags=re.IGNORECASE)
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
if jars:
|
|
123
|
+
saxon_jar_path = min(jars, key=lambda x: len(x.parts))
|
|
124
|
+
return saxon_jar_path
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def find_saxon_classpath():
|
|
128
|
+
"""Search JAVAs CLASSPATH for a Saxon .jar"""
|
|
129
|
+
classpath = os.environ.get("CLASSPATH", "")
|
|
130
|
+
for part in re.split(";|:", classpath):
|
|
131
|
+
if (
|
|
132
|
+
part.endswith(".jar")
|
|
133
|
+
and part.split("/")[-1].startswith("saxon")
|
|
134
|
+
and "test" not in part
|
|
135
|
+
and "xqj" not in part
|
|
136
|
+
):
|
|
137
|
+
return part
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def find_saxon_bin():
|
|
141
|
+
"""Search for a saxon executable"""
|
|
142
|
+
saxon_bin = shutil.which("saxon")
|
|
143
|
+
if saxon_bin:
|
|
144
|
+
with open(saxon_bin, "r") as file:
|
|
145
|
+
for line in file:
|
|
146
|
+
saxon_jar_path = re.search("[^ ]*saxon[^ ]*jar", line)
|
|
147
|
+
if saxon_jar_path:
|
|
148
|
+
return saxon_jar_path.group(0)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def download_saxon():
|
|
152
|
+
"""Downloads a zipfile containing Saxon and extract it to the current dir.
|
|
153
|
+
Return the full path to Saxon. This can be any Saxon version. Scripts that
|
|
154
|
+
wrap this should probably manipulate either the name of this file, and/or
|
|
155
|
+
the CLASSPATH"""
|
|
156
|
+
|
|
157
|
+
SAXON_PATH = "https://github.com/Saxonica/Saxon-HE/releases/download/SaxonHE10-9/SaxonHE10-9J.zip" # noqa: E501
|
|
158
|
+
|
|
159
|
+
resp = urlopen(SAXON_PATH, timeout=120.0)
|
|
160
|
+
zipfile = ZipFile(BytesIO(resp.read()))
|
|
161
|
+
# Zipfile has a list of the ZipInfos. Look inside for a Saxon jar
|
|
162
|
+
for file in zipfile.filelist:
|
|
163
|
+
if re.match(_saxon_regex, file.filename, flags=re.IGNORECASE):
|
|
164
|
+
path = zipfile.extract(file)
|
|
165
|
+
del zipfile
|
|
166
|
+
return path
|
|
167
|
+
raise FileNotFoundError(f"No Saxon jar found in given zipfile '{SAXON_PATH}'")
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def get_data_dictionary_repo() -> Tuple[bool, bool]:
|
|
171
|
+
try:
|
|
172
|
+
import git # Import git here, the user might not have it!
|
|
173
|
+
except ModuleNotFoundError:
|
|
174
|
+
raise RuntimeError(
|
|
175
|
+
"Could not find 'git' module, try 'pip install gitpython'. \
|
|
176
|
+
Will not build Data Dictionaries!"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# We need the actual source code (for now) so grab it from ITER
|
|
180
|
+
dd_repo_path = "data-dictionary"
|
|
181
|
+
|
|
182
|
+
if "DD_DIRECTORY" in os.environ:
|
|
183
|
+
logger.info("Found DD_DIRECTORY, copying")
|
|
184
|
+
try:
|
|
185
|
+
shutil.copytree(os.environ["DD_DIRECTORY"], dd_repo_path)
|
|
186
|
+
except FileExistsError:
|
|
187
|
+
pass
|
|
188
|
+
else:
|
|
189
|
+
logger.info("Trying to pull data dictionary git repo from ITER")
|
|
190
|
+
|
|
191
|
+
# Set up a bare repo and fetch the data-dictionary repository in it
|
|
192
|
+
os.makedirs(dd_repo_path, exist_ok=True)
|
|
193
|
+
try:
|
|
194
|
+
repo = git.Repo(dd_repo_path)
|
|
195
|
+
except git.exc.InvalidGitRepositoryError:
|
|
196
|
+
repo = git.Repo.init(dd_repo_path)
|
|
197
|
+
logger.info("Set up local git repository {!s}".format(repo))
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
origin = repo.remote()
|
|
201
|
+
except ValueError:
|
|
202
|
+
dd_repo_url = "https://github.com/iterorganization/imas-data-dictionary.git"
|
|
203
|
+
origin = repo.create_remote("origin", url=dd_repo_url)
|
|
204
|
+
logger.info("Set up remote '{!s}' linking to '{!s}'".format(origin, origin.url))
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
origin.fetch(tags=True)
|
|
208
|
+
except git.exc.GitCommandError as ee:
|
|
209
|
+
logger.warning(
|
|
210
|
+
"Could not fetch tags from %s. Git reports:\n %s." "\nTrying to continue",
|
|
211
|
+
list(origin.urls),
|
|
212
|
+
ee,
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
logger.info("Remote tags fetched")
|
|
216
|
+
return repo
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _run_data_dictionary(
|
|
220
|
+
args: Union[Sequence, str], tag: str, saxon_jar_path: str
|
|
221
|
+
) -> int:
|
|
222
|
+
"""Run in a Data Dictionary environment. Used e.g. to run the DD Makefile
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
args: The "args" argument directly passed to :func:`subprocess.run`,
|
|
226
|
+
e.g. ``["make", "clean"]``
|
|
227
|
+
tag: The DD version tag that will be printed on error
|
|
228
|
+
saxon_jar_path: The path to the saxon jar; Added to CLASSPATH and used
|
|
229
|
+
to generate the DD
|
|
230
|
+
"""
|
|
231
|
+
env = os.environ.copy()
|
|
232
|
+
env["CLASSPATH"] = f"{saxon_jar_path}:{env.get('CLASSPATH', '')}"
|
|
233
|
+
result = subprocess.run(
|
|
234
|
+
args,
|
|
235
|
+
bufsize=0,
|
|
236
|
+
capture_output=True,
|
|
237
|
+
cwd=os.getcwd() + "/data-dictionary",
|
|
238
|
+
env=env,
|
|
239
|
+
text=True,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if result.returncode != 0:
|
|
243
|
+
logger.warning("Error making DD version %s, make reported:", tag)
|
|
244
|
+
logger.warning("CLASSPATH ='%s'", saxon_jar_path)
|
|
245
|
+
logger.warning("PATH = '%s'", os.environ.get("PATH", ""))
|
|
246
|
+
logger.warning("stdout = '%s'", result.stdout.strip())
|
|
247
|
+
logger.warning("stderr = '%s'", result.stderr.strip())
|
|
248
|
+
logger.warning("continuing without DD version %s", tag)
|
|
249
|
+
else:
|
|
250
|
+
logger.debug(
|
|
251
|
+
"Successful make for DD %s.\n-- Make stdout --\n%s\n-- Make stderr --\n%s",
|
|
252
|
+
tag,
|
|
253
|
+
result.stdout,
|
|
254
|
+
result.stderr,
|
|
255
|
+
)
|
|
256
|
+
return result.returncode
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def build_data_dictionary(repo, tag: str, saxon_jar_path: str, rebuild=False) -> None:
|
|
260
|
+
"""Build a single version of the data dictionary given by the tag argument
|
|
261
|
+
if the IDS does not already exist.
|
|
262
|
+
|
|
263
|
+
In the data-dictionary repository sometimes IDSDef.xml is stored
|
|
264
|
+
directly, in which case we do not call make.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
repo: Repository object containing the DD source code
|
|
268
|
+
tag: The DD version tag that will be build
|
|
269
|
+
saxon_jar_path: The path to the saxon jar; Added to CLASSPATH and used
|
|
270
|
+
to generate the DD
|
|
271
|
+
rebuild: If true, overwrites existing pre-build tagged DD version
|
|
272
|
+
"""
|
|
273
|
+
_build_dir.mkdir(exist_ok=True)
|
|
274
|
+
result_xml = _build_dir / f"{tag}.xml"
|
|
275
|
+
|
|
276
|
+
if result_xml.exists() and not rebuild:
|
|
277
|
+
logger.debug(f"XML for tag '{tag}' already exists, skipping")
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
repo.git.checkout(tag, force=True)
|
|
281
|
+
if _run_data_dictionary(["make", "clean"], tag, saxon_jar_path) != 0:
|
|
282
|
+
return
|
|
283
|
+
if _run_data_dictionary(["make", "IDSDef.xml"], tag, saxon_jar_path) != 0:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
# copy and delete original instead of move (to follow symlink)
|
|
287
|
+
IDSDef = Path("data-dictionary/IDSDef.xml")
|
|
288
|
+
try:
|
|
289
|
+
shutil.copy(
|
|
290
|
+
IDSDef, # Hardcoded in access-layer makefile
|
|
291
|
+
result_xml,
|
|
292
|
+
follow_symlinks=True,
|
|
293
|
+
)
|
|
294
|
+
except shutil.SameFileError:
|
|
295
|
+
pass
|
|
296
|
+
IDSDef.unlink(missing_ok=True)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
prepare_data_dictionaries()
|
imas/dd_zip.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# This file is part of imas-python.
|
|
2
|
+
# You should have received the imas-python LICENSE file with this project.
|
|
3
|
+
""" Extract DD versions from a zip file.
|
|
4
|
+
|
|
5
|
+
The zip file contains files as
|
|
6
|
+
* `data-dictionary/3.30.0.xml`
|
|
7
|
+
* `data-dictionary/3.29.0.xml`
|
|
8
|
+
|
|
9
|
+
multiple paths are checked. See `ZIPFILE_LOCATIONS`.
|
|
10
|
+
First the environment variable IMASPY_DDZIP is checked.
|
|
11
|
+
If that exists and points to a file we will attempt to open it.
|
|
12
|
+
Then, IDSDef.zip is searched in site-packages, the current folder,
|
|
13
|
+
in .config/imas/ (`$$XDG_CONFIG_HOME`) and in
|
|
14
|
+
the assets/ folder within the imas-python package.
|
|
15
|
+
|
|
16
|
+
1. `$$IMASPY_DDZIP`
|
|
17
|
+
2. The virtual environment
|
|
18
|
+
3. USER_BASE`imas/IDSDef.zip`
|
|
19
|
+
4. All `site-packages/imas/IDSDef.zip`
|
|
20
|
+
5. `./IDSDef.zip`
|
|
21
|
+
6. `~/.config/imas/IDSDef.zip`
|
|
22
|
+
7. `__file__/../../imas/assets/IDSDef.zip`
|
|
23
|
+
|
|
24
|
+
All files are checked, i.e. if your .config/imas/IDSDef.zip is outdated
|
|
25
|
+
the imas-python-packaged version will be used.
|
|
26
|
+
|
|
27
|
+
The `assets/IDSDef.zip` provided with the package can be updated
|
|
28
|
+
with the `python setup.py build_DD` command, which is also performed on install
|
|
29
|
+
if you have access to the ITER data-dictionary git repo.
|
|
30
|
+
Reinstalling imas thus also will give you access to the latest DD versions.
|
|
31
|
+
"""
|
|
32
|
+
import logging
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
import xml.etree.ElementTree as ET
|
|
36
|
+
from contextlib import contextmanager, nullcontext
|
|
37
|
+
from functools import lru_cache
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
from typing import Dict, Iterator, List, Tuple, Union
|
|
40
|
+
from zipfile import ZipFile
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from importlib.resources import as_file, files
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
from importlib.resources.abc import Traversable
|
|
47
|
+
except ModuleNotFoundError: # Python 3.9/3.10 support
|
|
48
|
+
from importlib.abc import Traversable
|
|
49
|
+
|
|
50
|
+
except ImportError: # Python 3.8 support
|
|
51
|
+
from importlib_resources import as_file, files
|
|
52
|
+
from importlib_resources.abc import Traversable
|
|
53
|
+
|
|
54
|
+
from packaging.version import InvalidVersion, Version
|
|
55
|
+
|
|
56
|
+
import imas
|
|
57
|
+
from imas.exception import UnknownDDVersion
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _get_xdg_config_dir():
|
|
63
|
+
"""
|
|
64
|
+
Return the XDG config directory, according to the XDG base directory spec:
|
|
65
|
+
|
|
66
|
+
https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
|
67
|
+
"""
|
|
68
|
+
return os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _generate_zipfile_locations() -> Iterator[Union[Path, Traversable]]:
|
|
72
|
+
"""Build a list of potential data dictionary locations.
|
|
73
|
+
We start with the path (if any) of the IMASPY_DDZIP env var.
|
|
74
|
+
Then we look for IDSDef.zip in the current folder, in the
|
|
75
|
+
default XDG config dir (~/.config/imas/IDSDef.zip) and
|
|
76
|
+
finally in the assets distributed with this package.
|
|
77
|
+
"""
|
|
78
|
+
zip_name = "IDSDef.zip"
|
|
79
|
+
|
|
80
|
+
environ = os.environ.get("IMASPY_DDZIP")
|
|
81
|
+
if environ:
|
|
82
|
+
yield Path(environ).resolve()
|
|
83
|
+
|
|
84
|
+
yield Path(zip_name).resolve()
|
|
85
|
+
yield Path(_get_xdg_config_dir()).resolve() / "imas" / zip_name
|
|
86
|
+
yield files(imas) / "assets" / zip_name
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_dd_version(version: str) -> Version:
|
|
90
|
+
try:
|
|
91
|
+
return Version(version)
|
|
92
|
+
except InvalidVersion:
|
|
93
|
+
# This is probably a dev build of the DD, of which the version is obtained with
|
|
94
|
+
# `git describe` in the format X.Y.Z-<ncommits>-g<hash> with X.Y.Z the previous
|
|
95
|
+
# released version: try again after converting the first dash to a + and treat
|
|
96
|
+
# it like a `local` version specifier, which is recognized as newer.
|
|
97
|
+
# https://packaging.python.org/en/latest/specifications/version-specifiers/
|
|
98
|
+
return Version(version.replace("-", "+", 1))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# Expected use case is one, maximum two DD versions
|
|
102
|
+
# Cache is bigger than that: in pytest we currently use the following DD versions:
|
|
103
|
+
# - 3.22.0
|
|
104
|
+
# - 3.25.0
|
|
105
|
+
# - 3.28.0
|
|
106
|
+
# - 3.39.0
|
|
107
|
+
# - 4.0.0 (if available)
|
|
108
|
+
# - Environment default
|
|
109
|
+
# - IDS_fake_toplevel.xml
|
|
110
|
+
# - IDS_minimal.xml
|
|
111
|
+
# - IDS_minimal_2.xml
|
|
112
|
+
# - IDS_minimal_struct_array.xml
|
|
113
|
+
# - IDS_minimal_types.xml
|
|
114
|
+
_DD_CACHE_SIZE = 8
|
|
115
|
+
ZIPFILE_LOCATIONS = list(_generate_zipfile_locations())
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def dd_etree(version=None, xml_path=None):
|
|
119
|
+
"""Return the DD element tree corresponding to the provided dd_version or xml_file.
|
|
120
|
+
|
|
121
|
+
By default (``dd_version`` and ``dd_xml`` are not supplied), this will attempt
|
|
122
|
+
to get the version from the environment (``IMAS_VERSION``) and use the latest
|
|
123
|
+
available version as fallback.
|
|
124
|
+
|
|
125
|
+
You can also specify a specific DD version to use (e.g. "3.38.1") or point to a
|
|
126
|
+
specific data-dictionary XML file. These options are exclusive.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
version: DD version string, e.g. "3.38.1".
|
|
130
|
+
xml_path: XML file containing data dictionary definition.
|
|
131
|
+
"""
|
|
132
|
+
if version and xml_path:
|
|
133
|
+
raise ValueError("version and xml_path cannot be provided both.")
|
|
134
|
+
if not version and not xml_path:
|
|
135
|
+
# Figure out which DD version to use
|
|
136
|
+
if "IMAS_VERSION" in os.environ:
|
|
137
|
+
imas_version = os.environ["IMAS_VERSION"]
|
|
138
|
+
if imas_version in dd_xml_versions():
|
|
139
|
+
# Use bundled DD version when available
|
|
140
|
+
version = imas_version
|
|
141
|
+
elif "IMAS_PREFIX" in os.environ:
|
|
142
|
+
# Try finding the IDSDef.xml in this installation
|
|
143
|
+
imas_prefix = Path(os.environ["IMAS_PREFIX"]).resolve()
|
|
144
|
+
xml_file = imas_prefix / "include" / "IDSDef.xml"
|
|
145
|
+
if xml_file.exists():
|
|
146
|
+
xml_path = str(xml_file)
|
|
147
|
+
if not version and not xml_path:
|
|
148
|
+
logger.warning(
|
|
149
|
+
"Unable to load IMAS version %s, falling back to latest version.",
|
|
150
|
+
imas_version,
|
|
151
|
+
)
|
|
152
|
+
if not version and not xml_path:
|
|
153
|
+
# Use latest available from
|
|
154
|
+
version = latest_dd_version()
|
|
155
|
+
# Do the actual loading in a cached method:
|
|
156
|
+
return _load_etree(version, xml_path)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@lru_cache(_DD_CACHE_SIZE)
|
|
160
|
+
def _load_etree(version, xml_path):
|
|
161
|
+
if xml_path:
|
|
162
|
+
logger.info("Parsing data dictionary from file: %s", xml_path)
|
|
163
|
+
tree = ET.parse(xml_path)
|
|
164
|
+
else:
|
|
165
|
+
xml = get_dd_xml(version)
|
|
166
|
+
logger.info("Parsing data dictionary version %s", version)
|
|
167
|
+
tree = ET.ElementTree(ET.fromstring(xml))
|
|
168
|
+
return tree
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@contextmanager
|
|
172
|
+
def _open_zipfile(path: Union[Path, Traversable]) -> Iterator[ZipFile]:
|
|
173
|
+
"""Open a zipfile, given a Path or Traversable."""
|
|
174
|
+
if isinstance(path, Path):
|
|
175
|
+
ctx = nullcontext(path)
|
|
176
|
+
else:
|
|
177
|
+
ctx = as_file(path)
|
|
178
|
+
with ctx as file:
|
|
179
|
+
with ZipFile(file) as zipfile:
|
|
180
|
+
yield zipfile
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@lru_cache
|
|
184
|
+
def _read_dd_versions() -> Dict[str, Tuple[Union[Path, Traversable], str]]:
|
|
185
|
+
"""Traverse all possible DD zip files and return a map of known versions.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
version_map: version -> (zipfile path, filename)
|
|
189
|
+
"""
|
|
190
|
+
versions = {}
|
|
191
|
+
xml_re = re.compile(r"^data-dictionary/([0-9.]+)\.xml$")
|
|
192
|
+
for path in ZIPFILE_LOCATIONS:
|
|
193
|
+
if not path.is_file():
|
|
194
|
+
continue
|
|
195
|
+
with _open_zipfile(path) as zipfile:
|
|
196
|
+
for fname in zipfile.namelist():
|
|
197
|
+
match = xml_re.match(fname)
|
|
198
|
+
if match:
|
|
199
|
+
version = match.group(1)
|
|
200
|
+
if version not in versions:
|
|
201
|
+
versions[version] = (path, fname)
|
|
202
|
+
if not versions:
|
|
203
|
+
raise RuntimeError(
|
|
204
|
+
"Could not find any data dictionary definitions. "
|
|
205
|
+
f"Looked in: {', '.join(map(repr, ZIPFILE_LOCATIONS))}."
|
|
206
|
+
)
|
|
207
|
+
return versions
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@lru_cache
|
|
211
|
+
def _read_identifiers() -> Dict[str, Tuple[Union[Path, Traversable], str]]:
|
|
212
|
+
"""Traverse all possible DD zip files and return a map of known identifiers.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
identifier_map: identifier -> (zipfile path, filename)
|
|
216
|
+
"""
|
|
217
|
+
identifiers = {}
|
|
218
|
+
xml_re = re.compile(r"^identifiers/\w+/(\w+_identifier).xml$")
|
|
219
|
+
for path in ZIPFILE_LOCATIONS:
|
|
220
|
+
if not path.is_file():
|
|
221
|
+
continue
|
|
222
|
+
with _open_zipfile(path) as zipfile:
|
|
223
|
+
for fname in zipfile.namelist():
|
|
224
|
+
match = xml_re.match(fname)
|
|
225
|
+
if match:
|
|
226
|
+
identifier_name = match.group(1)
|
|
227
|
+
if identifier_name not in identifiers:
|
|
228
|
+
identifiers[identifier_name] = (path, fname)
|
|
229
|
+
return identifiers
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@lru_cache
|
|
233
|
+
def dd_xml_versions() -> List[str]:
|
|
234
|
+
"""Parse IDSDef.zip to find version numbers available"""
|
|
235
|
+
|
|
236
|
+
def sort_key(version):
|
|
237
|
+
try:
|
|
238
|
+
return parse_dd_version(version)
|
|
239
|
+
except InvalidVersion:
|
|
240
|
+
# Don't fail when a malformatted version is present in the DD zip
|
|
241
|
+
logger.error(
|
|
242
|
+
f"Could not convert DD XML version {version} to a Version.", exc_info=1
|
|
243
|
+
)
|
|
244
|
+
return Version(0)
|
|
245
|
+
|
|
246
|
+
return sorted(_read_dd_versions(), key=sort_key)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@lru_cache
|
|
250
|
+
def dd_identifiers() -> List[str]:
|
|
251
|
+
"""Parse IDSDef.zip to find available identifiers"""
|
|
252
|
+
|
|
253
|
+
return sorted(_read_identifiers())
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_dd_xml(version):
|
|
257
|
+
"""Read XML file for the given data dictionary version."""
|
|
258
|
+
dd_versions = dd_xml_versions()
|
|
259
|
+
if version not in dd_versions:
|
|
260
|
+
raise UnknownDDVersion(version, dd_versions)
|
|
261
|
+
path, fname = _read_dd_versions()[version]
|
|
262
|
+
with _open_zipfile(path) as zipfile:
|
|
263
|
+
return zipfile.read(fname)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def get_dd_xml_crc(version):
|
|
267
|
+
"""Given a version string, return its CRC checksum"""
|
|
268
|
+
# Note, by this time get_dd_xml is already called, so we don't need to check if the
|
|
269
|
+
# version is known
|
|
270
|
+
path, fname = _read_dd_versions()[version]
|
|
271
|
+
with _open_zipfile(path) as zipfile:
|
|
272
|
+
return zipfile.getinfo(fname).CRC
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def get_identifier_xml(identifier_name):
|
|
276
|
+
"""Get identifier XML for the given identifier name"""
|
|
277
|
+
path, fname = _read_identifiers()[identifier_name]
|
|
278
|
+
with _open_zipfile(path) as zipfile:
|
|
279
|
+
return zipfile.read(fname)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def print_supported_version_warning(version):
|
|
283
|
+
try:
|
|
284
|
+
if parse_dd_version(version) < imas.OLDEST_SUPPORTED_VERSION:
|
|
285
|
+
logger.warning(
|
|
286
|
+
"Version %s is below lowest supported version of %s.\
|
|
287
|
+
Proceed at your own risk.",
|
|
288
|
+
version,
|
|
289
|
+
imas.OLDEST_SUPPORTED_VERSION,
|
|
290
|
+
)
|
|
291
|
+
except InvalidVersion:
|
|
292
|
+
logging.warning("Ignoring version parsing error.", exc_info=1)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def latest_dd_version():
|
|
296
|
+
"""Find the latest version in data-dictionary/IDSDef.zip"""
|
|
297
|
+
return dd_xml_versions()[-1]
|
imas/exception.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# This file is part of imas-python.
|
|
2
|
+
# You should have received the imas-python LICENSE file with this project.
|
|
3
|
+
"""Exception classes used in imas-python.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import difflib
|
|
7
|
+
import logging
|
|
8
|
+
from typing import TYPE_CHECKING, List
|
|
9
|
+
|
|
10
|
+
from imas.backends.imas_core import imas_interface as _imas_interface
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from imas.ids_factory import IDSFactory
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Expose ALException, which may be thrown by the lowlevel
|
|
20
|
+
if _imas_interface.has_imas:
|
|
21
|
+
ALException = _imas_interface.lowlevel.ALException
|
|
22
|
+
else:
|
|
23
|
+
ALException = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UnknownDDVersion(ValueError):
|
|
27
|
+
"""Error raised when an unknown DD version is specified."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, version: str, available: List[str], note: str = "") -> None:
|
|
30
|
+
close_matches = difflib.get_close_matches(version, available, n=1)
|
|
31
|
+
if close_matches:
|
|
32
|
+
suggestions = f"Did you mean {close_matches[0]!r}?"
|
|
33
|
+
else:
|
|
34
|
+
suggestions = f"Available versions are {', '.join(reversed(available))}"
|
|
35
|
+
super().__init__(
|
|
36
|
+
f"Data dictionary version {version!r} cannot be found. {suggestions}{note}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class IDSNameError(ValueError):
|
|
41
|
+
"""Error raised by DBEntry.get(_slice) when providing an invalid IDS name."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, ids_name: str, factory: "IDSFactory") -> None:
|
|
44
|
+
suggestions = ""
|
|
45
|
+
close_matches = difflib.get_close_matches(ids_name, factory.ids_names(), n=1)
|
|
46
|
+
if close_matches:
|
|
47
|
+
suggestions = f" Did you mean {close_matches[0]!r}?"
|
|
48
|
+
super().__init__(f"IDS {ids_name!r} does not exist.{suggestions}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DataEntryException(RuntimeError):
|
|
52
|
+
"""Error raised by DBEntry for unexpected data in the backend."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MDSPlusModelError(RuntimeError):
|
|
56
|
+
"""Error raised when building MDS+ models."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, msg: str) -> None:
|
|
59
|
+
super().__init__(f"Error building MDSplus data model: {msg}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class LowlevelError(RuntimeError):
|
|
63
|
+
"""Error raised when lowlevel returns nonzero status"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, function: str, status: int):
|
|
66
|
+
super().__init__(
|
|
67
|
+
f"An Access Layer lowlevel operation ({function}) was unsuccessful "
|
|
68
|
+
f"({status=}). "
|
|
69
|
+
"More debug information should be available earlier in the program output."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class CoordinateLookupError(Exception):
|
|
74
|
+
"""Error raised by IDSCoordinate.__getitem__ when a coordinate cannot be found."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ValidationError(Exception):
|
|
78
|
+
"""Error raised by IDSToplevel.validate() to indicate the IDS is not valid."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CoordinateError(ValidationError):
|
|
82
|
+
"""Error raised by ids.validate() to indicate a coordinate check has failed."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, node, dimension, expected_size, coor_path):
|
|
85
|
+
"""Create a new CoordinateError
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
node: IDSBase object that this coordinate error is raised for
|
|
89
|
+
dimension: (0-based) dimension with incorrect size
|
|
90
|
+
expected_size: size of the coordinate for the specified dimension
|
|
91
|
+
coor_path: path of the coordinate, may be None when a coordinate is of fixed
|
|
92
|
+
size (e.g. ``1...3``)
|
|
93
|
+
"""
|
|
94
|
+
if coor_path is not None: # Error message when coordinate size doesnt match
|
|
95
|
+
details = (
|
|
96
|
+
f"its coordinate in dimension {dimension + 1} (`{coor_path}`) has "
|
|
97
|
+
f"size {expected_size}."
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
details = f"dimension {dimension + 1} must have size {expected_size}."
|
|
101
|
+
super().__init__(
|
|
102
|
+
f"Element `{node._path}` has incorrect shape {node.shape}: {details}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class InvalidNetCDFEntry(Exception):
|
|
107
|
+
"""Error raised when loading an IDS from a NetCDF file that fails validation."""
|