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.
Files changed (46) hide show
  1. datalab/__init__.py +1 -1
  2. datalab/config.py +4 -0
  3. datalab/control/baseproxy.py +160 -0
  4. datalab/control/remote.py +175 -1
  5. datalab/data/doc/DataLab_en.pdf +0 -0
  6. datalab/data/doc/DataLab_fr.pdf +0 -0
  7. datalab/data/icons/control/copy_connection_info.svg +11 -0
  8. datalab/data/icons/control/start_webapi_server.svg +19 -0
  9. datalab/data/icons/control/stop_webapi_server.svg +7 -0
  10. datalab/gui/docks.py +3 -2
  11. datalab/gui/main.py +221 -2
  12. datalab/gui/settings.py +10 -0
  13. datalab/gui/tour.py +2 -3
  14. datalab/locale/fr/LC_MESSAGES/datalab.mo +0 -0
  15. datalab/locale/fr/LC_MESSAGES/datalab.po +95 -1
  16. datalab/tests/__init__.py +32 -1
  17. datalab/tests/backbone/config_unit_test.py +1 -1
  18. datalab/tests/backbone/main_app_test.py +4 -0
  19. datalab/tests/backbone/memory_leak.py +1 -1
  20. datalab/tests/features/common/createobject_unit_test.py +1 -1
  21. datalab/tests/features/common/misc_app_test.py +5 -0
  22. datalab/tests/features/control/call_method_unit_test.py +104 -0
  23. datalab/tests/features/control/embedded1_unit_test.py +8 -0
  24. datalab/tests/features/control/remoteclient_app_test.py +39 -35
  25. datalab/tests/features/control/simpleclient_unit_test.py +7 -3
  26. datalab/tests/features/hdf5/h5browser2_unit.py +1 -1
  27. datalab/tests/features/image/background_dialog_test.py +2 -2
  28. datalab/tests/features/image/imagetools_unit_test.py +1 -1
  29. datalab/tests/features/signal/baseline_dialog_test.py +1 -1
  30. datalab/tests/webapi_test.py +395 -0
  31. datalab/webapi/__init__.py +95 -0
  32. datalab/webapi/actions.py +318 -0
  33. datalab/webapi/adapter.py +642 -0
  34. datalab/webapi/controller.py +379 -0
  35. datalab/webapi/routes.py +576 -0
  36. datalab/webapi/schema.py +198 -0
  37. datalab/webapi/serialization.py +388 -0
  38. datalab/widgets/status.py +61 -0
  39. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/METADATA +11 -7
  40. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/RECORD +46 -34
  41. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/WHEEL +1 -1
  42. /datalab/data/icons/{libre-gui-link.svg → control/libre-gui-link.svg} +0 -0
  43. /datalab/data/icons/{libre-gui-unlink.svg → control/libre-gui-unlink.svg} +0 -0
  44. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/entry_points.txt +0 -0
  45. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/licenses/LICENSE +0 -0
  46. {datalab_platform-1.0.3.dist-info → datalab_platform-1.1.0.dist-info}/top_level.txt +0 -0
@@ -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
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.4
38
- Requires-Dist: NumPy>=1.22
39
- Requires-Dist: SciPy>=1.10.1
40
- Requires-Dist: scikit-image>=0.19.2
41
- Requires-Dist: pandas>=1.4
42
- Requires-Dist: PyWavelets>=1.2
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
  ![DataLab](https://raw.githubusercontent.com/DataLab-Platform/DataLab/main/doc/images/DataLab-banner.png)