xconv2 0.1.0a1__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.
Files changed (42) hide show
  1. xconv2-0.1.0a1/LICENSE +21 -0
  2. xconv2-0.1.0a1/PKG-INFO +16 -0
  3. xconv2-0.1.0a1/README.md +46 -0
  4. xconv2-0.1.0a1/pyproject.toml +41 -0
  5. xconv2-0.1.0a1/setup.cfg +4 -0
  6. xconv2-0.1.0a1/tests/test_cf_interface.py +352 -0
  7. xconv2-0.1.0a1/tests/test_contour.py +248 -0
  8. xconv2-0.1.0a1/tests/test_coordinate_flow.py +309 -0
  9. xconv2-0.1.0a1/tests/test_lineplot.py +223 -0
  10. xconv2-0.1.0a1/tests/test_load_selected_file.py +114 -0
  11. xconv2-0.1.0a1/tests/test_worker_save_script.py +126 -0
  12. xconv2-0.1.0a1/xconv2/__init__.py +24 -0
  13. xconv2-0.1.0a1/xconv2/assets/Pyfive-logo.png +0 -0
  14. xconv2-0.1.0a1/xconv2/assets/UoR-logo.png +0 -0
  15. xconv2-0.1.0a1/xconv2/assets/cf-logo-t.svg +30367 -0
  16. xconv2-0.1.0a1/xconv2/assets/cf-logo.svg +30375 -0
  17. xconv2-0.1.0a1/xconv2/assets/dask-logo.svg +13 -0
  18. xconv2-0.1.0a1/xconv2/assets/ncas-logo.png +0 -0
  19. xconv2-0.1.0a1/xconv2/assets/under-construction.svg +9 -0
  20. xconv2-0.1.0a1/xconv2/cell_method_handler.py +92 -0
  21. xconv2-0.1.0a1/xconv2/cf_templates.py +154 -0
  22. xconv2-0.1.0a1/xconv2/colour_scales.py +43 -0
  23. xconv2-0.1.0a1/xconv2/core_window.py +1059 -0
  24. xconv2-0.1.0a1/xconv2/gui.py +43 -0
  25. xconv2-0.1.0a1/xconv2/lineplot.py +175 -0
  26. xconv2-0.1.0a1/xconv2/main_window.py +482 -0
  27. xconv2-0.1.0a1/xconv2/ui/__init__.py +5 -0
  28. xconv2-0.1.0a1/xconv2/ui/contour_options_controller.py +671 -0
  29. xconv2-0.1.0a1/xconv2/ui/dialogs.py +154 -0
  30. xconv2-0.1.0a1/xconv2/ui/field_metadata_controller.py +277 -0
  31. xconv2-0.1.0a1/xconv2/ui/menu_controller.py +146 -0
  32. xconv2-0.1.0a1/xconv2/ui/plot_view_controller.py +390 -0
  33. xconv2-0.1.0a1/xconv2/ui/selection_controller.py +414 -0
  34. xconv2-0.1.0a1/xconv2/ui/settings_store.py +204 -0
  35. xconv2-0.1.0a1/xconv2/worker.py +309 -0
  36. xconv2-0.1.0a1/xconv2/xconv_cf_interface.py +538 -0
  37. xconv2-0.1.0a1/xconv2.egg-info/PKG-INFO +16 -0
  38. xconv2-0.1.0a1/xconv2.egg-info/SOURCES.txt +40 -0
  39. xconv2-0.1.0a1/xconv2.egg-info/dependency_links.txt +1 -0
  40. xconv2-0.1.0a1/xconv2.egg-info/entry_points.txt +5 -0
  41. xconv2-0.1.0a1/xconv2.egg-info/requires.txt +10 -0
  42. xconv2-0.1.0a1/xconv2.egg-info/top_level.txt +1 -0
xconv2-0.1.0a1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NCAS University of Reading
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: xconv2
3
+ Version: 0.1.0a1
4
+ Summary: High-performance data viewer and simple data converter
5
+ Requires-Python: >=3.12
6
+ License-File: LICENSE
7
+ Requires-Dist: PySide6
8
+ Requires-Dist: superqt
9
+ Requires-Dist: cf-python
10
+ Requires-Dist: cf-plot
11
+ Requires-Dist: zarr
12
+ Requires-Dist: pandas
13
+ Requires-Dist: setuptools>80
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest; extra == "test"
16
+ Dynamic: license-file
@@ -0,0 +1,46 @@
1
+ # xconv2
2
+
3
+ xconv2 provides (or will provide) a graphical interface to local and remote weather and climate data. It is a replacement for the venerable [xconv](https://ncas-cms.github.io/xconv-doc/html/index.html) package that has been supported by NCAS CMS for decades.
4
+
5
+ <table>
6
+ <tr>
7
+ <td style="vertical-align: middle; padding: 2px;">
8
+ <img src="xconv2/assets/under-construction.svg" alt="Under construction icon" height="128px" />
9
+ </td>
10
+ <td style="vertical-align: middle;">
11
+ This version is under development. If you encounter something that looks like it should work, but doesn't, please
12
+ raise a bug issue at the address found in the About menu. If there is function that you would like to see, feel
13
+ free to raise an enhancement request at the same location.
14
+ </td>
15
+ </tr>
16
+ </table>
17
+
18
+ ## Installation
19
+
20
+ We intend to release this as a standalone executable for linux and macos, but for the moment you should use a Python 3.12 (or later) environment with
21
+ udunits2 installed, then you can pip install from the source using one of these:
22
+
23
+ `pip install "git+https://github.com/ncas-cms/xconv2.git"`
24
+ `pip install "git+ssh://github.com/ncas-cms/xconv2.git"`
25
+
26
+ and then you should have `xconv2` available on your command line.
27
+
28
+ ## Documentation
29
+
30
+ No user documentation is yet available, but it is designed to be intuitive
31
+ and basic usage should be possible without documentation once it is installed.
32
+
33
+ ### Developer Documentation
34
+
35
+ Limited developer documentation is available. For the moment it is
36
+ limited to the following:
37
+
38
+ #### UML Diagrams
39
+
40
+ - `docs/uml/alpha_core_window.puml`
41
+ - `docs/uml/core_window_gui_worker_signals.puml`
42
+ - `docs/uml/core_window_options_sequence.puml`
43
+
44
+ ## License
45
+
46
+ This project is licensed under the MIT License. See `LICENSE`.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=80"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xconv2"
7
+ version = "0.1.0a1"
8
+ description = "High-performance data viewer and simple data converter"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "PySide6",
12
+ "superqt",
13
+ "cf-python",
14
+ "cf-plot",
15
+ "zarr",
16
+ "pandas",
17
+ "setuptools>80",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ test = [
22
+ "pytest",
23
+ ]
24
+
25
+ [project.gui-scripts]
26
+ # This creates an 'xconv2' command that launches your GUI
27
+ xconv2 = "xconv2.gui:main"
28
+
29
+ [project.scripts]
30
+ # This creates a 'cf-worker' command for the backend process
31
+ cf-worker = "xconv2.worker:main"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["."]
35
+ include = ["xconv2*"]
36
+
37
+ [tool.setuptools.package-data]
38
+ xconv2 = ["assets/*.svg", "assets/*.png"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,352 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+ import xconv2.xconv_cf_interface as cf_interface
5
+
6
+ from xconv2.xconv_cf_interface import (
7
+ auto_contour_title,
8
+ coordinate_info,
9
+ field_info,
10
+ get_data_for_plotting,
11
+ run_contour_plot,
12
+ run_line_plot,
13
+ )
14
+
15
+ import numpy as np
16
+
17
+
18
+ cf = pytest.importorskip("cf")
19
+
20
+
21
+ class _MockCellMeasures:
22
+ def __call__(self) -> str:
23
+ return "cell_measures: area: areacella"
24
+
25
+
26
+ class _MockField:
27
+ shape = (2, 3)
28
+
29
+ def __str__(self) -> str:
30
+ return "mock-field-summary"
31
+
32
+ def identity(self) -> str:
33
+ return "air_temperature"
34
+
35
+ def properties(self) -> dict[str, str]:
36
+ return {"units": "K", "standard_name": "air_temperature"}
37
+
38
+ def coordinates(self) -> dict[str, str]:
39
+ return {
40
+ "dimensioncoordinate0": "Dimension coordinate: latitude(2) degrees_north",
41
+ "dimensioncoordinate1": "Dimension coordinate: longitude(3) degrees_east",
42
+ }
43
+
44
+ def cell_methods(self) -> str:
45
+ return ""
46
+
47
+ def cell_measures(self) -> str:
48
+ return _MockCellMeasures()()
49
+
50
+
51
+ def test_field_info_returns_serialized_rows() -> None:
52
+ payload = field_info([_MockField()])
53
+
54
+ assert isinstance(payload, list)
55
+ assert len(payload) == 1
56
+ assert isinstance(payload[0], str)
57
+
58
+ parts = payload[0].split("\x1f", 2)
59
+ assert len(parts) == 3
60
+ assert parts[0].startswith("air_temperature")
61
+ assert parts[1] == "mock-field-summary"
62
+ assert "units" in parts[2]
63
+
64
+
65
+ class _MockCoord:
66
+ def __init__(self, name: str, values: list[object]) -> None:
67
+ self._name = name
68
+ self.array = values
69
+
70
+ def identity(self, default: str = "unknown") -> str:
71
+ return self._name or default
72
+
73
+
74
+ class _MockCoordField:
75
+ def __init__(self) -> None:
76
+ self._coords = {
77
+ "time": _MockCoord("time", [1, 2, 3]),
78
+ "height": _MockCoord("height", [10]),
79
+ "lat": _MockCoord("latitude", [-90, 0, 90]),
80
+ }
81
+
82
+ def dimension_coordinates(self, **kwargs) -> list[str]:
83
+ return self._coords
84
+
85
+ def coordinate(self, key: str) -> _MockCoord:
86
+ return self._coords[key]
87
+
88
+
89
+ def test_coordinate_info_filters_singletons_and_serializes_values() -> None:
90
+ payload = coordinate_info(_MockCoordField())
91
+
92
+ assert payload == [
93
+ ("time", ["1", "2", "3"], ""),
94
+ ("latitude", ["-90", "0", "90"], ""),
95
+ ]
96
+
97
+
98
+ class _FakePlotField:
99
+ def __init__(self) -> None:
100
+ self.kwargs: dict[str, object] | None = None
101
+ self.collapse_calls: list[tuple[str, str, bool]] = []
102
+
103
+ def subspace(self, **kwargs: object) -> "_FakePlotField":
104
+ self.kwargs = kwargs
105
+ return self
106
+
107
+ def dimension_coordinate(self, coord_name) -> None:
108
+ return np.array([0.0, 1.0])
109
+
110
+ def collapse(self, instruction: str, weights: bool = False) -> "_FakePlotField":
111
+ self.collapse_calls.append((instruction, weights))
112
+ return self
113
+
114
+
115
+ def test_get_data_for_plotting_builds_subspace_kwargs() -> None:
116
+ fld = _FakePlotField()
117
+
118
+ pfld = get_data_for_plotting(
119
+ fld,
120
+ {
121
+ "time": ("3", "1"),
122
+ "level": ("850", "850"),
123
+ "name": ("foo", "foo"),
124
+ },
125
+ {"time": "mean", "name": "max"},
126
+ )
127
+
128
+ assert pfld is fld
129
+ assert fld.kwargs is not None
130
+ assert fld.kwargs["level"] == 850
131
+ assert fld.kwargs["name"] == "foo"
132
+ assert str(fld.kwargs["time"]) == str(cf.wi(1, 3))
133
+ assert fld.collapse_calls == [
134
+ ("time: mean name: max", True),
135
+ ]
136
+
137
+
138
+ class _FakeCFPlot:
139
+ def __init__(self) -> None:
140
+ self.levs_calls: list[dict[str, object]] = []
141
+ self.con_calls: list[dict[str, object]] = []
142
+ self.lineplot_calls: list[dict[str, object]] = []
143
+ self.cscale_calls: list[dict[str, object]] = []
144
+ self.gopen_calls: list[dict[str, object]] = []
145
+ self.setvars_calls: list[dict[str, object]] = []
146
+ self.gclose_calls = 0
147
+
148
+ def levs(self, **kwargs: object) -> None:
149
+ self.levs_calls.append(kwargs)
150
+
151
+ def con(self, _field: object, **kwargs: object) -> None:
152
+ self.con_calls.append(kwargs)
153
+
154
+ def lineplot(self, _field: object, **kwargs: object) -> None:
155
+ self.lineplot_calls.append(kwargs)
156
+
157
+ def cscale(self, **kwargs: object) -> None:
158
+ self.cscale_calls.append(kwargs)
159
+
160
+ def gopen(self, file: str = "cfplot.png", **kwargs: object) -> None:
161
+ payload: dict[str, object] = {"file": file}
162
+ payload.update(kwargs)
163
+ self.gopen_calls.append(payload)
164
+
165
+ def setvars(self, **kwargs: object) -> None:
166
+ self.setvars_calls.append(kwargs)
167
+
168
+ def gclose(self) -> None:
169
+ self.gclose_calls += 1
170
+
171
+
172
+ class _FakeFigure:
173
+ def __init__(self) -> None:
174
+ self.text_calls: list[tuple[tuple[object, ...], dict[str, object]]] = []
175
+ self.suptitle_calls: list[tuple[tuple[object, ...], dict[str, object]]] = []
176
+
177
+ def text(self, *args: object, **kwargs: object) -> None:
178
+ self.text_calls.append((args, kwargs))
179
+
180
+ def suptitle(self, *args: object, **kwargs: object) -> None:
181
+ self.suptitle_calls.append((args, kwargs))
182
+
183
+
184
+ class _FakePlt:
185
+ def __init__(self) -> None:
186
+ self.figure = _FakeFigure()
187
+
188
+ def gcf(self) -> _FakeFigure:
189
+ return self.figure
190
+
191
+
192
+ def test_run_contour_plot_applies_levels_annotations_and_save(
193
+ monkeypatch: pytest.MonkeyPatch,
194
+ ) -> None:
195
+ cfp = _FakeCFPlot()
196
+ plt_obj = _FakePlt()
197
+
198
+ monkeypatch.setattr(cf_interface, "cfp", cfp)
199
+ monkeypatch.setattr(cf_interface, "plt", plt_obj)
200
+
201
+ run_contour_plot(
202
+ pfld=object(),
203
+ options={
204
+ "mode": "explicit",
205
+ "levels": [-1.0, 0.0, 1.0],
206
+ "cscale": "magma",
207
+ "filename": "/tmp/mock.png",
208
+ "annotation_display": True,
209
+ "annotation_properties": [("units", "K")],
210
+ },
211
+ )
212
+
213
+ assert cfp.cscale_calls == [{"scale": "magma"}]
214
+ assert cfp.gopen_calls == [{"file": "/tmp/mock.png"}]
215
+ assert cfp.levs_calls == [{"manual": [-1.0, 0.0, 1.0]}]
216
+ assert cfp.setvars_calls == [{"title_fontsize": 10.5}]
217
+ assert cfp.con_calls
218
+ assert cfp.gclose_calls == 1
219
+ assert plt_obj.figure.text_calls
220
+ assert plt_obj.figure.text_calls[-1][1]["fontsize"] == 8.0
221
+
222
+
223
+ def test_run_contour_plot_uses_configured_title_font_sizes(
224
+ monkeypatch: pytest.MonkeyPatch,
225
+ ) -> None:
226
+ cfp = _FakeCFPlot()
227
+ plt_obj = _FakePlt()
228
+
229
+ monkeypatch.setattr(cf_interface, "cfp", cfp)
230
+ monkeypatch.setattr(cf_interface, "plt", plt_obj)
231
+
232
+ run_contour_plot(
233
+ pfld=object(),
234
+ options={
235
+ "mode": "default",
236
+ "page_title": "Overview",
237
+ "page_title_display": True,
238
+ "annotation_display": True,
239
+ "annotation_properties": [("units", "K")],
240
+ "contour_title_fontsize": 12.5,
241
+ "page_title_fontsize": 14.0,
242
+ "annotation_fontsize": 9.5,
243
+ },
244
+ )
245
+
246
+ assert cfp.setvars_calls == [{"title_fontsize": 12.5}]
247
+ assert plt_obj.figure.suptitle_calls == [
248
+ (("Overview",), {"y": 0.995, "fontsize": 14.0})
249
+ ]
250
+ assert plt_obj.figure.text_calls == [
251
+ ((0.5, 0.02, "units: K"), {"ha": "center", "va": "bottom", "fontsize": 9.5})
252
+ ]
253
+
254
+
255
+ def test_run_contour_plot_sets_title_from_singleton_selection(
256
+ monkeypatch: pytest.MonkeyPatch,
257
+ ) -> None:
258
+ cfp = _FakeCFPlot()
259
+ plt_obj = _FakePlt()
260
+
261
+ monkeypatch.setattr(cf_interface, "cfp", cfp)
262
+ monkeypatch.setattr(cf_interface, "plt", plt_obj)
263
+
264
+ run_contour_plot(
265
+ pfld=object(),
266
+ options={"mode": "default"},
267
+ selection_spec={"time": ("2000-01-01", "2000-01-01"), "lat": ("-90", "90")},
268
+ collapse_by_coord={},
269
+ )
270
+
271
+ assert cfp.gopen_calls == [{"file": "cfplot.png", "user_plot": 1}]
272
+ assert cfp.con_calls
273
+ assert cfp.con_calls[-1]["title"] == "time=2000-01-01"
274
+
275
+
276
+ def test_run_contour_plot_prefers_cell_method_title_for_collapses(
277
+ monkeypatch: pytest.MonkeyPatch,
278
+ ) -> None:
279
+ cfp = _FakeCFPlot()
280
+ plt_obj = _FakePlt()
281
+
282
+ monkeypatch.setattr(cf_interface, "cfp", cfp)
283
+ monkeypatch.setattr(cf_interface, "plt", plt_obj)
284
+ monkeypatch.setattr(cf_interface, "cell_methods_string_from_field", lambda _field, *args: "time: mean")
285
+
286
+ run_contour_plot(
287
+ pfld=object(),
288
+ options={"mode": "default"},
289
+ selection_spec={"time": ("2000-01-01", "2000-12-31")},
290
+ collapse_by_coord={"time": "mean"},
291
+ )
292
+
293
+ assert cfp.con_calls
294
+ assert cfp.con_calls[-1]["title"] == "time: mean"
295
+
296
+
297
+ def test_auto_contour_title_from_singleton_selection() -> None:
298
+ title = auto_contour_title(
299
+ pfld=object(),
300
+ selection_spec={"time": ("2001-01-01", "2001-01-01"), "lat": ("-90", "90")},
301
+ collapse_by_coord={},
302
+ )
303
+ assert title == "time=2001-01-01"
304
+
305
+
306
+ def test_auto_contour_title_prefers_cell_method_for_collapse(
307
+ monkeypatch: pytest.MonkeyPatch,
308
+ ) -> None:
309
+ monkeypatch.setattr(cf_interface, "cell_methods_string_from_field", lambda _field, *args: "time: mean")
310
+ title = auto_contour_title(
311
+ pfld=object(),
312
+ selection_spec={"time": ("2001-01-01", "2001-12-31")},
313
+ collapse_by_coord={"time": "mean"},
314
+ )
315
+ assert title == "time: mean"
316
+
317
+
318
+ def test_run_line_plot_uses_canonical_axes_and_wraps_file_output() -> None:
319
+ field_eg = object()
320
+ monkeypatch = pytest.MonkeyPatch()
321
+
322
+ captured: dict[str, object] = {}
323
+
324
+ class _FakeLinePlot:
325
+ def __init__(
326
+ self,
327
+ pfld: object,
328
+ options: dict[str, object] | None,
329
+ collapse_by_coord: dict[str, str] | None
330
+ ) -> None:
331
+ captured["pfld"] = pfld
332
+ captured["options"] = options
333
+
334
+ def render(self) -> None:
335
+ captured["rendered"] = True
336
+
337
+ monkeypatch.setattr(cf_interface, "LinePlot", _FakeLinePlot)
338
+
339
+ try:
340
+ run_line_plot(
341
+ pfld=field_eg,
342
+ options={"filename": "/tmp/line.png", "title": "line"},
343
+ selection_spec={"time": ("1", "2")},
344
+ collapse_by_coord={},
345
+ )
346
+ finally:
347
+ monkeypatch.undo()
348
+
349
+ assert captured["pfld"] is field_eg
350
+ assert captured["options"] == {"filename": "/tmp/line.png", "title": "line"}
351
+ assert captured["rendered"] is True
352
+