datalab-platform 1.0.3__py3-none-any.whl → 1.1.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.
- datalab/__init__.py +1 -1
- datalab/config.py +4 -0
- datalab/control/baseproxy.py +160 -0
- datalab/control/remote.py +175 -1
- datalab/data/doc/DataLab_en.pdf +0 -0
- datalab/data/doc/DataLab_fr.pdf +0 -0
- datalab/data/icons/control/copy_connection_info.svg +11 -0
- datalab/data/icons/control/start_webapi_server.svg +19 -0
- datalab/data/icons/control/stop_webapi_server.svg +7 -0
- datalab/gui/docks.py +3 -2
- datalab/gui/main.py +221 -2
- datalab/gui/settings.py +10 -0
- datalab/gui/tour.py +2 -3
- datalab/locale/fr/LC_MESSAGES/datalab.mo +0 -0
- datalab/locale/fr/LC_MESSAGES/datalab.po +95 -1
- datalab/tests/__init__.py +32 -1
- datalab/tests/backbone/config_unit_test.py +1 -1
- datalab/tests/backbone/main_app_test.py +4 -0
- datalab/tests/backbone/memory_leak.py +1 -1
- datalab/tests/features/common/createobject_unit_test.py +1 -1
- datalab/tests/features/common/misc_app_test.py +5 -0
- datalab/tests/features/control/call_method_unit_test.py +104 -0
- datalab/tests/features/control/embedded1_unit_test.py +8 -0
- datalab/tests/features/control/remoteclient_app_test.py +39 -35
- datalab/tests/features/control/simpleclient_unit_test.py +7 -3
- datalab/tests/features/hdf5/h5browser2_unit.py +1 -1
- datalab/tests/features/image/background_dialog_test.py +2 -2
- datalab/tests/features/image/imagetools_unit_test.py +1 -1
- datalab/tests/features/signal/baseline_dialog_test.py +1 -1
- datalab/tests/webapi_test.py +395 -0
- datalab/webapi/__init__.py +95 -0
- datalab/webapi/actions.py +318 -0
- datalab/webapi/adapter.py +642 -0
- datalab/webapi/controller.py +379 -0
- datalab/webapi/routes.py +576 -0
- datalab/webapi/schema.py +198 -0
- datalab/webapi/serialization.py +388 -0
- datalab/widgets/status.py +61 -0
- {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/METADATA +11 -7
- {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/RECORD +46 -34
- {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/WHEEL +1 -1
- /datalab/data/icons/{libre-gui-link.svg → control/libre-gui-link.svg} +0 -0
- /datalab/data/icons/{libre-gui-unlink.svg → control/libre-gui-unlink.svg} +0 -0
- {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/entry_points.txt +0 -0
- {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/top_level.txt +0 -0
datalab/webapi/schema.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause License
|
|
2
|
+
# See LICENSE file for details
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Web API Schema Definitions
|
|
6
|
+
==========================
|
|
7
|
+
|
|
8
|
+
Pydantic models defining the API contract for DataLab Web API.
|
|
9
|
+
|
|
10
|
+
These schemas define:
|
|
11
|
+
|
|
12
|
+
- Object metadata (signals and images)
|
|
13
|
+
- API request/response payloads
|
|
14
|
+
- Event messages
|
|
15
|
+
|
|
16
|
+
Design Principles
|
|
17
|
+
-----------------
|
|
18
|
+
|
|
19
|
+
1. **Minimal but complete**: Include only essential metadata for workspace operations
|
|
20
|
+
2. **Type-safe**: Full type annotations for all fields
|
|
21
|
+
3. **Serializable**: All models can be serialized to JSON
|
|
22
|
+
4. **Extensible**: Designed for future additions without breaking changes
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from enum import Enum
|
|
28
|
+
from typing import Any, Optional
|
|
29
|
+
|
|
30
|
+
from pydantic import BaseModel, Field
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ObjectType(str, Enum):
|
|
34
|
+
"""Type of data object in the workspace."""
|
|
35
|
+
|
|
36
|
+
SIGNAL = "signal"
|
|
37
|
+
IMAGE = "image"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ObjectMetadata(BaseModel):
|
|
41
|
+
"""Metadata for a workspace object.
|
|
42
|
+
|
|
43
|
+
This is the core representation of an object in the API.
|
|
44
|
+
The actual data is transferred separately via the binary data plane.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
name: str = Field(..., description="Unique object identifier/title")
|
|
48
|
+
type: ObjectType = Field(..., description="Object type (signal or image)")
|
|
49
|
+
shape: list[int] = Field(..., description="Array shape (e.g., [100] or [512, 512])")
|
|
50
|
+
dtype: str = Field(..., description="NumPy dtype string (e.g., 'float64')")
|
|
51
|
+
|
|
52
|
+
# Optional metadata
|
|
53
|
+
title: Optional[str] = Field(None, description="Display title")
|
|
54
|
+
xlabel: Optional[str] = Field(None, description="X-axis label")
|
|
55
|
+
ylabel: Optional[str] = Field(None, description="Y-axis label")
|
|
56
|
+
zlabel: Optional[str] = Field(None, description="Z-axis label (images only)")
|
|
57
|
+
xunit: Optional[str] = Field(None, description="X-axis unit")
|
|
58
|
+
yunit: Optional[str] = Field(None, description="Y-axis unit")
|
|
59
|
+
zunit: Optional[str] = Field(None, description="Z-axis unit (images only)")
|
|
60
|
+
|
|
61
|
+
# Extended attributes
|
|
62
|
+
attributes: dict[str, Any] = Field(
|
|
63
|
+
default_factory=dict, description="Additional key/value metadata"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
model_config = {"extra": "ignore"}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ObjectListResponse(BaseModel):
|
|
70
|
+
"""Response for listing workspace objects."""
|
|
71
|
+
|
|
72
|
+
objects: list[ObjectMetadata] = Field(..., description="List of object metadata")
|
|
73
|
+
count: int = Field(..., description="Total number of objects")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ObjectCreateRequest(BaseModel):
|
|
77
|
+
"""Request to create a new object (metadata only, data sent separately)."""
|
|
78
|
+
|
|
79
|
+
name: str = Field(..., description="Object name/title")
|
|
80
|
+
type: ObjectType = Field(..., description="Object type")
|
|
81
|
+
overwrite: bool = Field(False, description="Replace existing object if present")
|
|
82
|
+
|
|
83
|
+
# Optional metadata to set
|
|
84
|
+
title: Optional[str] = Field(None, description="Display title")
|
|
85
|
+
xlabel: Optional[str] = Field(None, description="X-axis label")
|
|
86
|
+
ylabel: Optional[str] = Field(None, description="Y-axis label")
|
|
87
|
+
zlabel: Optional[str] = Field(None, description="Z-axis label (images only)")
|
|
88
|
+
xunit: Optional[str] = Field(None, description="X-axis unit")
|
|
89
|
+
yunit: Optional[str] = Field(None, description="Y-axis unit")
|
|
90
|
+
zunit: Optional[str] = Field(None, description="Z-axis unit (images only)")
|
|
91
|
+
attributes: dict[str, Any] = Field(
|
|
92
|
+
default_factory=dict, description="Additional metadata"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class MetadataPatchRequest(BaseModel):
|
|
97
|
+
"""Request to update object metadata."""
|
|
98
|
+
|
|
99
|
+
title: Optional[str] = Field(None, description="New display title")
|
|
100
|
+
xlabel: Optional[str] = Field(None, description="New X-axis label")
|
|
101
|
+
ylabel: Optional[str] = Field(None, description="New Y-axis label")
|
|
102
|
+
zlabel: Optional[str] = Field(None, description="New Z-axis label")
|
|
103
|
+
xunit: Optional[str] = Field(None, description="New X-axis unit")
|
|
104
|
+
yunit: Optional[str] = Field(None, description="New Y-axis unit")
|
|
105
|
+
zunit: Optional[str] = Field(None, description="New Z-axis unit")
|
|
106
|
+
attributes: Optional[dict[str, Any]] = Field(
|
|
107
|
+
None, description="Attributes to merge (not replace)"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ApiStatus(BaseModel):
|
|
112
|
+
"""API server status information."""
|
|
113
|
+
|
|
114
|
+
running: bool = Field(..., description="Whether the API server is running")
|
|
115
|
+
version: str = Field(..., description="DataLab version")
|
|
116
|
+
api_version: str = Field("v1", description="API version")
|
|
117
|
+
url: Optional[str] = Field(None, description="Base URL when running")
|
|
118
|
+
workspace_mode: str = Field(..., description="Current workspace mode")
|
|
119
|
+
localhost_no_token: bool = Field(
|
|
120
|
+
False, description="Whether localhost connections can bypass authentication"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ErrorResponse(BaseModel):
|
|
125
|
+
"""Standard error response."""
|
|
126
|
+
|
|
127
|
+
error: str = Field(..., description="Error type/code")
|
|
128
|
+
message: str = Field(..., description="Human-readable error message")
|
|
129
|
+
detail: Optional[str] = Field(None, description="Additional detail")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# Event types for WebSocket (future)
|
|
133
|
+
class EventType(str, Enum):
|
|
134
|
+
"""Types of workspace events."""
|
|
135
|
+
|
|
136
|
+
OBJECT_ADDED = "object_added"
|
|
137
|
+
OBJECT_UPDATED = "object_updated"
|
|
138
|
+
OBJECT_REMOVED = "object_removed"
|
|
139
|
+
OBJECT_RENAMED = "object_renamed"
|
|
140
|
+
WORKSPACE_CLEARED = "workspace_cleared"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class WorkspaceEvent(BaseModel):
|
|
144
|
+
"""A workspace change event (for WebSocket notifications)."""
|
|
145
|
+
|
|
146
|
+
event: EventType = Field(..., description="Event type")
|
|
147
|
+
object_name: Optional[str] = Field(None, description="Affected object name")
|
|
148
|
+
old_name: Optional[str] = Field(None, description="Old name (for rename events)")
|
|
149
|
+
timestamp: float = Field(..., description="Unix timestamp")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# =============================================================================
|
|
153
|
+
# Computation API schemas
|
|
154
|
+
# =============================================================================
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class SelectObjectsRequest(BaseModel):
|
|
158
|
+
"""Request to select objects in a panel."""
|
|
159
|
+
|
|
160
|
+
selection: list[str] = Field(
|
|
161
|
+
..., description="List of object names/titles to select"
|
|
162
|
+
)
|
|
163
|
+
panel: Optional[ObjectType] = Field(
|
|
164
|
+
None, description="Panel to select in (signal/image). None = current panel."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class SelectObjectsResponse(BaseModel):
|
|
169
|
+
"""Response for object selection."""
|
|
170
|
+
|
|
171
|
+
selected: list[str] = Field(..., description="List of selected object names")
|
|
172
|
+
panel: str = Field(..., description="Panel where selection was made")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class CalcRequest(BaseModel):
|
|
176
|
+
"""Request to call a computation function.
|
|
177
|
+
|
|
178
|
+
The computation is applied to the currently selected objects in DataLab.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
name: str = Field(
|
|
182
|
+
..., description="Computation function name (e.g., 'normalize', 'fft')"
|
|
183
|
+
)
|
|
184
|
+
param: Optional[dict[str, Any]] = Field(
|
|
185
|
+
None,
|
|
186
|
+
description="Computation parameters as a dictionary. "
|
|
187
|
+
"Keys are parameter names, values are parameter values.",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class CalcResponse(BaseModel):
|
|
192
|
+
"""Response from a computation."""
|
|
193
|
+
|
|
194
|
+
success: bool = Field(..., description="Whether computation succeeded")
|
|
195
|
+
function: str = Field(..., description="Name of the computation function called")
|
|
196
|
+
result_names: list[str] = Field(
|
|
197
|
+
default_factory=list, description="Names of newly created result objects"
|
|
198
|
+
)
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause License
|
|
2
|
+
# See LICENSE file for details
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Web API Serialization
|
|
6
|
+
=====================
|
|
7
|
+
|
|
8
|
+
Binary serialization of DataLab objects using NumPy's NPZ format.
|
|
9
|
+
|
|
10
|
+
This module handles the data plane of the Web API, efficiently transferring
|
|
11
|
+
large numerical arrays between DataLab and clients.
|
|
12
|
+
|
|
13
|
+
Format Specification (V1)
|
|
14
|
+
-------------------------
|
|
15
|
+
|
|
16
|
+
The NPZ archive contains:
|
|
17
|
+
|
|
18
|
+
For SignalObj:
|
|
19
|
+
- ``x.npy``: X coordinates (1D float64 array)
|
|
20
|
+
- ``y.npy``: Y data (1D float64 array)
|
|
21
|
+
- ``dx.npy``: X uncertainties (optional)
|
|
22
|
+
- ``dy.npy``: Y uncertainties (optional)
|
|
23
|
+
- ``metadata.json``: JSON-encoded metadata
|
|
24
|
+
|
|
25
|
+
For ImageObj:
|
|
26
|
+
- ``data.npy``: Image data (2D array, preserves dtype)
|
|
27
|
+
- ``metadata.json``: JSON-encoded metadata
|
|
28
|
+
|
|
29
|
+
Metadata JSON includes:
|
|
30
|
+
- ``type``: "signal" or "image"
|
|
31
|
+
- ``title``, ``xlabel``, ``ylabel``, etc.
|
|
32
|
+
- ``x0``, ``y0``, ``dx``, ``dy`` (for images)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import io
|
|
38
|
+
import json
|
|
39
|
+
import zipfile
|
|
40
|
+
from typing import TYPE_CHECKING, Union
|
|
41
|
+
|
|
42
|
+
import numpy as np
|
|
43
|
+
from sigima.objects import ImageObj, SignalObj
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
DataObject = Union[SignalObj, ImageObj]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _to_python_scalar(value):
|
|
50
|
+
"""Convert numpy scalar types to Python native types for JSON serialization.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
value: Any value, possibly a numpy scalar.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Python native type if input was a numpy scalar, otherwise unchanged.
|
|
57
|
+
"""
|
|
58
|
+
if isinstance(value, np.integer):
|
|
59
|
+
return int(value)
|
|
60
|
+
if isinstance(value, np.floating):
|
|
61
|
+
return float(value)
|
|
62
|
+
if isinstance(value, np.bool_):
|
|
63
|
+
return bool(value)
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _make_json_serializable(value): # pylint: disable=too-many-return-statements
|
|
68
|
+
"""Recursively convert a value to be JSON serializable.
|
|
69
|
+
|
|
70
|
+
Handles numpy arrays, numpy scalars, nested dicts, and lists.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
value: Any value that may contain numpy types.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
JSON-serializable value.
|
|
77
|
+
"""
|
|
78
|
+
if isinstance(value, dict):
|
|
79
|
+
return {k: _make_json_serializable(v) for k, v in value.items()}
|
|
80
|
+
if isinstance(value, list):
|
|
81
|
+
return [_make_json_serializable(item) for item in value]
|
|
82
|
+
if isinstance(value, tuple):
|
|
83
|
+
return [_make_json_serializable(item) for item in value]
|
|
84
|
+
if isinstance(value, np.ndarray):
|
|
85
|
+
return value.tolist()
|
|
86
|
+
if isinstance(value, np.integer):
|
|
87
|
+
return int(value)
|
|
88
|
+
if isinstance(value, np.floating):
|
|
89
|
+
return float(value)
|
|
90
|
+
if isinstance(value, np.bool_):
|
|
91
|
+
return bool(value)
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _serialize_obj_metadata(obj_metadata: dict) -> dict:
|
|
96
|
+
"""Convert object metadata to JSON-serializable format.
|
|
97
|
+
|
|
98
|
+
Handles numpy arrays (from GeometryResult.coords, TableResult, etc.)
|
|
99
|
+
by converting them to nested lists, and numpy scalar types (int64, float64)
|
|
100
|
+
to Python native types.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
obj_metadata: The object's metadata dictionary.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
JSON-serializable dictionary.
|
|
107
|
+
"""
|
|
108
|
+
return _make_json_serializable(obj_metadata)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _deserialize_obj_metadata(serialized: dict) -> dict:
|
|
112
|
+
"""Convert deserialized metadata back to original types.
|
|
113
|
+
|
|
114
|
+
Converts nested lists back to numpy arrays where appropriate
|
|
115
|
+
(for coords fields in geometry results).
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
serialized: The JSON-deserialized metadata.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Metadata with numpy arrays restored.
|
|
122
|
+
"""
|
|
123
|
+
result = {}
|
|
124
|
+
for key, value in serialized.items():
|
|
125
|
+
if isinstance(value, dict):
|
|
126
|
+
# Recursively handle nested dicts
|
|
127
|
+
result[key] = _deserialize_obj_metadata(value)
|
|
128
|
+
elif isinstance(value, list) and key == "coords":
|
|
129
|
+
# Convert coords back to numpy array
|
|
130
|
+
result[key] = np.array(value)
|
|
131
|
+
else:
|
|
132
|
+
result[key] = value
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def serialize_object_to_npz(obj: DataObject, *, compress: bool = True) -> bytes:
|
|
137
|
+
"""Serialize a SignalObj or ImageObj to NPZ format.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
obj: The object to serialize.
|
|
141
|
+
compress: If True (default), use ZIP deflate compression.
|
|
142
|
+
Set to False for faster serialization at the cost of larger size.
|
|
143
|
+
For incompressible data (random images), False can be 10-30x faster.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Bytes containing the NPZ archive.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
TypeError: If object type is not supported.
|
|
150
|
+
"""
|
|
151
|
+
buffer = io.BytesIO()
|
|
152
|
+
|
|
153
|
+
# Detect object type
|
|
154
|
+
obj_type = type(obj).__name__
|
|
155
|
+
|
|
156
|
+
if obj_type == "SignalObj":
|
|
157
|
+
_serialize_signal(obj, buffer, compress=compress)
|
|
158
|
+
elif obj_type == "ImageObj":
|
|
159
|
+
_serialize_image(obj, buffer, compress=compress)
|
|
160
|
+
else:
|
|
161
|
+
raise TypeError(f"Unsupported object type: {obj_type}")
|
|
162
|
+
|
|
163
|
+
return buffer.getvalue()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _serialize_signal(obj, buffer: io.BytesIO, *, compress: bool = True) -> None:
|
|
167
|
+
"""Serialize a SignalObj to NPZ format."""
|
|
168
|
+
# Build metadata dict
|
|
169
|
+
metadata = {
|
|
170
|
+
"type": "signal",
|
|
171
|
+
"title": getattr(obj, "title", None),
|
|
172
|
+
"xlabel": getattr(obj, "xlabel", None),
|
|
173
|
+
"ylabel": getattr(obj, "ylabel", None),
|
|
174
|
+
"xunit": getattr(obj, "xunit", None),
|
|
175
|
+
"yunit": getattr(obj, "yunit", None),
|
|
176
|
+
}
|
|
177
|
+
# Include object metadata (contains Geometry_*, Table_* results)
|
|
178
|
+
obj_metadata = getattr(obj, "metadata", None)
|
|
179
|
+
if obj_metadata:
|
|
180
|
+
metadata["obj_metadata"] = _serialize_obj_metadata(obj_metadata)
|
|
181
|
+
|
|
182
|
+
# Create zip archive
|
|
183
|
+
compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED
|
|
184
|
+
with zipfile.ZipFile(buffer, "w", compression) as zf:
|
|
185
|
+
# Write arrays
|
|
186
|
+
_write_array_to_zip(zf, "x.npy", obj.x)
|
|
187
|
+
_write_array_to_zip(zf, "y.npy", obj.y)
|
|
188
|
+
|
|
189
|
+
if obj.dx is not None:
|
|
190
|
+
_write_array_to_zip(zf, "dx.npy", obj.dx)
|
|
191
|
+
if obj.dy is not None:
|
|
192
|
+
_write_array_to_zip(zf, "dy.npy", obj.dy)
|
|
193
|
+
|
|
194
|
+
# Write metadata
|
|
195
|
+
zf.writestr("metadata.json", json.dumps(metadata, ensure_ascii=False))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _serialize_image(obj, buffer: io.BytesIO, *, compress: bool = True) -> None:
|
|
199
|
+
"""Serialize an ImageObj to NPZ format."""
|
|
200
|
+
# Build metadata dict - use _to_python_scalar for numeric values that may be
|
|
201
|
+
# numpy scalars after HDF5 deserialization
|
|
202
|
+
metadata = {
|
|
203
|
+
"type": "image",
|
|
204
|
+
"title": getattr(obj, "title", None),
|
|
205
|
+
"xlabel": getattr(obj, "xlabel", None),
|
|
206
|
+
"ylabel": getattr(obj, "ylabel", None),
|
|
207
|
+
"zlabel": getattr(obj, "zlabel", None),
|
|
208
|
+
"xunit": getattr(obj, "xunit", None),
|
|
209
|
+
"yunit": getattr(obj, "yunit", None),
|
|
210
|
+
"zunit": getattr(obj, "zunit", None),
|
|
211
|
+
"x0": _to_python_scalar(getattr(obj, "x0", 0.0)),
|
|
212
|
+
"y0": _to_python_scalar(getattr(obj, "y0", 0.0)),
|
|
213
|
+
"dx": _to_python_scalar(getattr(obj, "dx", 1.0)),
|
|
214
|
+
"dy": _to_python_scalar(getattr(obj, "dy", 1.0)),
|
|
215
|
+
}
|
|
216
|
+
# Include object metadata (contains Geometry_*, Table_* results)
|
|
217
|
+
obj_metadata = getattr(obj, "metadata", None)
|
|
218
|
+
if obj_metadata:
|
|
219
|
+
metadata["obj_metadata"] = _serialize_obj_metadata(obj_metadata)
|
|
220
|
+
|
|
221
|
+
# Create zip archive
|
|
222
|
+
compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED
|
|
223
|
+
with zipfile.ZipFile(buffer, "w", compression) as zf:
|
|
224
|
+
_write_array_to_zip(zf, "data.npy", obj.data)
|
|
225
|
+
zf.writestr("metadata.json", json.dumps(metadata, ensure_ascii=False))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _write_array_to_zip(zf: zipfile.ZipFile, name: str, arr: np.ndarray) -> None:
|
|
229
|
+
"""Write a numpy array to a zip file."""
|
|
230
|
+
arr_buffer = io.BytesIO()
|
|
231
|
+
np.save(arr_buffer, arr, allow_pickle=False)
|
|
232
|
+
zf.writestr(name, arr_buffer.getvalue())
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def deserialize_object_from_npz(data: bytes) -> DataObject:
|
|
236
|
+
"""Deserialize a SignalObj or ImageObj from NPZ format.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
data: Bytes containing the NPZ archive.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
SignalObj or ImageObj.
|
|
243
|
+
|
|
244
|
+
Raises:
|
|
245
|
+
ValueError: If the archive format is invalid.
|
|
246
|
+
"""
|
|
247
|
+
buffer = io.BytesIO(data)
|
|
248
|
+
|
|
249
|
+
with zipfile.ZipFile(buffer, "r") as zf:
|
|
250
|
+
# Read metadata
|
|
251
|
+
if "metadata.json" not in zf.namelist():
|
|
252
|
+
raise ValueError("Invalid NPZ format: missing metadata.json")
|
|
253
|
+
|
|
254
|
+
metadata = json.loads(zf.read("metadata.json"))
|
|
255
|
+
obj_type = metadata.get("type")
|
|
256
|
+
|
|
257
|
+
if obj_type == "signal":
|
|
258
|
+
return _deserialize_signal(zf, metadata)
|
|
259
|
+
if obj_type == "image":
|
|
260
|
+
return _deserialize_image(zf, metadata)
|
|
261
|
+
|
|
262
|
+
raise ValueError(f"Unknown object type in NPZ: {obj_type}")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _read_array_from_zip(zf: zipfile.ZipFile, name: str) -> np.ndarray | None:
|
|
266
|
+
"""Read a numpy array from a zip file."""
|
|
267
|
+
if name not in zf.namelist():
|
|
268
|
+
return None
|
|
269
|
+
arr_buffer = io.BytesIO(zf.read(name))
|
|
270
|
+
return np.load(arr_buffer, allow_pickle=False)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _deserialize_signal(zf: zipfile.ZipFile, metadata: dict) -> DataObject:
|
|
274
|
+
"""Deserialize a SignalObj from NPZ archive."""
|
|
275
|
+
x = _read_array_from_zip(zf, "x.npy")
|
|
276
|
+
y = _read_array_from_zip(zf, "y.npy")
|
|
277
|
+
dx = _read_array_from_zip(zf, "dx.npy")
|
|
278
|
+
dy = _read_array_from_zip(zf, "dy.npy")
|
|
279
|
+
|
|
280
|
+
if x is None or y is None:
|
|
281
|
+
raise ValueError("Invalid signal NPZ: missing x.npy or y.npy")
|
|
282
|
+
|
|
283
|
+
obj = SignalObj()
|
|
284
|
+
obj.set_xydata(x, y, dx=dx, dy=dy)
|
|
285
|
+
|
|
286
|
+
# Set metadata
|
|
287
|
+
if metadata.get("title"):
|
|
288
|
+
obj.title = metadata["title"]
|
|
289
|
+
if metadata.get("xlabel"):
|
|
290
|
+
obj.xlabel = metadata["xlabel"]
|
|
291
|
+
if metadata.get("ylabel"):
|
|
292
|
+
obj.ylabel = metadata["ylabel"]
|
|
293
|
+
if metadata.get("xunit"):
|
|
294
|
+
obj.xunit = metadata["xunit"]
|
|
295
|
+
if metadata.get("yunit"):
|
|
296
|
+
obj.yunit = metadata["yunit"]
|
|
297
|
+
|
|
298
|
+
# Restore object metadata (Geometry_*, Table_* results)
|
|
299
|
+
if metadata.get("obj_metadata"):
|
|
300
|
+
obj.metadata = _deserialize_obj_metadata(metadata["obj_metadata"])
|
|
301
|
+
|
|
302
|
+
return obj
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _deserialize_image(zf: zipfile.ZipFile, metadata: dict) -> DataObject:
|
|
306
|
+
"""Deserialize an ImageObj from NPZ archive."""
|
|
307
|
+
data = _read_array_from_zip(zf, "data.npy")
|
|
308
|
+
if data is None:
|
|
309
|
+
raise ValueError("Invalid image NPZ: missing data.npy")
|
|
310
|
+
|
|
311
|
+
obj = ImageObj()
|
|
312
|
+
obj.data = data
|
|
313
|
+
|
|
314
|
+
# Set metadata
|
|
315
|
+
if metadata.get("title"):
|
|
316
|
+
obj.title = metadata["title"]
|
|
317
|
+
if metadata.get("xlabel"):
|
|
318
|
+
obj.xlabel = metadata["xlabel"]
|
|
319
|
+
if metadata.get("ylabel"):
|
|
320
|
+
obj.ylabel = metadata["ylabel"]
|
|
321
|
+
if metadata.get("zlabel"):
|
|
322
|
+
obj.zlabel = metadata["zlabel"]
|
|
323
|
+
if metadata.get("xunit"):
|
|
324
|
+
obj.xunit = metadata["xunit"]
|
|
325
|
+
if metadata.get("yunit"):
|
|
326
|
+
obj.yunit = metadata["yunit"]
|
|
327
|
+
if metadata.get("zunit"):
|
|
328
|
+
obj.zunit = metadata["zunit"]
|
|
329
|
+
|
|
330
|
+
# Set coordinate info
|
|
331
|
+
obj.x0 = metadata.get("x0", 0.0)
|
|
332
|
+
obj.y0 = metadata.get("y0", 0.0)
|
|
333
|
+
obj.dx = metadata.get("dx", 1.0)
|
|
334
|
+
obj.dy = metadata.get("dy", 1.0)
|
|
335
|
+
|
|
336
|
+
# Restore object metadata (Geometry_*, Table_* results)
|
|
337
|
+
if metadata.get("obj_metadata"):
|
|
338
|
+
obj.metadata = _deserialize_obj_metadata(metadata["obj_metadata"])
|
|
339
|
+
|
|
340
|
+
return obj
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def object_to_metadata(obj: DataObject, name: str) -> dict:
|
|
344
|
+
"""Extract metadata from an object for API responses.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
obj: The object to extract metadata from.
|
|
348
|
+
name: The object name in the workspace.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Dictionary suitable for ObjectMetadata schema.
|
|
352
|
+
"""
|
|
353
|
+
obj_type = type(obj).__name__
|
|
354
|
+
|
|
355
|
+
if obj_type == "SignalObj":
|
|
356
|
+
return {
|
|
357
|
+
"name": name,
|
|
358
|
+
"type": "signal",
|
|
359
|
+
"shape": list(obj.y.shape),
|
|
360
|
+
"dtype": str(obj.y.dtype),
|
|
361
|
+
"title": getattr(obj, "title", None),
|
|
362
|
+
"xlabel": getattr(obj, "xlabel", None),
|
|
363
|
+
"ylabel": getattr(obj, "ylabel", None),
|
|
364
|
+
"xunit": getattr(obj, "xunit", None),
|
|
365
|
+
"yunit": getattr(obj, "yunit", None),
|
|
366
|
+
"attributes": {},
|
|
367
|
+
}
|
|
368
|
+
if obj_type == "ImageObj":
|
|
369
|
+
return {
|
|
370
|
+
"name": name,
|
|
371
|
+
"type": "image",
|
|
372
|
+
"shape": list(obj.data.shape),
|
|
373
|
+
"dtype": str(obj.data.dtype),
|
|
374
|
+
"title": getattr(obj, "title", None),
|
|
375
|
+
"xlabel": getattr(obj, "xlabel", None),
|
|
376
|
+
"ylabel": getattr(obj, "ylabel", None),
|
|
377
|
+
"zlabel": getattr(obj, "zlabel", None),
|
|
378
|
+
"xunit": getattr(obj, "xunit", None),
|
|
379
|
+
"yunit": getattr(obj, "yunit", None),
|
|
380
|
+
"zunit": getattr(obj, "zunit", None),
|
|
381
|
+
"attributes": {
|
|
382
|
+
"x0": _to_python_scalar(getattr(obj, "x0", 0.0)),
|
|
383
|
+
"y0": _to_python_scalar(getattr(obj, "y0", 0.0)),
|
|
384
|
+
"dx": _to_python_scalar(getattr(obj, "dx", 1.0)),
|
|
385
|
+
"dy": _to_python_scalar(getattr(obj, "dy", 1.0)),
|
|
386
|
+
},
|
|
387
|
+
}
|
|
388
|
+
raise TypeError(f"Unsupported object type: {obj_type}")
|
datalab/widgets/status.py
CHANGED
|
@@ -247,3 +247,64 @@ class XMLRPCStatus(BaseStatus):
|
|
|
247
247
|
else:
|
|
248
248
|
self.label.setText(text + str(self.port))
|
|
249
249
|
self.set_icon("libre-gui-link.svg")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class WebAPIStatus(BaseStatus):
|
|
253
|
+
"""Web API status widget.
|
|
254
|
+
Shows the Web API server status and port number.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
parent (QWidget): parent widget
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
SIG_SHOW_INFO = QC.Signal() # Signal to show connection info
|
|
261
|
+
SIG_START_SERVER = QC.Signal() # Signal to propose starting server
|
|
262
|
+
|
|
263
|
+
def __init__(self, parent: QW.QWidget | None = None) -> None:
|
|
264
|
+
super().__init__(None, parent)
|
|
265
|
+
self.port: int | None = None
|
|
266
|
+
self.url: str | None = None
|
|
267
|
+
self.label.setCursor(QG.QCursor(QC.Qt.PointingHandCursor))
|
|
268
|
+
self.label.mouseReleaseEvent = self.on_click
|
|
269
|
+
self.update_status() # Initialize widget state
|
|
270
|
+
|
|
271
|
+
def on_click(self, event: QG.QMouseEvent) -> None:
|
|
272
|
+
"""Handle mouse click event on label.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
event: mouse event
|
|
276
|
+
"""
|
|
277
|
+
if event.button() == QC.Qt.LeftButton:
|
|
278
|
+
if self.port is None:
|
|
279
|
+
self.SIG_START_SERVER.emit()
|
|
280
|
+
else:
|
|
281
|
+
self.SIG_SHOW_INFO.emit()
|
|
282
|
+
|
|
283
|
+
def set_status(self, url: str | None, port: int | None) -> None:
|
|
284
|
+
"""Set Web API server status.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
url: Web API server URL (e.g. "http://127.0.0.1:8080").
|
|
288
|
+
If None, Web API server is not running.
|
|
289
|
+
port: Web API server port number.
|
|
290
|
+
"""
|
|
291
|
+
self.url = url
|
|
292
|
+
self.port = port
|
|
293
|
+
self.update_status()
|
|
294
|
+
|
|
295
|
+
def update_status(self) -> None:
|
|
296
|
+
"""Update status widget"""
|
|
297
|
+
if self.port is None:
|
|
298
|
+
self.label.setText(_("Web API"))
|
|
299
|
+
self.set_icon("libre-gui-unlink.svg")
|
|
300
|
+
self.setToolTip(
|
|
301
|
+
_("Web API server is not running") + "\n" + _("Click to start")
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
self.label.setText(_("Web API:") + " " + str(self.port))
|
|
305
|
+
self.set_icon("libre-gui-link.svg")
|
|
306
|
+
tooltip = _("Web API server is running") + "\n"
|
|
307
|
+
if self.url:
|
|
308
|
+
tooltip += f"URL: {self.url}\n"
|
|
309
|
+
tooltip += _("Click to view connection info")
|
|
310
|
+
self.setToolTip(tooltip)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datalab-platform
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: DataLab is a data processing and analysis software for scientific and industrial applications
|
|
5
5
|
Author-email: Pierre Raybaut <p.raybaut@codra.fr>
|
|
6
6
|
Maintainer-email: DataLab Platform Developers <p.raybaut@codra.fr>
|
|
@@ -34,14 +34,17 @@ Description-Content-Type: text/markdown
|
|
|
34
34
|
License-File: LICENSE
|
|
35
35
|
Requires-Dist: guidata>=3.13.4
|
|
36
36
|
Requires-Dist: PlotPy>=2.8.2
|
|
37
|
-
Requires-Dist: Sigima>=1.0
|
|
38
|
-
Requires-Dist: NumPy
|
|
39
|
-
Requires-Dist: SciPy
|
|
40
|
-
Requires-Dist: scikit-image
|
|
41
|
-
Requires-Dist: pandas
|
|
42
|
-
Requires-Dist: PyWavelets
|
|
37
|
+
Requires-Dist: Sigima>=1.1.0
|
|
38
|
+
Requires-Dist: NumPy<2.5,>=1.22
|
|
39
|
+
Requires-Dist: SciPy<1.17,>=1.10.1
|
|
40
|
+
Requires-Dist: scikit-image<0.27,>=0.19.2
|
|
41
|
+
Requires-Dist: pandas<3.0,>=1.4
|
|
42
|
+
Requires-Dist: PyWavelets<2.0,>=1.2
|
|
43
43
|
Requires-Dist: psutil>=5.8
|
|
44
44
|
Requires-Dist: packaging>=21.3
|
|
45
|
+
Requires-Dist: fastapi>=0.110.0
|
|
46
|
+
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
47
|
+
Requires-Dist: pydantic>=2.0
|
|
45
48
|
Provides-Extra: qt
|
|
46
49
|
Requires-Dist: PyQt5>=5.15.6; extra == "qt"
|
|
47
50
|
Provides-Extra: opencv
|
|
@@ -68,6 +71,7 @@ Requires-Dist: pydata-sphinx-theme; extra == "doc"
|
|
|
68
71
|
Provides-Extra: test
|
|
69
72
|
Requires-Dist: pytest; extra == "test"
|
|
70
73
|
Requires-Dist: pytest-xvfb; extra == "test"
|
|
74
|
+
Requires-Dist: httpx; extra == "test"
|
|
71
75
|
Dynamic: license-file
|
|
72
76
|
|
|
73
77
|

|