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.
Files changed (68) hide show
  1. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.gitignore +4 -1
  2. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/AGENTS.md +4 -1
  3. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/CHANGELOG.md +26 -1
  4. marpledata-3.6.0.dev1/CONTRIBUTING.md +59 -0
  5. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/PKG-INFO +26 -18
  6. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/README.md +24 -16
  7. marpledata-3.6.0.dev1/RELEASING.md +13 -0
  8. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.Signal.rst +1 -1
  9. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.SignalUpload.rst +0 -1
  10. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/getting-started.rst +10 -2
  11. marpledata-3.6.0.dev1/docs/tutorials.rst +128 -0
  12. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/example.py +3 -1
  13. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/pyproject.toml +6 -1
  14. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/__init__.py +1 -1
  15. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/__init__.py +33 -10
  16. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/dataset.py +72 -22
  17. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/datastream.py +21 -8
  18. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/signal.py +17 -5
  19. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/signal_upload.py +33 -4
  20. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/utils.py +10 -14
  21. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/tests/conftest.py +6 -10
  22. marpledata-3.6.0.dev1/tests/support.py +60 -0
  23. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/tests/test_db.py +110 -86
  24. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/tests/test_insight.py +3 -1
  25. marpledata-3.6.0.dev1/tests/test_signal_upload.py +476 -0
  26. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/tests/test_sql.py +3 -0
  27. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/uv.lock +1 -1
  28. marpledata-3.4.0.dev1/.env.local +0 -3
  29. marpledata-3.4.0.dev1/.env.nightly +0 -2
  30. marpledata-3.4.0.dev1/.env.staging +0 -4
  31. marpledata-3.4.0.dev1/CONTRIBUTING.md +0 -88
  32. marpledata-3.4.0.dev1/augment-dataset.py +0 -681
  33. marpledata-3.4.0.dev1/docs/tutorials.rst +0 -106
  34. marpledata-3.4.0.dev1/ingest-ibiza.py +0 -538
  35. marpledata-3.4.0.dev1/tests/support.py +0 -24
  36. marpledata-3.4.0.dev1/tests/test_signal_upload.py +0 -332
  37. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.flake8 +0 -0
  38. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.python-version +0 -0
  39. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.uv-cache/.gitignore +0 -0
  40. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.uv-cache/.lock +0 -0
  41. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.uv-cache/CACHEDIR.TAG +0 -0
  42. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.uv-cache/interpreter-v4/c2c2931c8a99aae1/588e1e7c128e2872.msgpack +0 -0
  43. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/.uv-cache/sdists-v9/.gitignore +0 -0
  44. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/LICENSE +0 -0
  45. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/DEPLOYMENT.md +0 -0
  46. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/_static/custom.css +0 -0
  47. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/_static/favicon.png +0 -0
  48. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/_static/logo.png +0 -0
  49. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/db.rst +0 -0
  50. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/insight.rst +0 -0
  51. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.Insight.rst +0 -0
  52. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.DB.rst +0 -0
  53. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.DataStream.rst +0 -0
  54. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.Dataset.rst +0 -0
  55. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.DatasetList.rst +0 -0
  56. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.LAKE_ARROW_SCHEMA.rst +0 -0
  57. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.SCHEMA.rst +0 -0
  58. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api/marple.db.SignalsAlreadyExistError.rst +0 -0
  59. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/api.rst +0 -0
  60. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/conf.py +0 -0
  61. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/docs/index.rst +0 -0
  62. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/examples_race.csv +0 -0
  63. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/pytest.xml +0 -0
  64. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/constants.py +0 -0
  65. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/db/sql.py +0 -0
  66. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/insight.py +0 -0
  67. {marpledata-3.4.0.dev1 → marpledata-3.6.0.dev1}/src/marple/py.typed +0 -0
  68. {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
- *.env
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
- or fail clearly when required credentials are missing.
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
- ## [3.4.0] - 2026-07-16
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.4
1
+ Metadata-Version: 2.5
2
2
  Name: marpledata
3
- Version: 3.4.0.dev1
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
- Each signal is a DataFrame, Arrow table, or on-disk parquet matching `LAKE_ARROW_SCHEMA`:
94
- columns `time` (int64 nanoseconds) plus `value` and/or `value_text`.
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
- ```python
99
- import pandas as pd
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
- speed = dataset.get_signal("car.speed").get_data()
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
- derived,
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": derived, "metadata": {"unit": "km/h"}},
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 to a file-stream**: `stream.push_file(file_path, metadata={...}, concurrency=4)`
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/realtime streams (creating and appending data):
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 a realtime dataset**: `dataset.cool()` then `dataset.wait_for_import()` to wait until `FINISHED`
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
- Each signal is a DataFrame, Arrow table, or on-disk parquet matching `LAKE_ARROW_SCHEMA`:
59
- columns `time` (int64 nanoseconds) plus `value` and/or `value_text`.
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
- ```python
64
- import pandas as pd
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
- speed = dataset.get_signal("car.speed").get_data()
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
- derived,
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": derived, "metadata": {"unit": "km/h"}},
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 to a file-stream**: `stream.push_file(file_path, metadata={...}, concurrency=4)`
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/realtime streams (creating and appending data):
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 a realtime dataset**: `dataset.cool()` then `dataset.wait_for_import()` to wait until `FINISHED`
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.
@@ -16,7 +16,7 @@
16
16
  ~Signal.cache_parquet
17
17
  ~Signal.get_data
18
18
  ~Signal.list_parquet_files
19
- ~Signal.wait_until_cold
19
+ ~Signal.wait_until_available
20
20
 
21
21
 
22
22
 
@@ -26,7 +26,6 @@
26
26
  ~SignalUpload.name
27
27
  ~SignalUpload.data
28
28
  ~SignalUpload.metadata
29
- ~SignalUpload.overwrite
30
29
  ~SignalUpload.priority
31
30
 
32
31
 
@@ -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([...])``. See
41
- :doc:`tutorials` for a full example.
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("examples_race.csv", metadata={"car_id": 2, "track": "track_1", "weather": "cloudy"})
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.4.0.dev1"
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"]
@@ -3,4 +3,4 @@ from .db import DB
3
3
  from .insight import Insight
4
4
 
5
5
  __all__ = ["DB", "Insight", "db"]
6
- __version__ = "3.4.0.dev1"
6
+ __version__ = "3.6.0.dev1"
@@ -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
- # Realtime functions #
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 live stream.
459
- Returns the ID of the newly created dataset.
460
-
461
- Use `dataset_append` to add data to the dataset and `upsert_signals` to define signals.
462
- Use `cool` to finalize the dataset when done.
463
-
464
- To add datasets from a file to a file stream, use `push_file` instead.
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