xconv2 0.0.0__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.
Files changed (63) hide show
  1. xconv2/__init__.py +19 -0
  2. xconv2/aaa/aaa_config.py +64 -0
  3. xconv2/animation_protocol.py +189 -0
  4. xconv2/animation_session.py +165 -0
  5. xconv2/assets/Pyfive-logo.png +0 -0
  6. xconv2/assets/UoR-logo.png +0 -0
  7. xconv2/assets/cf-logo-t.svg +30367 -0
  8. xconv2/assets/cf-logo.svg +30375 -0
  9. xconv2/assets/clipster.svg +71 -0
  10. xconv2/assets/dask-logo.svg +13 -0
  11. xconv2/assets/info-ncas.svg +5 -0
  12. xconv2/assets/ncas-logo.png +0 -0
  13. xconv2/assets/tooltip.svg +1 -0
  14. xconv2/assets/under-construction.svg +9 -0
  15. xconv2/cache_utils.py +293 -0
  16. xconv2/cell_method_handler.py +92 -0
  17. xconv2/cf_interface/__init__.py +76 -0
  18. xconv2/cf_interface/filtering.py +183 -0
  19. xconv2/cf_interface/lineplot.py +262 -0
  20. xconv2/cf_interface/maths.py +344 -0
  21. xconv2/cf_interface/metadata_operations.py +663 -0
  22. xconv2/cf_interface/plot_layout_helpers.py +139 -0
  23. xconv2/cf_interface/plotting.py +533 -0
  24. xconv2/cf_interface/regridding.py +189 -0
  25. xconv2/cf_templates.py +511 -0
  26. xconv2/colour_scales.py +82 -0
  27. xconv2/coordinate_subspace.py +87 -0
  28. xconv2/core_window.py +2651 -0
  29. xconv2/gui.py +128 -0
  30. xconv2/logging_utils.py +145 -0
  31. xconv2/main_window.py +2232 -0
  32. xconv2/main_window_components/__init__.py +1 -0
  33. xconv2/main_window_components/plot_ops.py +327 -0
  34. xconv2/main_window_components/remote_auth_ops.py +440 -0
  35. xconv2/main_window_components/remote_flow_ops.py +746 -0
  36. xconv2/main_window_components/replay_ops.py +474 -0
  37. xconv2/remote_access.py +758 -0
  38. xconv2/remote_fs.py +791 -0
  39. xconv2/science_environment.py +115 -0
  40. xconv2/tooltips.py +104 -0
  41. xconv2/ui/__init__.py +19 -0
  42. xconv2/ui/contour_options_controller.py +758 -0
  43. xconv2/ui/dialogs.py +2991 -0
  44. xconv2/ui/field_metadata_controller.py +378 -0
  45. xconv2/ui/lineplot_options_controller.py +176 -0
  46. xconv2/ui/menu_controller.py +224 -0
  47. xconv2/ui/plot_options_shared.py +258 -0
  48. xconv2/ui/plot_view_controller.py +831 -0
  49. xconv2/ui/remote_file_navigator.py +1226 -0
  50. xconv2/ui/science_stack_dialog.py +21 -0
  51. xconv2/ui/selection_controller.py +623 -0
  52. xconv2/ui/settings_store.py +297 -0
  53. xconv2/ui/vector_options_controller.py +197 -0
  54. xconv2/worker.py +1566 -0
  55. xconv2/worker_message_router.py +478 -0
  56. xconv2/workflow/provenance_export.py +416 -0
  57. xconv2/workflow/xconv_workflow_to_prov.py +301 -0
  58. xconv2-0.0.0.dist-info/METADATA +141 -0
  59. xconv2-0.0.0.dist-info/RECORD +63 -0
  60. xconv2-0.0.0.dist-info/WHEEL +5 -0
  61. xconv2-0.0.0.dist-info/entry_points.txt +5 -0
  62. xconv2-0.0.0.dist-info/licenses/LICENSE +21 -0
  63. xconv2-0.0.0.dist-info/top_level.txt +1 -0
xconv2/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """xconv2 package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import version, PackageNotFoundError
6
+
7
+
8
+ try:
9
+ __version__ = version("xconv2")
10
+ except PackageNotFoundError as exc:
11
+ msg = (
12
+ "xconv2 package not found, please run `pip install -e .` before "
13
+ "importing the package."
14
+ )
15
+ raise PackageNotFoundError(
16
+ msg,
17
+ ) from exc
18
+
19
+
@@ -0,0 +1,64 @@
1
+ from pathlib import Path
2
+ import json
3
+ import warnings
4
+ from urllib.parse import urlparse
5
+
6
+ def get_locations(config_file=None):
7
+ """
8
+ Read config file (config.json) and find usable locations.
9
+ We are expecting a file in the "minio configuration format",
10
+ and we expect it in either ~/.mc/ or in ~/.config/cfview.
11
+ """
12
+
13
+ if config_file is None:
14
+ candidates = [
15
+ Path.home() / '.mc/config.json',
16
+ Path.home() / '.config/cfview/config.json',
17
+ ]
18
+ config_file = next((path for path in candidates if path.is_file()), None)
19
+ if config_file is None:
20
+ return None
21
+ else:
22
+ config_file = Path(config_file).expanduser()
23
+ if not config_file.is_file():
24
+ raise FileNotFoundError(f'Configuration file not found: {config_file}')
25
+
26
+ try:
27
+ with open(config_file, 'r', encoding='utf-8') as jfile:
28
+ jdata = json.load(jfile)
29
+ jd = jdata['aliases']
30
+ except (json.JSONDecodeError, KeyError, TypeError) as exc:
31
+ raise ValueError(f'Malformed configuration file: {config_file}') from exc
32
+
33
+ locations = {}
34
+ servers = {}
35
+ for k, v in jd.items():
36
+ api = v.get('api')
37
+ if (api or '').lower() != 's3v4':
38
+ warnings.warn(f'WARNING: Found unexpected S3 API {api} for {k} in configuration file {config_file}')
39
+ else:
40
+ locations[k] = v
41
+
42
+ parsed = urlparse(v.get('url', ''))
43
+ host = parsed.netloc or parsed.path
44
+ if host:
45
+ servers[host] = k
46
+
47
+ return locations, servers
48
+
49
+
50
+ def get_user_config(target, config_file=None):
51
+ """
52
+ Obtain credentials from user configuration file
53
+ """
54
+ if config_file is None:
55
+ config_file = Path.home()/'.mc/config.json'
56
+ options = get_locations(config_file)
57
+ if options is None:
58
+ raise ValueError(f'No configuration file found for target [{target}]')
59
+
60
+ locations, _ = options
61
+ try:
62
+ return locations[target]
63
+ except KeyError:
64
+ raise ValueError(f'Target [{target}] not found in ~/{config_file}')
@@ -0,0 +1,189 @@
1
+ """Animation protocol types and message helpers for xconv2 streaming animation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ import pickle
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+
13
+ @dataclass
14
+ class AnimationMetadata:
15
+ """Metadata for an animation session."""
16
+
17
+ session_id: str | None
18
+ total_frames: int | None
19
+ fps_hint: float | None
20
+ title_template: str | None
21
+ plot_kind: str
22
+ levels_locked: bool
23
+
24
+ def to_dict(self) -> dict[str, Any]:
25
+ """Convert to payload dict."""
26
+ return {
27
+ "session_id": self.session_id,
28
+ "total_frames": self.total_frames,
29
+ "fps_hint": self.fps_hint,
30
+ "title_template": self.title_template,
31
+ "plot_kind": self.plot_kind,
32
+ "levels_locked": self.levels_locked,
33
+ }
34
+
35
+ @classmethod
36
+ def from_dict(cls, data: dict[str, Any]) -> AnimationMetadata:
37
+ """Construct from payload dict."""
38
+ return cls(
39
+ session_id=data.get("session_id"),
40
+ total_frames=data.get("total_frames"),
41
+ fps_hint=data.get("fps_hint"),
42
+ title_template=data.get("title_template"),
43
+ plot_kind=str(data.get("plot_kind", "contour")),
44
+ levels_locked=bool(data.get("levels_locked", False)),
45
+ )
46
+
47
+
48
+ @dataclass
49
+ class AnimationFrame:
50
+ """A single frame in an animation stream."""
51
+
52
+ request_id: str
53
+ session_id: str | None
54
+ frame_index: int
55
+ total_frames: int | None
56
+ png_bytes: bytes
57
+ frame_value_label: str | None
58
+ emitted_at: float
59
+
60
+ def to_dict(self) -> dict[str, Any]:
61
+ """Convert to payload dict (bytes included as-is)."""
62
+ return {
63
+ "request_id": self.request_id,
64
+ "session_id": self.session_id,
65
+ "frame_index": self.frame_index,
66
+ "total_frames": self.total_frames,
67
+ "png_bytes": self.png_bytes,
68
+ "frame_value_label": self.frame_value_label,
69
+ "emitted_at": self.emitted_at,
70
+ }
71
+
72
+ @classmethod
73
+ def from_dict(cls, data: dict[str, Any]) -> AnimationFrame:
74
+ """Construct from payload dict."""
75
+ return cls(
76
+ request_id=str(data.get("request_id", "")),
77
+ session_id=data.get("session_id"),
78
+ frame_index=int(data.get("frame_index", 0)),
79
+ total_frames=data.get("total_frames"),
80
+ png_bytes=bytes(data.get("png_bytes", b"")),
81
+ frame_value_label=data.get("frame_value_label"),
82
+ emitted_at=float(data.get("emitted_at", time.time())),
83
+ )
84
+
85
+
86
+ @dataclass
87
+ class AnimationStart:
88
+ """Animation session start notification."""
89
+
90
+ request_id: str
91
+ session_id: str | None
92
+ total_frames: int | None
93
+ fps_hint: float | None
94
+ title_template: str | None
95
+ started_at: float
96
+
97
+ def to_dict(self) -> dict[str, Any]:
98
+ """Convert to payload dict."""
99
+ return {
100
+ "request_id": self.request_id,
101
+ "session_id": self.session_id,
102
+ "total_frames": self.total_frames,
103
+ "fps_hint": self.fps_hint,
104
+ "title_template": self.title_template,
105
+ "started_at": self.started_at,
106
+ }
107
+
108
+ @classmethod
109
+ def from_dict(cls, data: dict[str, Any]) -> AnimationStart:
110
+ """Construct from payload dict."""
111
+ return cls(
112
+ request_id=str(data.get("request_id", "")),
113
+ session_id=data.get("session_id"),
114
+ total_frames=data.get("total_frames"),
115
+ fps_hint=data.get("fps_hint"),
116
+ title_template=data.get("title_template"),
117
+ started_at=float(data.get("started_at", time.time())),
118
+ )
119
+
120
+
121
+ @dataclass
122
+ class AnimationEnd:
123
+ """Animation session completion notification."""
124
+
125
+ request_id: str
126
+ session_id: str | None
127
+ frames_emitted: int
128
+ completed_at: float
129
+
130
+ def to_dict(self) -> dict[str, Any]:
131
+ """Convert to payload dict."""
132
+ return {
133
+ "request_id": self.request_id,
134
+ "session_id": self.session_id,
135
+ "frames_emitted": self.frames_emitted,
136
+ "completed_at": self.completed_at,
137
+ }
138
+
139
+ @classmethod
140
+ def from_dict(cls, data: dict[str, Any]) -> AnimationEnd:
141
+ """Construct from payload dict."""
142
+ return cls(
143
+ request_id=str(data.get("request_id", "")),
144
+ session_id=data.get("session_id"),
145
+ frames_emitted=int(data.get("frames_emitted", 0)),
146
+ completed_at=float(data.get("completed_at", time.time())),
147
+ )
148
+
149
+
150
+ @dataclass
151
+ class AnimationError:
152
+ """Animation session error notification."""
153
+
154
+ request_id: str
155
+ session_id: str | None
156
+ frame_index: int | None
157
+ error: str
158
+ failed_at: float
159
+
160
+ def to_dict(self) -> dict[str, Any]:
161
+ """Convert to payload dict."""
162
+ return {
163
+ "request_id": self.request_id,
164
+ "session_id": self.session_id,
165
+ "frame_index": self.frame_index,
166
+ "error": self.error,
167
+ "failed_at": self.failed_at,
168
+ }
169
+
170
+ @classmethod
171
+ def from_dict(cls, data: dict[str, Any]) -> AnimationError:
172
+ """Construct from payload dict."""
173
+ return cls(
174
+ request_id=str(data.get("request_id", "")),
175
+ session_id=data.get("session_id"),
176
+ frame_index=data.get("frame_index"),
177
+ error=str(data.get("error", "")),
178
+ failed_at=float(data.get("failed_at", time.time())),
179
+ )
180
+
181
+
182
+ def encode_payload(payload: dict[str, Any]) -> str:
183
+ """Encode a payload dict as base64(pickle) for stdout transmission."""
184
+ return base64.b64encode(pickle.dumps(payload)).decode("ascii")
185
+
186
+
187
+ def decode_payload(encoded: str) -> dict[str, Any]:
188
+ """Decode a base64(pickle) payload from stdout."""
189
+ return pickle.loads(base64.b64decode(encoded.encode("ascii")))
@@ -0,0 +1,165 @@
1
+ """Backend animation session controller for xconv2."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+ from dataclasses import dataclass, field
8
+ from enum import Enum
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class AnimationPlaybackState(Enum):
14
+ """Playback state for an animation session."""
15
+
16
+ IDLE = "idle"
17
+ STREAMING = "streaming"
18
+ BUFFERED = "buffered"
19
+ PLAYING = "playing"
20
+ PAUSED = "paused"
21
+ STOPPED = "stopped"
22
+ COMPLETED = "completed"
23
+ FAILED = "failed"
24
+
25
+
26
+ @dataclass
27
+ class AnimationSession:
28
+ """Backend state for a single animation session."""
29
+
30
+ request_id: str
31
+ session_id: str | None = None
32
+ total_frames: int | None = None
33
+ fps_hint: float | None = None
34
+ title_template: str | None = None
35
+ frames: list[bytes] = field(default_factory=list)
36
+ playback_state: AnimationPlaybackState = field(default=AnimationPlaybackState.IDLE)
37
+ current_frame_index: int = 0
38
+ error_message: str | None = None
39
+ started_at: float | None = None
40
+ completed_at: float | None = None
41
+
42
+ def add_frame(self, png_bytes: bytes) -> None:
43
+ """Append a frame to the buffer."""
44
+ if self.playback_state == AnimationPlaybackState.FAILED:
45
+ logger.warning("Ignoring frame for failed session %s", self.request_id)
46
+ return
47
+ self.frames.append(png_bytes)
48
+ if self.playback_state == AnimationPlaybackState.IDLE:
49
+ self.playback_state = AnimationPlaybackState.STREAMING
50
+
51
+ def frame_count(self) -> int:
52
+ """Return number of buffered frames."""
53
+ return len(self.frames)
54
+
55
+ def get_frame(self, index: int) -> bytes | None:
56
+ """Get frame at index, or None if out of bounds."""
57
+ if 0 <= index < len(self.frames):
58
+ return self.frames[index]
59
+ return None
60
+
61
+ def mark_started(self, total_frames: int | None, fps_hint: float | None, title_template: str | None) -> None:
62
+ """Mark session as started with metadata."""
63
+ self.started_at = time.time()
64
+ self.total_frames = total_frames
65
+ self.fps_hint = fps_hint or 4.0
66
+ self.title_template = title_template
67
+ self.playback_state = AnimationPlaybackState.STREAMING
68
+
69
+ def mark_completed(self) -> None:
70
+ """Mark session as completed."""
71
+ if self.playback_state != AnimationPlaybackState.FAILED:
72
+ self.playback_state = AnimationPlaybackState.BUFFERED
73
+ self.completed_at = time.time()
74
+ logger.info(
75
+ "Animation session complete request_id=%s session_id=%s frames=%d",
76
+ self.request_id,
77
+ self.session_id,
78
+ len(self.frames),
79
+ )
80
+
81
+ def mark_failed(self, error: str) -> None:
82
+ """Mark session as failed."""
83
+ self.playback_state = AnimationPlaybackState.FAILED
84
+ self.error_message = error
85
+ logger.error("Animation session failed request_id=%s: %s", self.request_id, error)
86
+
87
+ def start_playback(self) -> None:
88
+ """Transition from buffered to playing."""
89
+ if self.playback_state == AnimationPlaybackState.BUFFERED:
90
+ self.playback_state = AnimationPlaybackState.PLAYING
91
+ self.current_frame_index = 0
92
+ logger.info("Playback started for request_id=%s", self.request_id)
93
+
94
+ def pause_playback(self) -> None:
95
+ """Pause playback."""
96
+ if self.playback_state == AnimationPlaybackState.PLAYING:
97
+ self.playback_state = AnimationPlaybackState.PAUSED
98
+ logger.info("Playback paused for request_id=%s at frame %d", self.request_id, self.current_frame_index)
99
+
100
+ def resume_playback(self) -> None:
101
+ """Resume from pause."""
102
+ if self.playback_state == AnimationPlaybackState.PAUSED:
103
+ self.playback_state = AnimationPlaybackState.PLAYING
104
+ logger.info("Playback resumed for request_id=%s", self.request_id)
105
+
106
+ def stop_playback(self) -> None:
107
+ """Stop playback and reset to frame 0."""
108
+ if self.playback_state in (AnimationPlaybackState.PLAYING, AnimationPlaybackState.PAUSED):
109
+ self.current_frame_index = 0
110
+ self.playback_state = AnimationPlaybackState.STOPPED
111
+ logger.info("Playback stopped for request_id=%s", self.request_id)
112
+
113
+ def next_frame(self) -> bytes | None:
114
+ """Advance to next frame and return its bytes, or None if at end."""
115
+ if self.current_frame_index < len(self.frames):
116
+ frame = self.frames[self.current_frame_index]
117
+ self.current_frame_index += 1
118
+ return frame
119
+ return None
120
+
121
+ def seek_to_frame(self, index: int) -> bytes | None:
122
+ """Seek to a specific frame and return its bytes."""
123
+ if 0 <= index < len(self.frames):
124
+ self.current_frame_index = index
125
+ return self.frames[index]
126
+ return None
127
+
128
+ def reset_playback(self) -> None:
129
+ """Reset playback to frame 0 without changing state."""
130
+ self.current_frame_index = 0
131
+
132
+
133
+ class AnimationSessionController:
134
+ """Manager for multiple concurrent animation sessions."""
135
+
136
+ def __init__(self) -> None:
137
+ self._sessions: dict[str, AnimationSession] = {}
138
+
139
+ def create_session(self, request_id: str, session_id: str | None = None) -> AnimationSession:
140
+ """Create a new animation session."""
141
+ session = AnimationSession(request_id=request_id, session_id=session_id)
142
+ self._sessions[request_id] = session
143
+ logger.info("Created animation session request_id=%s session_id=%s", request_id, session_id)
144
+ return session
145
+
146
+ def get_session(self, request_id: str) -> AnimationSession | None:
147
+ """Retrieve an existing session by request_id."""
148
+ return self._sessions.get(request_id)
149
+
150
+ def end_session(self, request_id: str) -> None:
151
+ """Remove a session from tracking."""
152
+ if request_id in self._sessions:
153
+ del self._sessions[request_id]
154
+ logger.info("Ended animation session request_id=%s", request_id)
155
+
156
+ def list_sessions(self) -> list[AnimationSession]:
157
+ """Return all active sessions."""
158
+ return list(self._sessions.values())
159
+
160
+ def cancel_session(self, request_id: str) -> None:
161
+ """Cancel/abandon a session."""
162
+ session = self._sessions.get(request_id)
163
+ if session:
164
+ session.mark_failed("User cancelled")
165
+ logger.info("Cancelled animation session request_id=%s", request_id)
Binary file
Binary file