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.
- xconv2/__init__.py +19 -0
- xconv2/aaa/aaa_config.py +64 -0
- xconv2/animation_protocol.py +189 -0
- xconv2/animation_session.py +165 -0
- xconv2/assets/Pyfive-logo.png +0 -0
- xconv2/assets/UoR-logo.png +0 -0
- xconv2/assets/cf-logo-t.svg +30367 -0
- xconv2/assets/cf-logo.svg +30375 -0
- xconv2/assets/clipster.svg +71 -0
- xconv2/assets/dask-logo.svg +13 -0
- xconv2/assets/info-ncas.svg +5 -0
- xconv2/assets/ncas-logo.png +0 -0
- xconv2/assets/tooltip.svg +1 -0
- xconv2/assets/under-construction.svg +9 -0
- xconv2/cache_utils.py +293 -0
- xconv2/cell_method_handler.py +92 -0
- xconv2/cf_interface/__init__.py +76 -0
- xconv2/cf_interface/filtering.py +183 -0
- xconv2/cf_interface/lineplot.py +262 -0
- xconv2/cf_interface/maths.py +344 -0
- xconv2/cf_interface/metadata_operations.py +663 -0
- xconv2/cf_interface/plot_layout_helpers.py +139 -0
- xconv2/cf_interface/plotting.py +533 -0
- xconv2/cf_interface/regridding.py +189 -0
- xconv2/cf_templates.py +511 -0
- xconv2/colour_scales.py +82 -0
- xconv2/coordinate_subspace.py +87 -0
- xconv2/core_window.py +2651 -0
- xconv2/gui.py +128 -0
- xconv2/logging_utils.py +145 -0
- xconv2/main_window.py +2232 -0
- xconv2/main_window_components/__init__.py +1 -0
- xconv2/main_window_components/plot_ops.py +327 -0
- xconv2/main_window_components/remote_auth_ops.py +440 -0
- xconv2/main_window_components/remote_flow_ops.py +746 -0
- xconv2/main_window_components/replay_ops.py +474 -0
- xconv2/remote_access.py +758 -0
- xconv2/remote_fs.py +791 -0
- xconv2/science_environment.py +115 -0
- xconv2/tooltips.py +104 -0
- xconv2/ui/__init__.py +19 -0
- xconv2/ui/contour_options_controller.py +758 -0
- xconv2/ui/dialogs.py +2991 -0
- xconv2/ui/field_metadata_controller.py +378 -0
- xconv2/ui/lineplot_options_controller.py +176 -0
- xconv2/ui/menu_controller.py +224 -0
- xconv2/ui/plot_options_shared.py +258 -0
- xconv2/ui/plot_view_controller.py +831 -0
- xconv2/ui/remote_file_navigator.py +1226 -0
- xconv2/ui/science_stack_dialog.py +21 -0
- xconv2/ui/selection_controller.py +623 -0
- xconv2/ui/settings_store.py +297 -0
- xconv2/ui/vector_options_controller.py +197 -0
- xconv2/worker.py +1566 -0
- xconv2/worker_message_router.py +478 -0
- xconv2/workflow/provenance_export.py +416 -0
- xconv2/workflow/xconv_workflow_to_prov.py +301 -0
- xconv2-0.0.0.dist-info/METADATA +141 -0
- xconv2-0.0.0.dist-info/RECORD +63 -0
- xconv2-0.0.0.dist-info/WHEEL +5 -0
- xconv2-0.0.0.dist-info/entry_points.txt +5 -0
- xconv2-0.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
|
xconv2/aaa/aaa_config.py
ADDED
|
@@ -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
|