scrylab 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ __pycache__/
2
+ .pytest_cache/
3
+ dist/
scrylab-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ScryLab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
scrylab-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.4
2
+ Name: scrylab
3
+ Version: 0.1.0
4
+ Summary: Python client for the ScryLab GUI – send signals into a live ScryLab session from scripts, notebooks or simulation pipelines
5
+ Project-URL: Repository, https://github.com/scrylab/scrylab-python
6
+ Project-URL: Issues, https://github.com/scrylab/scrylab-python/issues
7
+ Author-email: ScryLab <support@scrylab.de>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: measurement,plot,scrylab,signal,time-series,visualization
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: numpy>=1.24
22
+ Requires-Dist: requests>=2.28
23
+ Provides-Extra: dev
24
+ Requires-Dist: pandas>=1.5; extra == 'dev'
25
+ Requires-Dist: pytest>=7; extra == 'dev'
26
+ Provides-Extra: pandas
27
+ Requires-Dist: pandas>=1.5; extra == 'pandas'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # scrylab-python
31
+
32
+ Python wrapper for the [ScryLab](https://scrylab.de) REST API. Lets you send signals directly into a running ScryLab GUI from any Python environment – scripts, Jupyter notebooks or simulation pipelines – as a faster, more interactive alternative to matplotlib or plotly for exploratory signal analysis.
33
+
34
+ ## What you can do
35
+
36
+ - Send 1D signals (time series, measurement channels, …) into ScryLab's data browser
37
+ - Send ~~colored lines (signal + scalar color axis)~~ (coming soon) or spectrograms (2D matrix)
38
+ - Convenience function: open signals directly in a plot from code
39
+
40
+ ## Installation
41
+
42
+ 1. Prerequisite: the ScryLab GUI (desktop app) needs to be running on the same machine. [Installation Guide](https://docs.scrylab.de/docs/getting-started/installation/)
43
+ 2. Install the wrapper: `pip install scrylab`
44
+
45
+ ## Quick start
46
+
47
+ ```python
48
+ import scrylab as scry
49
+ import numpy as np
50
+
51
+ t = np.linspace(0, 10, 1_000)
52
+ y = np.sin(2 * np.pi * 5 * t)
53
+
54
+ scry.plot(y, name="Example Signal", x=t, y_unit="V", x_unit="s") # Convenience function: send + plot in one step
55
+
56
+ # Or send without plotting – then you can drag the signal into plots manually from the data browser
57
+ scry.send(y, name="Example Signal via send", x=t, y_unit="V", x_unit="s")
58
+
59
+ # Update the "Example Signal"
60
+ y2 = np.sin(2 * np.pi * 10 * t)
61
+ scry.send(y2, name="Example Signal", x=t, y_unit="V", x_unit="s", overwrite=True) # overwrite=True replaces the existing signal with the same name
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ### `scry.send(y, …)`
67
+
68
+ Sends a signal into a data source without automatic plotting.
69
+
70
+ ```python
71
+ scry.send(y, name="Speed", source="Testrun 01", x=t, y_unit="km/h", x_unit="s")
72
+ ```
73
+
74
+ | Parameter | Description |
75
+ |-----------|-------------|
76
+ | `y` | numpy array, list, or `pandas.Series`; for a Series, `name` and `x` default to the series' name and index |
77
+ | `name` | signal name (auto-generated if omitted) |
78
+ | `source` | [data source](https://docs.scrylab.de/docs/concepts/data-sources/) to send into, default `"Sent from API"`; created automatically if it doesn't exist yet |
79
+ | `x` | numpy array, list, or `pandas.Series`/`Index`; auto-generated if omitted |
80
+ | `z` | optional color axis – 1D array/list/`pandas.Series` (one value per sample, colors the trace) or 2D array/`pandas.DataFrame` (matrix → spectrogram; columns map to x-axis, index to y-axis) |
81
+ | `y_unit`, `x_unit`, `z_unit` | axis units, e.g. `"V"`, `"s"`, `"Hz"` |
82
+ | `overwrite` | replace an existing signal with the same name; raises `ScryLabError` if `False` (default) and a signal with that name already exists |
83
+
84
+ ### `scry.send_many(y, …)`
85
+
86
+ Sends multiple signals at once. `y` can be a list of arrays or a `pandas.DataFrame` (each column becomes a signal).
87
+
88
+ ```python
89
+ scry.send_many(
90
+ y=[channel_1, channel_2, channel_3],
91
+ names=["Speed", "RPM", "Torque"],
92
+ y_unit=["km/h", "1/min", "Nm"],
93
+ source="Testrun 02",
94
+ )
95
+ ```
96
+
97
+ | Parameter | Description |
98
+ |-----------|-------------|
99
+ | `y` | list of numpy arrays/lists/`pandas.Series`, or `pandas.DataFrame` (each column becomes one signal); Series names and indices are used automatically |
100
+ | `names` | list of signal names; auto-generated if omitted |
101
+ | `source` | [data source](https://docs.scrylab.de/docs/concepts/data-sources/) to send into, default `"Sent from API"`; created automatically if it doesn't exist yet |
102
+ | `x` | a single numpy array/list broadcast to all signals, or a list of numpy arrays/lists (one per signal) |
103
+ | `z` | ~~1D array (colored trace)~~ (coming soon) or 2D array/`pandas.DataFrame` (spectrogram) – broadcast a single value to all signals, or pass a list (one per signal) |
104
+ | `y_unit`, `x_unit`, `z_unit` | a single string applied to all signals, or a list of strings (one per signal) |
105
+ | `overwrite` | replace existing signals with the same name; raises `ScryLabError` if `False` (default) and a signal with that name already exists |
106
+
107
+ ### `scry.plot(y, …)`
108
+
109
+ Same as `send()` but also opens the signal in a plot. Always uses `"Sent from API"` as the data source – use `send()` if you need a specific source. A new plot is created if none exists yet.
110
+
111
+ ```python
112
+ scry.plot(y, name="Accelerometer", y_unit="m/s²", x_unit="s")
113
+ ```
114
+
115
+ ## pandas integration
116
+
117
+ Pass a `pandas.Series` as `y` and `name` and `x` are derived automatically from the series:
118
+
119
+ ```python
120
+ import pandas as pd
121
+ import scrylab as scry
122
+
123
+ voltage = pd.Series(
124
+ data=[0.1, 0.4, 0.9, 0.7],
125
+ index=[0.0, 0.1, 0.2, 0.3],
126
+ name="Voltage",
127
+ )
128
+
129
+ scry.send(voltage, y_unit="V", x_unit="s")
130
+ # equivalent to: scry.send(voltage.to_numpy(), name="Voltage", x=voltage.index, y_unit="V", x_unit="s")
131
+ ```
132
+
133
+ You can still override `name` or `x` explicitly – they take precedence over the series metadata.
134
+
135
+ `send_many()` works the same way for a list of Series. Pass a `pandas.DataFrame` and every column becomes a signal, with the index shared as x-axis:
136
+
137
+ ```python
138
+ df = pd.DataFrame({"Speed": [...], "RPM": [...]}, index=t)
139
+ scry.send_many(df, source="Testrun 03", y_unit=["km/h", "1/min"], x_unit="s")
140
+ ```
141
+
142
+ ## Spectrograms
143
+
144
+ Pass a `pandas.DataFrame` as `z` to send a spectrogram – columns map to the x-axis (e.g. time), the index to the y-axis (e.g. frequency):
145
+
146
+ ```python
147
+ import numpy as np
148
+ import pandas as pd
149
+ from scipy.signal import spectrogram
150
+
151
+ fs = 1000 # Hz
152
+ t = np.linspace(0, 4, fs * 4, endpoint=False)
153
+
154
+ # 20 Hz for the first 2 s, then 80 Hz
155
+ y = np.where(t < 2, np.sin(2 * np.pi * 20 * t), np.sin(2 * np.pi * 80 * t))
156
+
157
+ f, t_bins, Sxx = spectrogram(y, fs=fs, nperseg=256)
158
+ Sxx_db = 10 * np.log10(Sxx + 1e-12)
159
+
160
+ spec = pd.DataFrame(Sxx_db, index=f, columns=t_bins)
161
+ scry.send(y=f, z=spec, name="Spectrogram", y_unit="Hz", x_unit="s", z_unit="dB")
162
+ ```
@@ -0,0 +1,133 @@
1
+ # scrylab-python
2
+
3
+ Python wrapper for the [ScryLab](https://scrylab.de) REST API. Lets you send signals directly into a running ScryLab GUI from any Python environment – scripts, Jupyter notebooks or simulation pipelines – as a faster, more interactive alternative to matplotlib or plotly for exploratory signal analysis.
4
+
5
+ ## What you can do
6
+
7
+ - Send 1D signals (time series, measurement channels, …) into ScryLab's data browser
8
+ - Send ~~colored lines (signal + scalar color axis)~~ (coming soon) or spectrograms (2D matrix)
9
+ - Convenience function: open signals directly in a plot from code
10
+
11
+ ## Installation
12
+
13
+ 1. Prerequisite: the ScryLab GUI (desktop app) needs to be running on the same machine. [Installation Guide](https://docs.scrylab.de/docs/getting-started/installation/)
14
+ 2. Install the wrapper: `pip install scrylab`
15
+
16
+ ## Quick start
17
+
18
+ ```python
19
+ import scrylab as scry
20
+ import numpy as np
21
+
22
+ t = np.linspace(0, 10, 1_000)
23
+ y = np.sin(2 * np.pi * 5 * t)
24
+
25
+ scry.plot(y, name="Example Signal", x=t, y_unit="V", x_unit="s") # Convenience function: send + plot in one step
26
+
27
+ # Or send without plotting – then you can drag the signal into plots manually from the data browser
28
+ scry.send(y, name="Example Signal via send", x=t, y_unit="V", x_unit="s")
29
+
30
+ # Update the "Example Signal"
31
+ y2 = np.sin(2 * np.pi * 10 * t)
32
+ scry.send(y2, name="Example Signal", x=t, y_unit="V", x_unit="s", overwrite=True) # overwrite=True replaces the existing signal with the same name
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### `scry.send(y, …)`
38
+
39
+ Sends a signal into a data source without automatic plotting.
40
+
41
+ ```python
42
+ scry.send(y, name="Speed", source="Testrun 01", x=t, y_unit="km/h", x_unit="s")
43
+ ```
44
+
45
+ | Parameter | Description |
46
+ |-----------|-------------|
47
+ | `y` | numpy array, list, or `pandas.Series`; for a Series, `name` and `x` default to the series' name and index |
48
+ | `name` | signal name (auto-generated if omitted) |
49
+ | `source` | [data source](https://docs.scrylab.de/docs/concepts/data-sources/) to send into, default `"Sent from API"`; created automatically if it doesn't exist yet |
50
+ | `x` | numpy array, list, or `pandas.Series`/`Index`; auto-generated if omitted |
51
+ | `z` | optional color axis – 1D array/list/`pandas.Series` (one value per sample, colors the trace) or 2D array/`pandas.DataFrame` (matrix → spectrogram; columns map to x-axis, index to y-axis) |
52
+ | `y_unit`, `x_unit`, `z_unit` | axis units, e.g. `"V"`, `"s"`, `"Hz"` |
53
+ | `overwrite` | replace an existing signal with the same name; raises `ScryLabError` if `False` (default) and a signal with that name already exists |
54
+
55
+ ### `scry.send_many(y, …)`
56
+
57
+ Sends multiple signals at once. `y` can be a list of arrays or a `pandas.DataFrame` (each column becomes a signal).
58
+
59
+ ```python
60
+ scry.send_many(
61
+ y=[channel_1, channel_2, channel_3],
62
+ names=["Speed", "RPM", "Torque"],
63
+ y_unit=["km/h", "1/min", "Nm"],
64
+ source="Testrun 02",
65
+ )
66
+ ```
67
+
68
+ | Parameter | Description |
69
+ |-----------|-------------|
70
+ | `y` | list of numpy arrays/lists/`pandas.Series`, or `pandas.DataFrame` (each column becomes one signal); Series names and indices are used automatically |
71
+ | `names` | list of signal names; auto-generated if omitted |
72
+ | `source` | [data source](https://docs.scrylab.de/docs/concepts/data-sources/) to send into, default `"Sent from API"`; created automatically if it doesn't exist yet |
73
+ | `x` | a single numpy array/list broadcast to all signals, or a list of numpy arrays/lists (one per signal) |
74
+ | `z` | ~~1D array (colored trace)~~ (coming soon) or 2D array/`pandas.DataFrame` (spectrogram) – broadcast a single value to all signals, or pass a list (one per signal) |
75
+ | `y_unit`, `x_unit`, `z_unit` | a single string applied to all signals, or a list of strings (one per signal) |
76
+ | `overwrite` | replace existing signals with the same name; raises `ScryLabError` if `False` (default) and a signal with that name already exists |
77
+
78
+ ### `scry.plot(y, …)`
79
+
80
+ Same as `send()` but also opens the signal in a plot. Always uses `"Sent from API"` as the data source – use `send()` if you need a specific source. A new plot is created if none exists yet.
81
+
82
+ ```python
83
+ scry.plot(y, name="Accelerometer", y_unit="m/s²", x_unit="s")
84
+ ```
85
+
86
+ ## pandas integration
87
+
88
+ Pass a `pandas.Series` as `y` and `name` and `x` are derived automatically from the series:
89
+
90
+ ```python
91
+ import pandas as pd
92
+ import scrylab as scry
93
+
94
+ voltage = pd.Series(
95
+ data=[0.1, 0.4, 0.9, 0.7],
96
+ index=[0.0, 0.1, 0.2, 0.3],
97
+ name="Voltage",
98
+ )
99
+
100
+ scry.send(voltage, y_unit="V", x_unit="s")
101
+ # equivalent to: scry.send(voltage.to_numpy(), name="Voltage", x=voltage.index, y_unit="V", x_unit="s")
102
+ ```
103
+
104
+ You can still override `name` or `x` explicitly – they take precedence over the series metadata.
105
+
106
+ `send_many()` works the same way for a list of Series. Pass a `pandas.DataFrame` and every column becomes a signal, with the index shared as x-axis:
107
+
108
+ ```python
109
+ df = pd.DataFrame({"Speed": [...], "RPM": [...]}, index=t)
110
+ scry.send_many(df, source="Testrun 03", y_unit=["km/h", "1/min"], x_unit="s")
111
+ ```
112
+
113
+ ## Spectrograms
114
+
115
+ Pass a `pandas.DataFrame` as `z` to send a spectrogram – columns map to the x-axis (e.g. time), the index to the y-axis (e.g. frequency):
116
+
117
+ ```python
118
+ import numpy as np
119
+ import pandas as pd
120
+ from scipy.signal import spectrogram
121
+
122
+ fs = 1000 # Hz
123
+ t = np.linspace(0, 4, fs * 4, endpoint=False)
124
+
125
+ # 20 Hz for the first 2 s, then 80 Hz
126
+ y = np.where(t < 2, np.sin(2 * np.pi * 20 * t), np.sin(2 * np.pi * 80 * t))
127
+
128
+ f, t_bins, Sxx = spectrogram(y, fs=fs, nperseg=256)
129
+ Sxx_db = 10 * np.log10(Sxx + 1e-12)
130
+
131
+ spec = pd.DataFrame(Sxx_db, index=f, columns=t_bins)
132
+ scry.send(y=f, z=spec, name="Spectrogram", y_unit="Hz", x_unit="s", z_unit="dB")
133
+ ```
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.26"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "scrylab"
7
+ version = "0.1.0"
8
+ description = "Python client for the ScryLab GUI – send signals into a live ScryLab session from scripts, notebooks or simulation pipelines"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [
14
+ { name = "ScryLab", email = "support@scrylab.de" },
15
+ ]
16
+ keywords = ["scrylab", "signal", "measurement", "visualization", "time-series", "plot"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Science/Research",
20
+ "Intended Audience :: Developers",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Scientific/Engineering",
27
+ ]
28
+ dependencies = [
29
+ "requests>=2.28",
30
+ "numpy>=1.24",
31
+ ]
32
+
33
+ [project.urls]
34
+ Repository = "https://github.com/scrylab/scrylab-python"
35
+ Issues = "https://github.com/scrylab/scrylab-python/issues"
36
+
37
+ [project.optional-dependencies]
38
+ pandas = ["pandas>=1.5"]
39
+ dev = ["pytest>=7", "pandas>=1.5"]
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/scrylab"]
@@ -0,0 +1,105 @@
1
+ """
2
+ scrylab – Python client for ScryLab.
3
+ """
4
+
5
+ from ._client import ScryLabError, _default_client
6
+ from ._utils import normalize_one, normalize_many
7
+ from typing import Optional, Union
8
+
9
+ ScryLabError.__module__ = "scrylab"
10
+
11
+ __all__ = ["send", "send_many", "plot", "ScryLabError"]
12
+ __version__ = "0.1.0"
13
+
14
+
15
+ def send(
16
+ y,
17
+ name: Optional[str] = None,
18
+ source: str = "Sent from API",
19
+ x=None,
20
+ z=None,
21
+ y_unit: Optional[str] = None,
22
+ x_unit: Optional[str] = None,
23
+ z_unit: Optional[str] = None,
24
+ overwrite: bool = False,
25
+ ) -> None:
26
+ """Send a single signal to ScryLab without plotting.
27
+
28
+ y accepts a numpy array, a plain list, or a pandas Series.
29
+ For a Series, x defaults to the index and name to the series name.
30
+ x accepts a numpy array, list, or pandas Series/Index.
31
+ z is optional – pass a 1D array/list/Series for a color axis or a 2D array/DataFrame for a spectrogram.
32
+ source is created automatically if it doesn't exist yet.
33
+ Raises ScryLabError on failure or if the name already exists (overwrite=False).
34
+ """
35
+ try:
36
+ yi, ni, xi, zi = normalize_one(y, name, x, z)
37
+ client = _default_client
38
+ source_id = client._resolve_source(source)
39
+ client.send_one(yi, ni, source_id, xi, zi, y_unit, x_unit, z_unit, overwrite=overwrite)
40
+ except ScryLabError as e:
41
+ raise ScryLabError(str(e)) from None
42
+ except Exception as e:
43
+ raise ScryLabError(str(e)) from None
44
+
45
+
46
+ def send_many(
47
+ y,
48
+ names=None,
49
+ source: str = "Sent from API",
50
+ x=None,
51
+ z=None,
52
+ y_unit: Union[str, list, None] = None,
53
+ x_unit: Union[str, list, None] = None,
54
+ z_unit: Union[str, list, None] = None,
55
+ overwrite: bool = False,
56
+ ) -> None:
57
+ """Send multiple signals to ScryLab without plotting.
58
+
59
+ y accepts:
60
+ - a list of arrays, lists, or pandas Series: each element is one signal
61
+ - a pandas DataFrame: each column is one signal, index as x
62
+
63
+ x and z can each be a list (one entry per signal) or a single value broadcast to all.
64
+ z accepts a 1D array (color axis) or 2D matrix (spectrogram) per signal.
65
+ y_unit, x_unit, z_unit each accept a single string (applied to all signals) or a list
66
+ (one unit per signal).
67
+ Raises ScryLabError on failure.
68
+ """
69
+ try:
70
+ ys, ns, xs, zs = normalize_many(y, names, x, z)
71
+ client = _default_client
72
+ source_id = client._resolve_source(source)
73
+ client.send(ys, ns, source_id, xs, zs, y_unit, x_unit, z_unit, overwrite=overwrite)
74
+ except ScryLabError as e:
75
+ raise ScryLabError(str(e)) from None
76
+ except Exception as e:
77
+ raise ScryLabError(str(e)) from None
78
+
79
+
80
+ def plot(
81
+ y,
82
+ name: Optional[str] = None,
83
+ x=None,
84
+ z=None,
85
+ y_unit: Optional[str] = None,
86
+ x_unit: Optional[str] = None,
87
+ z_unit: Optional[str] = None,
88
+ overwrite: bool = False,
89
+ ) -> None:
90
+ """Send a single signal to ScryLab and plot a signal-instance.
91
+
92
+ Accepts the same y, x, z types as send(). Always lands in data source "Sent from API".
93
+ A new plot is created if none exists.
94
+ """
95
+ try:
96
+ yi, ni, xi, zi = normalize_one(y, name, x, z)
97
+ client = _default_client
98
+ source_id = client._resolve_source("Sent from API")
99
+ results = client.send_one(yi, ni, source_id, xi, zi, y_unit, x_unit, z_unit, overwrite=overwrite)
100
+ for r in results:
101
+ client.plot(r["signal_id"])
102
+ except ScryLabError as e:
103
+ raise ScryLabError(str(e)) from None
104
+ except Exception as e:
105
+ raise ScryLabError(str(e)) from None
@@ -0,0 +1,127 @@
1
+ import io
2
+ import json
3
+ from typing import Optional
4
+
5
+ import requests
6
+
7
+
8
+ _MIN_APP_VERSION = "0.1.10"
9
+
10
+
11
+ def _parse_version(v: str) -> tuple:
12
+ try:
13
+ return tuple(int(x) for x in v.split("."))
14
+ except (ValueError, AttributeError):
15
+ return (0,)
16
+
17
+
18
+ class ScryLabError(Exception):
19
+ pass
20
+
21
+
22
+ class _Client:
23
+ def __init__(self, host: str = "127.0.0.1", port: int = 5678, timeout: float = 30):
24
+ self._base = f"http://{host}:{port}"
25
+ self._timeout = timeout
26
+ self._session = requests.Session()
27
+ self._version_checked = False
28
+
29
+ def _url(self, path):
30
+ return f"{self._base}{path}"
31
+
32
+ def _request(self, method, path, **kwargs):
33
+ try:
34
+ return self._check(self._session.request(method, self._url(path), timeout=self._timeout, **kwargs))
35
+ except ScryLabError:
36
+ raise
37
+ except Exception:
38
+ raise ScryLabError("ScryLab desktop app is not running or unreachable – download it at https://scrylab.de/download.") from None
39
+
40
+ def _get(self, path):
41
+ return self._request("GET", path)
42
+
43
+ def _post(self, path, body=None, *, files=None, data=None):
44
+ return self._request("POST", path, json=body, files=files, data=data)
45
+
46
+ @staticmethod
47
+ def _check(resp):
48
+ try:
49
+ data = resp.json()
50
+ except ValueError:
51
+ resp.raise_for_status()
52
+ return {}
53
+ if resp.status_code == 404:
54
+ raise ScryLabError(f"Feature not available – please update ScryLab to v{_MIN_APP_VERSION} or later")
55
+ if resp.status_code >= 400:
56
+ raise ScryLabError(f"ScryLab error ({resp.status_code}): {data.get('error', resp.text)}")
57
+ if data.get("status") == "error":
58
+ raise ScryLabError(data.get("error", "Unknown error"))
59
+ return data
60
+
61
+ def _ensure_version(self):
62
+ if self._version_checked:
63
+ return
64
+ data = self._get("/api/status")
65
+ app_ver = data.get("version", "")
66
+ if _parse_version(app_ver) < _parse_version(_MIN_APP_VERSION):
67
+ raise ScryLabError(
68
+ f"ScryLab v{app_ver} is too old – please update to v{_MIN_APP_VERSION} or later"
69
+ )
70
+ self._version_checked = True
71
+
72
+ def _resolve_source(self, name: str) -> str:
73
+ self._ensure_version()
74
+ data = self._get("/api/sources")
75
+ for s in (data.get("result") or []):
76
+ if s.get("name") == name:
77
+ return s["source_id"]
78
+ created = self._post("/api/sources", {"name": name})
79
+ return (created.get("result") or {})["source_id"]
80
+
81
+ def send(self, ys: list, names: list, source_id: str, xs: list, zs: list,
82
+ y_units, x_units, z_units,
83
+ overwrite: bool = False) -> list[dict]:
84
+ import numpy as np
85
+
86
+ n = len(ys)
87
+ def _norm(u):
88
+ if u is None or isinstance(u, str):
89
+ return [u] * n
90
+ lst = list(u)
91
+ return lst * n if len(lst) == 1 else lst
92
+
93
+ y_units, x_units, z_units = _norm(y_units), _norm(x_units), _norm(z_units)
94
+ files, metas = [], []
95
+ for yi, ni, xi, zi, yu, xu, zu in zip(ys, names, xs, zs, y_units, x_units, z_units):
96
+ buf = io.BytesIO()
97
+ arrays = {"y": np.asarray(yi)}
98
+ if xi is not None: arrays["x"] = np.asarray(xi)
99
+ if zi is not None: arrays["z"] = np.asarray(zi)
100
+ np.savez(buf, **arrays)
101
+ buf.seek(0)
102
+ files.append(("file", (f"{ni}.npz", buf.read(), "application/octet-stream")))
103
+ m = {"name": ni, "target_source_id": source_id}
104
+ if yu is not None: m["y_unit"] = yu
105
+ if xu is not None: m["x_unit"] = xu
106
+ if zu is not None: m["z_unit"] = zu
107
+ if overwrite: m["overwrite"] = True
108
+ metas.append(m)
109
+
110
+ data = self._post("/api/signals/upload_batch", files=files, data={"meta": json.dumps(metas)})
111
+ result = data.get("result") or {}
112
+ errors = result.get("errors", [])
113
+ if errors:
114
+ details = "; ".join(f"#{e['index']}: {e['error']}" for e in errors)
115
+ raise ScryLabError(f"{len(errors)} signal(s) failed: {details}")
116
+ return result.get("signals", [])
117
+
118
+ def send_one(self, y, name: str, source_id: str, x, z,
119
+ y_unit, x_unit, z_unit, overwrite: bool = False) -> list[dict]:
120
+ return self.send([y], [name], source_id, [x], [z],
121
+ y_unit, x_unit, z_unit, overwrite=overwrite)
122
+
123
+ def plot(self, signal_id: str):
124
+ self._post("/api/plot", {"signal_id": signal_id})
125
+
126
+
127
+ _default_client = _Client()
@@ -0,0 +1,62 @@
1
+ from typing import Optional
2
+ import numpy as np
3
+
4
+
5
+ def _broadcast(val, n):
6
+ return list(val) if isinstance(val, (list, tuple)) else [val] * n
7
+
8
+
9
+ def normalize_one(y, name: Optional[str], x, z):
10
+ try:
11
+ import pandas as pd
12
+ if isinstance(y, pd.Series):
13
+ name = name or y.name
14
+ x = x if x is not None else y.index.to_numpy()
15
+ y = y.to_numpy()
16
+ if isinstance(z, pd.DataFrame):
17
+ x = x if x is not None else z.columns.to_numpy()
18
+ z = z.to_numpy()
19
+ elif isinstance(z, pd.Series):
20
+ z = z.to_numpy()
21
+ except ImportError:
22
+ pass
23
+ if x is not None:
24
+ x = np.asarray(x)
25
+ if z is not None:
26
+ z = np.asarray(z)
27
+ return np.asarray(y), name or "Unnamed Signal 1", x, z
28
+
29
+
30
+ def normalize_many(y, names, x, z):
31
+ try:
32
+ import pandas as pd
33
+ except ImportError:
34
+ pd = None
35
+
36
+ if pd is not None and isinstance(y, pd.DataFrame):
37
+ cols = list(y.columns)
38
+ ys = [y[col].to_numpy() for col in cols]
39
+ names = names if names is not None else cols
40
+ x_eff = y.index.to_numpy() if x is None else x
41
+ xs = [np.asarray(xi) if xi is not None else None for xi in _broadcast(x_eff, len(ys))]
42
+ zs = [np.asarray(zi) if zi is not None else None for zi in _broadcast(z, len(ys))]
43
+ return ys, names, xs, zs
44
+
45
+ y_items = list(y)
46
+ n = len(y_items)
47
+ ys, auto_names, xs, zs = [], [], [], []
48
+ for yi, xi, zi in zip(y_items, _broadcast(x, n), _broadcast(z, n)):
49
+ if pd is not None and isinstance(yi, pd.Series):
50
+ auto_names.append(yi.name)
51
+ xs.append(np.asarray(xi) if xi is not None else yi.index.to_numpy())
52
+ ys.append(yi.to_numpy())
53
+ else:
54
+ auto_names.append(None)
55
+ xs.append(np.asarray(xi) if xi is not None else None)
56
+ ys.append(np.asarray(yi))
57
+ zs.append(np.asarray(zi) if zi is not None else None)
58
+ if names is None:
59
+ names = [an or f"Unnamed Signal {i + 1}" for i, an in enumerate(auto_names)]
60
+ else:
61
+ names = list(names)
62
+ return ys, names, xs, zs
File without changes
@@ -0,0 +1,71 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ from scrylab._utils import normalize_one, normalize_many
5
+
6
+ # --- normalize_one ---
7
+
8
+ def test_normalize_one_numpy_array():
9
+ y = np.array([1.0, 2.0, 3.0])
10
+ yi, name, xi, zi = normalize_one(y, None, None, None)
11
+ assert isinstance(yi, np.ndarray)
12
+ np.testing.assert_array_equal(yi, y)
13
+ assert name == "Unnamed Signal 1"
14
+ assert xi is None
15
+ assert zi is None
16
+
17
+
18
+ def test_normalize_one_series_name_and_index():
19
+ s = pd.Series([10.0, 20.0], index=[0.0, 0.5], name="Speed")
20
+ yi, name, xi, zi = normalize_one(s, None, None, None)
21
+ assert name == "Speed"
22
+ np.testing.assert_array_equal(xi, [0.0, 0.5])
23
+ np.testing.assert_array_equal(yi, [10.0, 20.0])
24
+
25
+
26
+ def test_normalize_one_series_x_override():
27
+ s = pd.Series([10.0, 20.0], index=[0.0, 0.5], name="Speed")
28
+ x = np.array([1.0, 2.0])
29
+ _, _, xi, _ = normalize_one(s, None, x, None)
30
+ np.testing.assert_array_equal(xi, x)
31
+
32
+
33
+ def test_normalize_one_z_dataframe():
34
+ z_df = pd.DataFrame([[1, 2], [3, 4]], index=[10.0, 20.0], columns=[0.0, 1.0])
35
+ _, _, xi, zi = normalize_one(np.array([10.0, 20.0]), None, None, z_df)
36
+ np.testing.assert_array_equal(zi, z_df.to_numpy())
37
+ np.testing.assert_array_equal(xi, z_df.columns.to_numpy())
38
+
39
+
40
+ def test_normalize_one_custom_name_overrides_series():
41
+ s = pd.Series([1.0, 2.0], name="Original")
42
+ _, name, _, _ = normalize_one(s, "Custom", None, None)
43
+ assert name == "Custom"
44
+
45
+
46
+ # --- normalize_many ---
47
+
48
+ def test_normalize_many_list_of_arrays():
49
+ a = np.array([1.0, 2.0])
50
+ b = np.array([3.0, 4.0])
51
+ ys, names, xs, zs = normalize_many([a, b], None, None, None)
52
+ assert names == ["Unnamed Signal 1", "Unnamed Signal 2"]
53
+ assert all(xi is None for xi in xs)
54
+ assert all(zi is None for zi in zs)
55
+
56
+
57
+ def test_normalize_many_dataframe_input():
58
+ df = pd.DataFrame({"Alpha": [1.0, 2.0], "Beta": [3.0, 4.0]}, index=[0.0, 1.0])
59
+ _, names, xs, _ = normalize_many(df, None, None, None)
60
+ assert names == ["Alpha", "Beta"]
61
+ np.testing.assert_array_equal(xs[0], df.index.to_numpy())
62
+ np.testing.assert_array_equal(xs[1], df.index.to_numpy())
63
+
64
+
65
+ def test_normalize_many_list_of_series():
66
+ s1 = pd.Series([1.0, 2.0], index=[0.0, 0.5], name="Temp")
67
+ s2 = pd.Series([3.0, 4.0], index=[0.0, 0.5], name="Pressure")
68
+ _, names, xs, _ = normalize_many([s1, s2], None, None, None)
69
+ assert names == ["Temp", "Pressure"]
70
+ np.testing.assert_array_equal(xs[0], s1.index.to_numpy())
71
+ np.testing.assert_array_equal(xs[1], s2.index.to_numpy())