marpledata 3.4.0.dev1__tar.gz → 3.6.0.dev1__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.
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.gitignore +4 -1
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/AGENTS.md +4 -1
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/CHANGELOG.md +26 -1
- marpledata-3.6.0.dev1/CONTRIBUTING.md +59 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/PKG-INFO +26 -18
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/README.md +24 -16
- marpledata-3.6.0.dev1/RELEASING.md +13 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.Signal.rst +1 -1
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.SignalUpload.rst +0 -1
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/getting-started.rst +10 -2
- marpledata-3.6.0.dev1/docs/tutorials.rst +128 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/example.py +3 -1
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/pyproject.toml +6 -1
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/__init__.py +1 -1
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/__init__.py +33 -10
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/dataset.py +72 -22
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/datastream.py +21 -8
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/signal.py +17 -5
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/signal_upload.py +33 -4
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/utils.py +10 -14
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/tests/conftest.py +6 -10
- marpledata-3.6.0.dev1/tests/support.py +60 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/tests/test_db.py +110 -86
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/tests/test_insight.py +3 -1
- marpledata-3.6.0.dev1/tests/test_signal_upload.py +476 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/tests/test_sql.py +3 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/uv.lock +1 -1
- marpledata-3.4.0.dev1/.env.local +0 -3
- marpledata-3.4.0.dev1/.env.nightly +0 -2
- marpledata-3.4.0.dev1/.env.staging +0 -4
- marpledata-3.4.0.dev1/CONTRIBUTING.md +0 -88
- marpledata-3.4.0.dev1/augment-dataset.py +0 -681
- marpledata-3.4.0.dev1/docs/tutorials.rst +0 -106
- marpledata-3.4.0.dev1/ingest-ibiza.py +0 -538
- marpledata-3.4.0.dev1/tests/support.py +0 -24
- marpledata-3.4.0.dev1/tests/test_signal_upload.py +0 -332
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.flake8 +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.python-version +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.uv-cache/.gitignore +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.uv-cache/.lock +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.uv-cache/CACHEDIR.TAG +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.uv-cache/interpreter-v4/c2c2931c8a99aae1/588e1e7c128e2872.msgpack +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.uv-cache/sdists-v9/.gitignore +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/LICENSE +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/DEPLOYMENT.md +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/_static/custom.css +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/_static/favicon.png +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/_static/logo.png +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/db.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/insight.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.Insight.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.DB.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.DataStream.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.Dataset.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.DatasetList.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.LAKE_ARROW_SCHEMA.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.SCHEMA.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.SignalsAlreadyExistError.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/conf.py +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/index.rst +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/examples_race.csv +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/pytest.xml +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/constants.py +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/sql.py +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/insight.py +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/py.typed +0 -0
- {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/tests/test_reliability.py +0 -0
|
@@ -169,4 +169,7 @@ cython_debug/
|
|
|
169
169
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
170
170
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
171
171
|
.idea/
|
|
172
|
-
|
|
172
|
+
.env*
|
|
173
|
+
|
|
174
|
+
# Client handoff pack (may contain a real API token)
|
|
175
|
+
/marple-matlab/
|
|
@@ -15,6 +15,7 @@ This directory contains the Python SDK package published as `marpledata`.
|
|
|
15
15
|
## Commands
|
|
16
16
|
|
|
17
17
|
- Run tests with output: `uv run pytest -vs`
|
|
18
|
+
- Unit tests only: `uv run pytest -m "not integration"`
|
|
18
19
|
- Build docs: `uv run --group docs sphinx-build -b html docs docs/_build/html`
|
|
19
20
|
- Build package: `uv build`
|
|
20
21
|
- Fix formatting: `uv run isort src tests && uv run black src tests`
|
|
@@ -29,4 +30,6 @@ Run commands from `python/` unless a command explicitly says otherwise.
|
|
|
29
30
|
- Keep Python SDK tests in `python/tests/`.
|
|
30
31
|
- Integration tests run against live Marple services and may require
|
|
31
32
|
`MDB_TOKEN`, `MDB_URL`, `INSIGHT_TOKEN`, and `INSIGHT_URL`. Tests should skip
|
|
32
|
-
|
|
33
|
+
clearly when required credentials are missing. Mark live tests with
|
|
34
|
+
`pytest.mark.integration`.
|
|
35
|
+
- Release steps: `RELEASING.md`.
|
|
@@ -5,7 +5,32 @@ All notable changes to the Python SDK package `marpledata` will be documented in
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Exact signal names in `get_data` / `get_signal` / `get_signals` resolve via the dataset cache or `GET /datapool/{pool}/signal/{name}/id` instead of downloading the full datapool `signal_map`. Regex patterns still use the map.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `DB.delete_signals`, `Dataset.delete_signal` / `Dataset.delete_signals`, and `Signal.delete` to remove signals from a dataset.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Allow `add_signal` in post-processing
|
|
21
|
+
|
|
22
|
+
## [3.5.0] - 2026-08-20
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `Dataset.add_signal` and `Dataset.add_signals` now accept a time-indexed pandas Series or DataFrame (DatetimeIndex / TimedeltaIndex when there is no `time` column).
|
|
27
|
+
- `DataStream.push_file(..., overwrite=False)` to replace an existing dataset with the same name (also on the deprecated `DB.push_file`).
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- An empty local parquet cache folder is treated as a cache miss and re-downloaded, instead of being assumed to mean the signal has no data.
|
|
32
|
+
|
|
33
|
+
## [3.4.0] - 2026-07-28
|
|
9
34
|
|
|
10
35
|
### Added
|
|
11
36
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
## Development (Python)
|
|
2
|
+
|
|
3
|
+
### Formatting, linting, and typing
|
|
4
|
+
|
|
5
|
+
Checks run on `src/` and `tests/` in GitLab CI (`python:lint`, `python:typing`).
|
|
6
|
+
|
|
7
|
+
Fix formatting locally:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
uv run isort src tests
|
|
11
|
+
uv run black src tests
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Verify (mirror CI):
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv run isort src tests --check --diff
|
|
18
|
+
uv run flake8 --config .flake8 src tests
|
|
19
|
+
uv run black --check src tests
|
|
20
|
+
uv run mypy --install-types --non-interactive
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Testing
|
|
24
|
+
|
|
25
|
+
The testing suite includes local unit tests and integration tests against a live Marple DB / Insight deployment. Integration tests share one imported example dataset per session, use a small CSV for extra ingestions, and mark themselves with `pytest.mark.integration`.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv run pytest -m "not integration" # local unit tests only
|
|
29
|
+
export MDB_TOKEN=...
|
|
30
|
+
export INSIGHT_TOKEN=...
|
|
31
|
+
# Optional, defaults to SaaS URLs:
|
|
32
|
+
export MDB_URL=https://db.marpledata.com/api/v1
|
|
33
|
+
export INSIGHT_URL=https://insight.marpledata.com/api/v1
|
|
34
|
+
uv run pytest -vs
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Documentation
|
|
38
|
+
|
|
39
|
+
Build the Sphinx docs locally:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv sync --group docs
|
|
43
|
+
uv run sphinx-build -b html docs docs/_build/html
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Open `docs/_build/html/index.html` in your browser to view the site.
|
|
47
|
+
|
|
48
|
+
### Local build
|
|
49
|
+
|
|
50
|
+
- `uv build`
|
|
51
|
+
- `uv run pip install dist/*.whl` (Install in your local .venv)
|
|
52
|
+
- `uv run python`
|
|
53
|
+
- `import marple`
|
|
54
|
+
- `marple.__version__`
|
|
55
|
+
- `from marple import Insight, DB`
|
|
56
|
+
|
|
57
|
+
### Publishing
|
|
58
|
+
|
|
59
|
+
See [`RELEASING.md`](RELEASING.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: marpledata
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.0.dev1
|
|
4
4
|
Summary: Marple SDK for Python
|
|
5
5
|
Project-URL: Homepage, https://www.marpledata.com/
|
|
6
6
|
Project-URL: Documentation, https://marpledata.gitlab.io/marple-sdk/
|
|
@@ -52,7 +52,7 @@ from marple import DB # Marple DB
|
|
|
52
52
|
from marple import Insight # Marple Insight
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
For release notes, see [`CHANGELOG.md`](CHANGELOG.md).
|
|
55
|
+
For release notes, see [`CHANGELOG.md`](CHANGELOG.md). To publish a release, see [`RELEASING.md`](RELEASING.md).
|
|
56
56
|
|
|
57
57
|
## Marple DB
|
|
58
58
|
|
|
@@ -90,34 +90,40 @@ dataset = dataset.wait_for_import(timeout=10)
|
|
|
90
90
|
#### Add signals to an imported dataset
|
|
91
91
|
|
|
92
92
|
Upload additional signals (for example derived channels) on an existing dataset.
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
DataFrames, Arrow tables, and on-disk parquet use `LAKE_ARROW_SCHEMA`: columns
|
|
94
|
+
`time` (int64 nanoseconds) plus `value` and/or `value_text`.
|
|
95
95
|
Times must overlap the dataset time range. Use `overwrite=True` to replace an existing signal name;
|
|
96
96
|
a conflict without overwrite raises `SignalsAlreadyExistError`.
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
from marple.db import LAKE_ARROW_SCHEMA # time + value and/or value_text
|
|
98
|
+
A Series, or a DataFrame without a `time` column, takes its times from a `DatetimeIndex` or
|
|
99
|
+
`TimedeltaIndex`. Indexed DataFrames must still have a `value` and/or `value_text` column.
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
derived = pd.DataFrame({
|
|
104
|
-
"time": speed.index.asi8,
|
|
105
|
-
"value": speed["value"] * 3.6,
|
|
106
|
-
})
|
|
101
|
+
```python
|
|
107
102
|
# Single signal: wait until available before reading
|
|
103
|
+
speed = dataset.get_signal("car.speed").get_data()
|
|
108
104
|
signal = dataset.add_signal(
|
|
109
105
|
"car.speed_kmh",
|
|
110
|
-
|
|
106
|
+
speed * 3.6,
|
|
111
107
|
metadata={"unit": "km/h"},
|
|
112
108
|
).wait_until_available()
|
|
113
109
|
|
|
114
110
|
# Batch: returns IDs as soon as upload completes (no wait)
|
|
115
111
|
ids = dataset.add_signals([
|
|
116
|
-
{"name": "car.speed_kmh", "data":
|
|
112
|
+
{"name": "car.speed_kmh", "data": speed * 3.6, "metadata": {"unit": "km/h"}},
|
|
117
113
|
], overwrite=True, concurrency=4)
|
|
118
114
|
signals = dataset.get_signals(signal_ids=ids)
|
|
119
115
|
```
|
|
120
116
|
|
|
117
|
+
Data assembled from scratch uses the explicit schema columns instead:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import pandas as pd
|
|
121
|
+
from marple.db import LAKE_ARROW_SCHEMA # time + value and/or value_text
|
|
122
|
+
|
|
123
|
+
samples = pd.DataFrame({"time": [t0, t0 + 1_000_000_000], "value": [1.0, 2.0]})
|
|
124
|
+
dataset.add_signal("car.custom", samples)
|
|
125
|
+
```
|
|
126
|
+
|
|
121
127
|
#### Upload large files
|
|
122
128
|
|
|
123
129
|
`stream.push_file(...)` starts an ingestion and lets the Marple DB API choose the best upload mode. Depending on the deployment and file size, the SDK can upload through the API server, upload directly to Azure Blob Storage, use a single presigned URL, or split the file into multipart uploads.
|
|
@@ -203,7 +209,8 @@ if len(datasets) > 0:
|
|
|
203
209
|
|
|
204
210
|
- **List streams**: `db.get_streams()`
|
|
205
211
|
- **List datasets in a stream**: `stream.get_datasets()`
|
|
206
|
-
- **Upload a file
|
|
212
|
+
- **Upload a file (file stream, default)**: `stream.push_file(file_path, metadata={...}, concurrency=4)`
|
|
213
|
+
- **Custom lake ingest (file stream)**: `stream.add_dataset(...)` then `dataset.add_signal(...)` / `add_signals([...])`
|
|
207
214
|
- **Add signals to a dataset**: `dataset.add_signal(name, data, ...)` / `dataset.add_signals([...])` — data matches `LAKE_ARROW_SCHEMA` (`time` + `value` and/or `value_text`)
|
|
208
215
|
- **Wait until a signal is available**: `signal.wait_until_available(timeout=60)`
|
|
209
216
|
- **Fetch signals by ID**: `dataset.get_signals(signal_ids=[...], refresh=False)`
|
|
@@ -213,13 +220,14 @@ if len(datasets) > 0:
|
|
|
213
220
|
- **Get a resampled df of multiple signals**: `dataset.get_data(signals=[...], resample_rule="1s")`
|
|
214
221
|
- **Delete a stream**: `stream.delete()` or `db.delete_stream(stream_key)`
|
|
215
222
|
- **Delete a dataset**: `dataset.delete()` or `db.delete_dataset(dataset_id, dataset_path)`
|
|
223
|
+
- **Delete signals**: `signal.delete()`, `dataset.delete_signal(signal_id)` / `dataset.delete_signals(signal_ids)`, or `db.delete_signals(dataset_id, dataset_path, signal_ids)`
|
|
216
224
|
|
|
217
|
-
For live
|
|
225
|
+
For live streams:
|
|
218
226
|
|
|
219
227
|
- **Create an empty dataset**: `stream.add_dataset(dataset_name, metadata=None)`
|
|
220
228
|
- **Upsert signal definitions**: `dataset.upsert_signals(signals=[...])`
|
|
221
229
|
- **Append timeseries data**: `dataset.append(data=df, shape="long"|"wide"|None)`
|
|
222
|
-
- **Cool
|
|
230
|
+
- **Cool to cold storage**: `dataset.cool()` then `dataset.wait_for_import()` until `FINISHED`
|
|
223
231
|
|
|
224
232
|
### Calling endpoints directly
|
|
225
233
|
|
|
@@ -17,7 +17,7 @@ from marple import DB # Marple DB
|
|
|
17
17
|
from marple import Insight # Marple Insight
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
For release notes, see [`CHANGELOG.md`](CHANGELOG.md).
|
|
20
|
+
For release notes, see [`CHANGELOG.md`](CHANGELOG.md). To publish a release, see [`RELEASING.md`](RELEASING.md).
|
|
21
21
|
|
|
22
22
|
## Marple DB
|
|
23
23
|
|
|
@@ -55,34 +55,40 @@ dataset = dataset.wait_for_import(timeout=10)
|
|
|
55
55
|
#### Add signals to an imported dataset
|
|
56
56
|
|
|
57
57
|
Upload additional signals (for example derived channels) on an existing dataset.
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
DataFrames, Arrow tables, and on-disk parquet use `LAKE_ARROW_SCHEMA`: columns
|
|
59
|
+
`time` (int64 nanoseconds) plus `value` and/or `value_text`.
|
|
60
60
|
Times must overlap the dataset time range. Use `overwrite=True` to replace an existing signal name;
|
|
61
61
|
a conflict without overwrite raises `SignalsAlreadyExistError`.
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
from marple.db import LAKE_ARROW_SCHEMA # time + value and/or value_text
|
|
63
|
+
A Series, or a DataFrame without a `time` column, takes its times from a `DatetimeIndex` or
|
|
64
|
+
`TimedeltaIndex`. Indexed DataFrames must still have a `value` and/or `value_text` column.
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
derived = pd.DataFrame({
|
|
69
|
-
"time": speed.index.asi8,
|
|
70
|
-
"value": speed["value"] * 3.6,
|
|
71
|
-
})
|
|
66
|
+
```python
|
|
72
67
|
# Single signal: wait until available before reading
|
|
68
|
+
speed = dataset.get_signal("car.speed").get_data()
|
|
73
69
|
signal = dataset.add_signal(
|
|
74
70
|
"car.speed_kmh",
|
|
75
|
-
|
|
71
|
+
speed * 3.6,
|
|
76
72
|
metadata={"unit": "km/h"},
|
|
77
73
|
).wait_until_available()
|
|
78
74
|
|
|
79
75
|
# Batch: returns IDs as soon as upload completes (no wait)
|
|
80
76
|
ids = dataset.add_signals([
|
|
81
|
-
{"name": "car.speed_kmh", "data":
|
|
77
|
+
{"name": "car.speed_kmh", "data": speed * 3.6, "metadata": {"unit": "km/h"}},
|
|
82
78
|
], overwrite=True, concurrency=4)
|
|
83
79
|
signals = dataset.get_signals(signal_ids=ids)
|
|
84
80
|
```
|
|
85
81
|
|
|
82
|
+
Data assembled from scratch uses the explicit schema columns instead:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import pandas as pd
|
|
86
|
+
from marple.db import LAKE_ARROW_SCHEMA # time + value and/or value_text
|
|
87
|
+
|
|
88
|
+
samples = pd.DataFrame({"time": [t0, t0 + 1_000_000_000], "value": [1.0, 2.0]})
|
|
89
|
+
dataset.add_signal("car.custom", samples)
|
|
90
|
+
```
|
|
91
|
+
|
|
86
92
|
#### Upload large files
|
|
87
93
|
|
|
88
94
|
`stream.push_file(...)` starts an ingestion and lets the Marple DB API choose the best upload mode. Depending on the deployment and file size, the SDK can upload through the API server, upload directly to Azure Blob Storage, use a single presigned URL, or split the file into multipart uploads.
|
|
@@ -168,7 +174,8 @@ if len(datasets) > 0:
|
|
|
168
174
|
|
|
169
175
|
- **List streams**: `db.get_streams()`
|
|
170
176
|
- **List datasets in a stream**: `stream.get_datasets()`
|
|
171
|
-
- **Upload a file
|
|
177
|
+
- **Upload a file (file stream, default)**: `stream.push_file(file_path, metadata={...}, concurrency=4)`
|
|
178
|
+
- **Custom lake ingest (file stream)**: `stream.add_dataset(...)` then `dataset.add_signal(...)` / `add_signals([...])`
|
|
172
179
|
- **Add signals to a dataset**: `dataset.add_signal(name, data, ...)` / `dataset.add_signals([...])` — data matches `LAKE_ARROW_SCHEMA` (`time` + `value` and/or `value_text`)
|
|
173
180
|
- **Wait until a signal is available**: `signal.wait_until_available(timeout=60)`
|
|
174
181
|
- **Fetch signals by ID**: `dataset.get_signals(signal_ids=[...], refresh=False)`
|
|
@@ -178,13 +185,14 @@ if len(datasets) > 0:
|
|
|
178
185
|
- **Get a resampled df of multiple signals**: `dataset.get_data(signals=[...], resample_rule="1s")`
|
|
179
186
|
- **Delete a stream**: `stream.delete()` or `db.delete_stream(stream_key)`
|
|
180
187
|
- **Delete a dataset**: `dataset.delete()` or `db.delete_dataset(dataset_id, dataset_path)`
|
|
188
|
+
- **Delete signals**: `signal.delete()`, `dataset.delete_signal(signal_id)` / `dataset.delete_signals(signal_ids)`, or `db.delete_signals(dataset_id, dataset_path, signal_ids)`
|
|
181
189
|
|
|
182
|
-
For live
|
|
190
|
+
For live streams:
|
|
183
191
|
|
|
184
192
|
- **Create an empty dataset**: `stream.add_dataset(dataset_name, metadata=None)`
|
|
185
193
|
- **Upsert signal definitions**: `dataset.upsert_signals(signals=[...])`
|
|
186
194
|
- **Append timeseries data**: `dataset.append(data=df, shape="long"|"wide"|None)`
|
|
187
|
-
- **Cool
|
|
195
|
+
- **Cool to cold storage**: `dataset.cool()` then `dataset.wait_for_import()` until `FINISHED`
|
|
188
196
|
|
|
189
197
|
### Calling endpoints directly
|
|
190
198
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Releasing `marpledata`
|
|
2
|
+
|
|
3
|
+
Tokens: 1Password (`username: __token__`). Run from `python/`.
|
|
4
|
+
|
|
5
|
+
- **Bump version**
|
|
6
|
+
- `uv version --bump minor` / `uv version x.y.z`
|
|
7
|
+
- Manual update `__init__.py`, `CHANGELOG.md`
|
|
8
|
+
- **TestPyPI** — `rm -rf dist && uv build && uv publish --index testpypi`
|
|
9
|
+
Smoke: `pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple marpledata==<version>`
|
|
10
|
+
- **PyPI** — `rm -rf dist && uv build && uv publish`
|
|
11
|
+
- **Docs** — run the GitLab `pages` job (Sphinx → https://marpledata.gitlab.io/marple-sdk/)
|
|
12
|
+
|
|
13
|
+
No GitHub tag/release; `CHANGELOG.md` is the release history.
|
|
@@ -37,8 +37,9 @@ Marple DB quickstart
|
|
|
37
37
|
dataset = dataset.wait_for_import(timeout=10)
|
|
38
38
|
|
|
39
39
|
After import, you can add derived signals with
|
|
40
|
-
``dataset.add_signal(...)`` / ``dataset.add_signals([...])``.
|
|
41
|
-
|
|
40
|
+
``dataset.add_signal(...)`` / ``dataset.add_signals([...])``. For custom
|
|
41
|
+
ingest without file parsing, use ``stream.add_dataset(...)`` then
|
|
42
|
+
``add_signal``. See :doc:`tutorials` for a full example.
|
|
42
43
|
|
|
43
44
|
``stream.push_file(...)`` starts an ingestion and lets the Marple DB API choose
|
|
44
45
|
the best upload mode for the deployment and file size. For large files, use a
|
|
@@ -49,6 +50,13 @@ longer ``wait_for_import`` timeout and optionally increase upload concurrency:
|
|
|
49
50
|
dataset = stream.push_file("large_export.csv", concurrency=8)
|
|
50
51
|
dataset = dataset.wait_for_import(timeout=180)
|
|
51
52
|
|
|
53
|
+
To replace an existing dataset with the same name, pass ``overwrite=True``:
|
|
54
|
+
|
|
55
|
+
.. code-block:: python
|
|
56
|
+
|
|
57
|
+
dataset = stream.push_file("examples_race.csv", overwrite=True)
|
|
58
|
+
dataset = dataset.wait_for_import(timeout=10)
|
|
59
|
+
|
|
52
60
|
If direct storage uploads are blocked by your network or proxy, force upload
|
|
53
61
|
through the Marple DB API server:
|
|
54
62
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Tutorials
|
|
2
|
+
=========
|
|
3
|
+
|
|
4
|
+
These examples use the high-level ``DataStream``, ``Dataset``, and ``Signal``
|
|
5
|
+
APIs. Create a stream and API token in Marple DB first.
|
|
6
|
+
|
|
7
|
+
Setup
|
|
8
|
+
-----
|
|
9
|
+
|
|
10
|
+
.. code-block:: python
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
from marple import DB
|
|
18
|
+
|
|
19
|
+
db = DB(os.environ["MDB_TOKEN"])
|
|
20
|
+
stream = db.get_stream("Car data")
|
|
21
|
+
|
|
22
|
+
For VPC or self-hosted deployments, pass ``os.environ["MDB_URL"]`` as the
|
|
23
|
+
second argument to ``DB``.
|
|
24
|
+
|
|
25
|
+
Import a file and wait for import
|
|
26
|
+
---------------------------------
|
|
27
|
+
|
|
28
|
+
.. code-block:: python
|
|
29
|
+
|
|
30
|
+
dataset = stream.push_file(
|
|
31
|
+
"examples_race.csv",
|
|
32
|
+
metadata={"source": "testbench"},
|
|
33
|
+
concurrency=8,
|
|
34
|
+
overwrite=False,
|
|
35
|
+
).wait_for_import(timeout=180)
|
|
36
|
+
|
|
37
|
+
Pass ``overwrite=True`` to replace an existing dataset with the same name.
|
|
38
|
+
|
|
39
|
+
Add signals to a dataset
|
|
40
|
+
------------------------
|
|
41
|
+
|
|
42
|
+
Add signals after import, or start with an empty dataset using
|
|
43
|
+
``stream.add_dataset``. Input follows :data:`marple.db.LAKE_ARROW_SCHEMA`:
|
|
44
|
+
``time`` (int64 nanoseconds) plus ``value`` and/or ``value_text``.
|
|
45
|
+
|
|
46
|
+
A Series, or a DataFrame without a ``time`` column, takes its times from a
|
|
47
|
+
``DatetimeIndex`` or ``TimedeltaIndex``.
|
|
48
|
+
|
|
49
|
+
.. code-block:: python
|
|
50
|
+
|
|
51
|
+
speed = dataset.get_signal("car.speed").get_data()
|
|
52
|
+
signal = dataset.add_signal(
|
|
53
|
+
"car.speed_kmh",
|
|
54
|
+
speed * 3.6,
|
|
55
|
+
metadata={"unit": "km/h"},
|
|
56
|
+
).wait_until_available()
|
|
57
|
+
|
|
58
|
+
Data assembled from scratch uses the explicit schema columns instead:
|
|
59
|
+
|
|
60
|
+
.. code-block:: python
|
|
61
|
+
|
|
62
|
+
derived = pd.DataFrame({
|
|
63
|
+
"time": [t0, t0 + 1_000_000_000],
|
|
64
|
+
"value": [1.0, 2.0],
|
|
65
|
+
})
|
|
66
|
+
dataset.add_signal("car.custom", derived)
|
|
67
|
+
|
|
68
|
+
For batches, ``add_signals`` returns signal IDs without waiting:
|
|
69
|
+
|
|
70
|
+
.. code-block:: python
|
|
71
|
+
|
|
72
|
+
ids = dataset.add_signals(
|
|
73
|
+
[{"name": "car.speed_kmh", "data": speed * 3.6}],
|
|
74
|
+
overwrite=True,
|
|
75
|
+
)
|
|
76
|
+
signals = [
|
|
77
|
+
signal.wait_until_available()
|
|
78
|
+
for signal in dataset.get_signals(signal_ids=ids, refresh=True)
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
Filter datasets and get resampled data
|
|
82
|
+
--------------------------------------
|
|
83
|
+
|
|
84
|
+
.. code-block:: python
|
|
85
|
+
|
|
86
|
+
datasets = (
|
|
87
|
+
stream.get_datasets()
|
|
88
|
+
.where_metadata({"car_id": [1, 2], "track": "track_1"})
|
|
89
|
+
.wait_for_import()
|
|
90
|
+
.where_imported()
|
|
91
|
+
.where_signal("car.speed", "max", greater_than=75)
|
|
92
|
+
)
|
|
93
|
+
for dataset, data in datasets.get_data(
|
|
94
|
+
signals=["car.speed", re.compile(r"car\.wheel\..*\.speed")],
|
|
95
|
+
resample_rule="0.17s",
|
|
96
|
+
):
|
|
97
|
+
print(dataset.path, data.shape)
|
|
98
|
+
|
|
99
|
+
Ingest realtime data
|
|
100
|
+
--------------------
|
|
101
|
+
|
|
102
|
+
Use ``append`` with a realtime stream, then ``cool`` the dataset to cold
|
|
103
|
+
storage when ingestion is complete.
|
|
104
|
+
|
|
105
|
+
.. code-block:: python
|
|
106
|
+
|
|
107
|
+
realtime = db.get_stream("Live car data")
|
|
108
|
+
dataset = realtime.add_dataset("race-1", metadata={"driver": "Alice"})
|
|
109
|
+
dataset.upsert_signals([{"signal": "car.speed", "unit": "m/s"}])
|
|
110
|
+
dataset.append(pd.DataFrame({
|
|
111
|
+
"time": [1_700_000_000_000_000_000, 1_700_000_001_000_000_000],
|
|
112
|
+
"car.speed": [10.0, 12.0],
|
|
113
|
+
}))
|
|
114
|
+
dataset = dataset.cool().wait_for_import(timeout=180)
|
|
115
|
+
|
|
116
|
+
Query with SQL
|
|
117
|
+
--------------
|
|
118
|
+
|
|
119
|
+
Trino querying is available on VPC and self-hosted deployments, not SaaS.
|
|
120
|
+
|
|
121
|
+
.. code-block:: python
|
|
122
|
+
|
|
123
|
+
info = db.trino_info
|
|
124
|
+
table = f"{info['cold_catalog']}.{info['datapool']}.data"
|
|
125
|
+
data = db.query(
|
|
126
|
+
f"SELECT time, signal, value FROM {table} WHERE dataset = ? LIMIT 1000",
|
|
127
|
+
params=[dataset.id],
|
|
128
|
+
)
|
|
@@ -20,7 +20,9 @@ if stream_name not in [stream.name for stream in db.get_streams()]:
|
|
|
20
20
|
|
|
21
21
|
stream = db.get_stream(stream_name)
|
|
22
22
|
stream.push_file("examples_race.csv", metadata={"car_id": 1, "track": "track_1", "weather": "sunny"})
|
|
23
|
-
stream.push_file(
|
|
23
|
+
stream.push_file(
|
|
24
|
+
"examples_race.csv", metadata={"car_id": 2, "track": "track_1", "weather": "cloudy"}, overwrite=True
|
|
25
|
+
)
|
|
24
26
|
stream.push_file("examples_race.csv", metadata={"car_id": 3, "track": "track_1", "weather": "rainy"})
|
|
25
27
|
stream.push_file("examples_race.csv", metadata={"car_id": 1, "track": "track_2", "weather": "sunny"})
|
|
26
28
|
stream.push_file("examples_race.csv", metadata={"car_id": 2, "track": "track_2", "weather": "sunny"})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "marpledata"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.6.0.dev1"
|
|
4
4
|
description = "Marple SDK for Python"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Matthias Baert", email = "support@marpledata.com" },
|
|
@@ -72,6 +72,11 @@ docs = [
|
|
|
72
72
|
"sphinx>=7.2.6",
|
|
73
73
|
]
|
|
74
74
|
|
|
75
|
+
[tool.pytest.ini_options]
|
|
76
|
+
markers = [
|
|
77
|
+
"integration: tests that talk to a live Marple DB / Insight instance",
|
|
78
|
+
]
|
|
79
|
+
|
|
75
80
|
[tool.black]
|
|
76
81
|
line-length = 113
|
|
77
82
|
target-version = ["py311"]
|
|
@@ -353,19 +353,21 @@ class DB:
|
|
|
353
353
|
file_path: str,
|
|
354
354
|
metadata: dict | None = None,
|
|
355
355
|
file_name: str | None = None,
|
|
356
|
+
overwrite: bool = False,
|
|
356
357
|
) -> int:
|
|
357
358
|
"""
|
|
358
359
|
Push a file to a datastream.
|
|
359
360
|
- `stream_key`: The name or ID of the stream to push the file to.
|
|
360
361
|
- `file_path`: The path to the file to be pushed.
|
|
361
362
|
- `metadata`: (optional) A dictionary of metadata to be associated with the file.
|
|
362
|
-
- `file_name`: (optional) The name of the file to be stored in the
|
|
363
|
+
- `file_name`: (optional) The name of the file to be stored in the stream.
|
|
364
|
+
- `overwrite`: (optional) If true, existing dataset with the same name will be overwritten.
|
|
363
365
|
|
|
364
366
|
Note:
|
|
365
367
|
This function is deprecated and it is encouraged to use the `push_file` method in the `DataStream` class directly.
|
|
366
368
|
"""
|
|
367
369
|
stream = self.get_stream(stream_key)
|
|
368
|
-
return stream.push_file(file_path, metadata, file_name).id
|
|
370
|
+
return stream.push_file(file_path, metadata, file_name, overwrite=overwrite).id
|
|
369
371
|
|
|
370
372
|
@deprecated
|
|
371
373
|
def get_status(self, stream_key: str | int, dataset_id: int) -> dict:
|
|
@@ -430,6 +432,20 @@ class DB:
|
|
|
430
432
|
r = self.post(f"/stream/{dataset.datastream_id}/dataset/{dataset.id}/delete")
|
|
431
433
|
validate_response(r, "Delete dataset failed")
|
|
432
434
|
|
|
435
|
+
def delete_signals(
|
|
436
|
+
self,
|
|
437
|
+
dataset_id: int | None,
|
|
438
|
+
dataset_path: str | None,
|
|
439
|
+
signal_ids: Sequence[int],
|
|
440
|
+
) -> None:
|
|
441
|
+
"""
|
|
442
|
+
Delete signals from a dataset by ID or path.
|
|
443
|
+
|
|
444
|
+
Warning:
|
|
445
|
+
This is a destructive operation that cannot be undone.
|
|
446
|
+
"""
|
|
447
|
+
self.get_dataset(dataset_id, dataset_path).delete_signals(signal_ids)
|
|
448
|
+
|
|
433
449
|
@deprecated
|
|
434
450
|
def update_metadata(
|
|
435
451
|
self,
|
|
@@ -451,17 +467,24 @@ class DB:
|
|
|
451
467
|
metadata = {}
|
|
452
468
|
self.get_dataset(dataset_id, dataset_path).update_metadata(metadata, overwrite)
|
|
453
469
|
|
|
454
|
-
#
|
|
470
|
+
# Dataset creation and realtime ingest #
|
|
455
471
|
|
|
456
472
|
def add_dataset(self, stream_key: str | int, dataset_name: str, metadata: dict | None = None) -> int:
|
|
457
473
|
"""
|
|
458
|
-
Create a new empty dataset in the specified
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
474
|
+
Create a new empty dataset in the specified stream.
|
|
475
|
+
|
|
476
|
+
**Live (realtime) stream**
|
|
477
|
+
- Define signals with :meth:`~marple.db.dataset.Dataset.upsert_signals`
|
|
478
|
+
- Push data with :meth:`~marple.db.dataset.Dataset.append`
|
|
479
|
+
- Call :meth:`~marple.db.dataset.Dataset.cool` when finished to move the
|
|
480
|
+
dataset to cold Parquet/Iceberg storage, then
|
|
481
|
+
:meth:`~marple.db.dataset.Dataset.wait_for_import` until it is `FINISHED`.
|
|
482
|
+
**File (non-live) stream**
|
|
483
|
+
- Default path: use :meth:`push_file` instead of `add_dataset` — upload a
|
|
484
|
+
file (e.g. CSV); Marple parses it and writes it to the data lake.
|
|
485
|
+
- Custom ingestion: create an empty dataset with `add_dataset`, then upload
|
|
486
|
+
signal data with :meth:`~marple.db.dataset.Dataset.add_signal` /
|
|
487
|
+
:meth:`~marple.db.dataset.Dataset.add_signals` straight into the lake.
|
|
465
488
|
"""
|
|
466
489
|
return self.get_stream(stream_key).add_dataset(dataset_name, metadata).id
|
|
467
490
|
|