marpledata 3.6.0.dev2__tar.gz → 3.7.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.
Files changed (67) hide show
  1. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/CHANGELOG.md +11 -3
  2. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/PKG-INFO +5 -1
  3. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/README.md +4 -0
  4. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/docs/getting-started.rst +1 -0
  5. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/docs/tutorials.rst +3 -0
  6. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/example.py +2 -0
  7. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/pyproject.toml +1 -1
  8. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/__init__.py +1 -1
  9. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/db/__init__.py +11 -3
  10. marpledata-3.7.0/src/marple/db/activity.py +28 -0
  11. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/db/dataset.py +24 -1
  12. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/db/datastream.py +7 -0
  13. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/db/script.py +8 -2
  14. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/db/signal.py +2 -0
  15. marpledata-3.7.0/tests/test_activity.py +52 -0
  16. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/tests/test_db.py +9 -5
  17. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/tests/test_signal_upload.py +15 -0
  18. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/uv.lock +1 -1
  19. marpledata-3.6.0.dev2/.uv-cache/.gitignore +0 -1
  20. marpledata-3.6.0.dev2/.uv-cache/.lock +0 -0
  21. marpledata-3.6.0.dev2/.uv-cache/CACHEDIR.TAG +0 -1
  22. marpledata-3.6.0.dev2/.uv-cache/interpreter-v4/c2c2931c8a99aae1/588e1e7c128e2872.msgpack +0 -0
  23. marpledata-3.6.0.dev2/.uv-cache/sdists-v9/.gitignore +0 -0
  24. marpledata-3.6.0.dev2/docs/api/marple.Insight.rst +0 -33
  25. marpledata-3.6.0.dev2/docs/api/marple.db.DB.rst +0 -60
  26. marpledata-3.6.0.dev2/docs/api/marple.db.DataStream.rst +0 -55
  27. marpledata-3.6.0.dev2/docs/api/marple.db.Dataset.rst +0 -67
  28. marpledata-3.6.0.dev2/docs/api/marple.db.DatasetList.rst +0 -40
  29. marpledata-3.6.0.dev2/docs/api/marple.db.LAKE_ARROW_SCHEMA.rst +0 -6
  30. marpledata-3.6.0.dev2/docs/api/marple.db.SCHEMA.rst +0 -6
  31. marpledata-3.6.0.dev2/docs/api/marple.db.SandboxJob.rst +0 -43
  32. marpledata-3.6.0.dev2/docs/api/marple.db.SandboxJobStatus.rst +0 -28
  33. marpledata-3.6.0.dev2/docs/api/marple.db.Script.rst +0 -41
  34. marpledata-3.6.0.dev2/docs/api/marple.db.ScriptVersion.rst +0 -21
  35. marpledata-3.6.0.dev2/docs/api/marple.db.Signal.rst +0 -47
  36. marpledata-3.6.0.dev2/docs/api/marple.db.SignalUpload.rst +0 -31
  37. marpledata-3.6.0.dev2/docs/api/marple.db.SignalsAlreadyExistError.rst +0 -6
  38. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/.flake8 +0 -0
  39. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/.gitignore +0 -0
  40. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/.python-version +0 -0
  41. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/AGENTS.md +0 -0
  42. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/CONTRIBUTING.md +0 -0
  43. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/LICENSE +0 -0
  44. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/RELEASING.md +0 -0
  45. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/docs/DEPLOYMENT.md +0 -0
  46. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/docs/_static/custom.css +0 -0
  47. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/docs/_static/favicon.png +0 -0
  48. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/docs/_static/logo.png +0 -0
  49. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/docs/api/db.rst +0 -0
  50. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/docs/api/insight.rst +0 -0
  51. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/docs/api.rst +0 -0
  52. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/docs/conf.py +0 -0
  53. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/docs/index.rst +0 -0
  54. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/examples_race.csv +0 -0
  55. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/pytest.xml +0 -0
  56. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/db/constants.py +0 -0
  57. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/db/signal_upload.py +0 -0
  58. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/db/sql.py +0 -0
  59. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/insight.py +0 -0
  60. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/py.typed +0 -0
  61. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/src/marple/utils.py +0 -0
  62. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/tests/conftest.py +0 -0
  63. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/tests/support.py +0 -0
  64. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/tests/test_insight.py +0 -0
  65. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/tests/test_reliability.py +0 -0
  66. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/tests/test_scripts.py +0 -0
  67. {marpledata-3.6.0.dev2 → marpledata-3.7.0}/tests/test_sql.py +0 -0
@@ -5,7 +5,14 @@ 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
- ## [Unreleased]
8
+ ## [3.7.0] - 2026-09-28
9
+
10
+ ### Changed
11
+
12
+ - Clarify docs to use add_signals() for bulk adding signals
13
+ - `Dataset.add_signal` and `Dataset.add_signals` can upload onto a dataset in `POSTPROCESSING_FAILED`
14
+
15
+ ## [3.6.0] - 2026-09-04
9
16
 
10
17
  ### Changed
11
18
 
@@ -20,8 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
20
27
  - `Dataset.run` / `DB.run_script` to execute a stored processing script against a dataset via a server-side sandbox job.
21
28
  - `DataStream.scripts`, `DataStream.update`, and `DataStream.rerun_processing` (`DB.rerun_processing`) to edit a stream and set the script pipeline
22
29
  - `Dataset.rerun_processing` and `Dataset.get_debug_messages`.
23
- - `DB.delete_signals`, `Dataset.delete_signal` / `Dataset.delete_signals`, and `Signal.delete` to remove signals from a dataset.
24
- - `Dataset.reingest` to reingest a dataset from its original uploaded file, with optional `plugin_args`.
30
+ - `DB.delete_signals`, `Dataset.delete_signal` / `Dataset.delete_signals`, and `Signal.delete` to remove signals from a dataset.
31
+ - `Dataset.reingest` to reingest a dataset from its original uploaded file, with optional `plugin_args`.
32
+ - `logger.debug` lines on SDK mutations (`add_signal`, `update_metadata`, …), off by default. Call `db.verbose()` to print them.
25
33
 
26
34
  ### Fixed
27
35
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: marpledata
3
- Version: 3.6.0.dev2
3
+ Version: 3.7.0
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/
@@ -78,6 +78,7 @@ API_TOKEN = "<your api token>"
78
78
  API_URL = "https://db.marpledata.com/api/v1" # optional if using the default SaaS
79
79
 
80
80
  db = DB(API_TOKEN, API_URL)
81
+ db.verbose() # print SDK mutation activity (push_file, add_signal, …) to stdout
81
82
 
82
83
  db.check_connection()
83
84
 
@@ -98,6 +99,8 @@ a conflict without overwrite raises `SignalsAlreadyExistError`.
98
99
  A Series, or a DataFrame without a `time` column, takes its times from a `DatetimeIndex` or
99
100
  `TimedeltaIndex`. Indexed DataFrames must still have a `value` and/or `value_text` column.
100
101
 
102
+ Warning: for performance reasons, prefer adding signals in bulk using `add_signals` over multiple usages of `add_signal`.
103
+
101
104
  ```python
102
105
  # Single signal: wait until available before reading
103
106
  speed = dataset.get_signal("car.speed").get_data()
@@ -127,6 +130,7 @@ dataset.add_signal("car.custom", samples)
127
130
  #### Processing scripts
128
131
 
129
132
  Write a `process(dataset)` function, store it, and try it on any imported dataset. This runs on the server and writes to that dataset.
133
+ Warning: for performance reasons, prefer adding signals in bulk using `add_signals` over multiple usages of `add_signal`.
130
134
 
131
135
  ```python
132
136
  source = """
@@ -43,6 +43,7 @@ API_TOKEN = "<your api token>"
43
43
  API_URL = "https://db.marpledata.com/api/v1" # optional if using the default SaaS
44
44
 
45
45
  db = DB(API_TOKEN, API_URL)
46
+ db.verbose() # print SDK mutation activity (push_file, add_signal, …) to stdout
46
47
 
47
48
  db.check_connection()
48
49
 
@@ -63,6 +64,8 @@ a conflict without overwrite raises `SignalsAlreadyExistError`.
63
64
  A Series, or a DataFrame without a `time` column, takes its times from a `DatetimeIndex` or
64
65
  `TimedeltaIndex`. Indexed DataFrames must still have a `value` and/or `value_text` column.
65
66
 
67
+ Warning: for performance reasons, prefer adding signals in bulk using `add_signals` over multiple usages of `add_signal`.
68
+
66
69
  ```python
67
70
  # Single signal: wait until available before reading
68
71
  speed = dataset.get_signal("car.speed").get_data()
@@ -92,6 +95,7 @@ dataset.add_signal("car.custom", samples)
92
95
  #### Processing scripts
93
96
 
94
97
  Write a `process(dataset)` function, store it, and try it on any imported dataset. This runs on the server and writes to that dataset.
98
+ Warning: for performance reasons, prefer adding signals in bulk using `add_signals` over multiple usages of `add_signal`.
95
99
 
96
100
  ```python
97
101
  source = """
@@ -40,6 +40,7 @@ After import, you can add derived signals with
40
40
  ``dataset.add_signal(...)`` / ``dataset.add_signals([...])``. For custom
41
41
  ingest without file parsing, use ``stream.add_dataset(...)`` then
42
42
  ``add_signal``. See :doc:`tutorials` for a full example.
43
+ Warning: for performance reasons, prefer adding signals in bulk using ``add_signals`` over multiple usages of ``add_signal``.
43
44
 
44
45
  ``stream.push_file(...)`` starts an ingestion and lets the Marple DB API choose
45
46
  the best upload mode for the deployment and file size. For large files, use a
@@ -46,6 +46,8 @@ Add signals after import, or start with an empty dataset using
46
46
  A Series, or a DataFrame without a ``time`` column, takes its times from a
47
47
  ``DatetimeIndex`` or ``TimedeltaIndex``.
48
48
 
49
+ Warning: for performance reasons, prefer adding signals in bulk using ``add_signals`` over multiple usages of ``add_signal``.
50
+
49
51
  .. code-block:: python
50
52
 
51
53
  speed = dataset.get_signal("car.speed").get_data()
@@ -83,6 +85,7 @@ Processing scripts
83
85
 
84
86
  Write a ``process(dataset)`` function, store it, and try it on any imported dataset.
85
87
  This runs on the server and writes to that dataset.
88
+ Warning: for performance reasons, prefer adding signals in bulk using ``add_signals`` over multiple usages of ``add_signal``.
86
89
 
87
90
  .. code-block:: python
88
91
 
@@ -2,6 +2,7 @@ import os
2
2
  import re
3
3
 
4
4
  import dotenv
5
+
5
6
  from src.marple.db import DB, Dataset
6
7
 
7
8
  dotenv.load_dotenv()
@@ -11,6 +12,7 @@ if api_token is None:
11
12
 
12
13
  url = os.getenv("MDB_URL")
13
14
  db = DB(api_token, url)
15
+ db.verbose() # print SDK mutation activity (push_file, add_signal, …) to stdout
14
16
  db.check_connection()
15
17
 
16
18
  stream_name = "CSV Stream"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "marpledata"
3
- version = "3.6.0.dev2"
3
+ version = "3.7.0"
4
4
  description = "Marple SDK for Python"
5
5
  authors = [
6
6
  { name = "Matthias Baert", email = "support@marpledata.com" },
@@ -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.6.0.dev2"
6
+ __version__ = "3.7.0"
@@ -1,14 +1,15 @@
1
1
  import warnings
2
2
  from functools import wraps
3
3
  from pathlib import Path
4
- from typing import TYPE_CHECKING, Any, Literal, Optional, Sequence
4
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Sequence, TextIO
5
5
 
6
6
  import pandas as pd
7
7
  from pydantic import ValidationError
8
8
  from requests import Response
9
9
  from requests.exceptions import ConnectionError
10
10
 
11
- from marple.db import sql
11
+ from marple.db import activity, sql
12
+ from marple.db.activity import logger
12
13
  from marple.db.constants import LAKE_ARROW_SCHEMA as _LAKE_ARROW_SCHEMA
13
14
  from marple.db.constants import SAAS_URL
14
15
  from marple.db.constants import SCHEMA as _SCHEMA
@@ -156,6 +157,10 @@ class DB:
156
157
  self._refresh_stream_cache(r)
157
158
  return True
158
159
 
160
+ def verbose(self, enabled: bool = True, file: TextIO | None = None) -> None:
161
+ """Turn SDK mutation activity logging on or off (stdout by default)."""
162
+ activity.verbose(enabled, file)
163
+
159
164
  # Stream functions #
160
165
 
161
166
  def create_stream(
@@ -313,7 +318,9 @@ class DB:
313
318
  "script": Script.resolve_source(script),
314
319
  },
315
320
  )
316
- return Script(client=self.client, **validate_response(r, "Create script failed"))
321
+ created = Script(client=self.client, **validate_response(r, "Create script failed"))
322
+ logger.debug(f"Created script {name}")
323
+ return created
317
324
 
318
325
  def delete_script(self, script_id: int) -> None:
319
326
  """
@@ -324,6 +331,7 @@ class DB:
324
331
  """
325
332
  r = self.delete(f"/script/{script_id}")
326
333
  validate_response(r, "Delete script failed")
334
+ logger.debug(f"Deleted script {script_id}")
327
335
 
328
336
  def _refresh_stream_cache(self, r: Response | None = None) -> None:
329
337
  if r is None:
@@ -0,0 +1,28 @@
1
+ import logging
2
+ import sys
3
+ from typing import TextIO
4
+
5
+ logger = logging.getLogger("marple.sdk")
6
+ logger.addHandler(logging.NullHandler())
7
+ logger.propagate = False
8
+
9
+ _handler: logging.Handler | None = None
10
+
11
+
12
+ def verbose(enabled: bool = True, file: TextIO | None = None) -> None:
13
+ """Turn SDK mutation activity logging on or off (stdout by default)."""
14
+ global _handler
15
+ if enabled:
16
+ logger.setLevel(logging.DEBUG)
17
+ if _handler is not None:
18
+ return
19
+ _handler = logging.StreamHandler(file or sys.stdout)
20
+ _handler.setFormatter(logging.Formatter("%(message)s"))
21
+ logger.addHandler(_handler)
22
+ return
23
+
24
+ if _handler is not None:
25
+ logger.removeHandler(_handler)
26
+ _handler.close()
27
+ _handler = None
28
+ logger.setLevel(logging.NOTSET)
@@ -15,6 +15,7 @@ import pyarrow.parquet as pq
15
15
  from pandas._typing import AggFuncType, Frequency
16
16
  from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, ValidationError
17
17
 
18
+ from marple.db.activity import logger
18
19
  from marple.db.constants import (
19
20
  COL_SIG,
20
21
  COL_TIME,
@@ -55,6 +56,14 @@ STABLE_STATUSES = [
55
56
  ]
56
57
  BUSY_STATUSES = [v for v in ImportStatus if v not in STABLE_STATUSES]
57
58
 
59
+ # A processing script may write in these statuses. POSTPROCESSING is in-flight;
60
+ # only the stable members can start a run.
61
+ SCRIPTABLE_STATUSES = [
62
+ ImportStatus.FINISHED,
63
+ ImportStatus.POSTPROCESSING,
64
+ ImportStatus.POSTPROCESSING_FAILED,
65
+ ]
66
+
58
67
  GET_SIGNALS_CHUNK_SIZE = 200
59
68
 
60
69
 
@@ -333,6 +342,8 @@ class Dataset(BaseModel):
333
342
  new_metadata = metadata if overwrite else {**self.metadata, **metadata}
334
343
  r = self._client.post(f"/stream/{self.datastream_id}/dataset/{self.id}/metadata", json=new_metadata)
335
344
  validate_response(r, "Update metadata failed")
345
+ suffix = " (overwrite=True)" if overwrite else ""
346
+ logger.debug(f"Updated dataset metadata: {metadata}{suffix}")
336
347
  return self.fetch(self._client, self.id)
337
348
 
338
349
  def upsert_signals(self, signals: list[dict]) -> None:
@@ -347,6 +358,7 @@ class Dataset(BaseModel):
347
358
  """
348
359
  r = self._client.post(f"/stream/{self.datastream_id}/dataset/{self.id}/signals", json=signals)
349
360
  validate_response(r, "Upsert signals failed")
361
+ logger.debug(f"Upserted metadata for {len(signals)} signals from dataset {self.path}")
350
362
 
351
363
  def add_signal(
352
364
  self,
@@ -369,6 +381,8 @@ class Dataset(BaseModel):
369
381
  Returns the new signal immediately after upload completes. Call
370
382
  :meth:`Signal.wait_until_available` to wait until the signal is available.
371
383
 
384
+ Warning: for performance reasons, prefer adding signals in bulk using ``add_signals`` over multiple usages of ``add_signal``.
385
+
372
386
  Args:
373
387
  name: Signal name.
374
388
  data: Signal samples (DataFrame, Series, Arrow table, or parquet path).
@@ -408,13 +422,15 @@ class Dataset(BaseModel):
408
422
  return []
409
423
  if len(signals) > MAX_SIGNALS_PER_ADD:
410
424
  raise ValueError(f"Provide at most {MAX_SIGNALS_PER_ADD} signals per call")
411
- if self.import_status not in (ImportStatus.FINISHED, ImportStatus.POSTPROCESSING):
425
+ if self.import_status not in SCRIPTABLE_STATUSES:
412
426
  raise ValueError(f"Dataset {self.id} is not in a writable state (status: {self.import_status})")
413
427
 
414
428
  signal_ids = run_signal_uploads(self, signals, overwrite=overwrite, concurrency=concurrency)
415
429
  for signal_id in signal_ids: # Invalidate new signals from cache
416
430
  if signal_id in self._signals:
417
431
  del self._signals[signal_id]
432
+ names = [item.name if isinstance(item, SignalUpload) else item["name"] for item in signals]
433
+ logger.debug(f"Added {', '.join(names)} to {self.path} (overwrite={overwrite})")
418
434
  return signal_ids
419
435
 
420
436
  def append(
@@ -461,6 +477,7 @@ class Dataset(BaseModel):
461
477
  timeout=self._client.STORAGE_TIMEOUT,
462
478
  )
463
479
  validate_response(r, "Append data failed")
480
+ logger.debug(f"Appended {len(data)} rows to realtime dataset {self.path}")
464
481
 
465
482
  def cool(self) -> "Dataset":
466
483
  """
@@ -481,6 +498,7 @@ class Dataset(BaseModel):
481
498
 
482
499
  r = self._client.post(f"/stream/{self.datastream_id}/dataset/{self.id}/cool")
483
500
  validate_response(r, "Cool dataset failed")
501
+ logger.debug(f"Started cooling dataset {self.path}")
484
502
  return self.fetch(self._client, self.id)
485
503
 
486
504
  def reingest(self, plugin_args: str | None = None) -> "Dataset":
@@ -500,6 +518,7 @@ class Dataset(BaseModel):
500
518
  kwargs = {} if plugin_args is None else {"json": {"plugin_args": plugin_args}}
501
519
  r = self._client.post(f"/stream/{self.datastream_id}/dataset/{self.id}/reingest", **kwargs)
502
520
  validate_response(r, "Reingest dataset failed")
521
+ logger.debug(f"Started reingest of dataset {self.path}")
503
522
  return self.fetch(self._client, self.id)
504
523
 
505
524
  def wait_for_import(self, timeout: float = 60, force_fetch: bool = False) -> "Dataset":
@@ -551,6 +570,7 @@ class Dataset(BaseModel):
551
570
  validate_response(r, "Delete signals failed")
552
571
  for signal_id in to_delete:
553
572
  self._signals.pop(signal_id, None)
573
+ logger.debug(f"Deleted {len(to_delete)} signals from dataset {self.path}")
554
574
 
555
575
  def delete(self) -> None:
556
576
  """
@@ -561,6 +581,7 @@ class Dataset(BaseModel):
561
581
  """
562
582
  r = self._client.post(f"/stream/{self.datastream_id}/dataset/{self.id}/delete")
563
583
  validate_response(r, "Delete dataset failed")
584
+ logger.debug(f"Deleted dataset {self.path}")
564
585
 
565
586
  def rerun_processing(self) -> "Dataset":
566
587
  """
@@ -574,6 +595,7 @@ class Dataset(BaseModel):
574
595
  json=[self.id],
575
596
  )
576
597
  validate_response(r, "Rerun processing failed")
598
+ logger.debug(f"Reran processing for dataset {self.path}")
577
599
  return self.fetch(self._client, self.id)
578
600
 
579
601
  def get_debug_messages(self) -> list[str]:
@@ -641,6 +663,7 @@ class Dataset(BaseModel):
641
663
  if job.status == SandboxJobStatus.FAILED:
642
664
  raise RuntimeError(f"Script failed (job {job.id}): {job.log or 'no log'}")
643
665
 
666
+ logger.debug(f"Ran script {script_id} on dataset {self.path}")
644
667
  return self.fetch(self._client, self.id)
645
668
 
646
669
 
@@ -7,6 +7,7 @@ from typing import Any, Literal, Optional
7
7
 
8
8
  from pydantic import BaseModel, Field, PrivateAttr, field_validator
9
9
 
10
+ from marple.db.activity import logger
10
11
  from marple.db.dataset import Dataset, DatasetList
11
12
  from marple.utils import (
12
13
  OMITTED,
@@ -123,6 +124,7 @@ class DataStream(BaseModel):
123
124
  json={"dataset_name": dataset_name, "metadata": metadata or {}},
124
125
  )
125
126
  r_json = validate_response(r, "Add dataset failed")
127
+ logger.debug(f"Added dataset {dataset_name}")
126
128
  return self.get_dataset(r_json["dataset_id"])
127
129
 
128
130
  def push_file(
@@ -171,6 +173,7 @@ class DataStream(BaseModel):
171
173
  self._abort_upload(init.ingestion_id, str(exc) or type(exc).__name__)
172
174
  raise
173
175
 
176
+ logger.debug(f"Pushed file {file_name or path.name}")
174
177
  return self.get_dataset(init.dataset_id)
175
178
 
176
179
  def _init_ingestion(
@@ -337,6 +340,7 @@ class DataStream(BaseModel):
337
340
 
338
341
  r = self._client.post(f"/stream/update/{self.id}", json=payload)
339
342
  validate_response(r, "Update stream failed")
343
+ logger.debug(f"Updated stream {self.name}: {payload}")
340
344
  return self.fetch(self._client, self.id)
341
345
 
342
346
  def rerun_processing(self, dataset_ids: Sequence[int] | None = None) -> None:
@@ -354,6 +358,8 @@ class DataStream(BaseModel):
354
358
  raise ValueError("rerun_processing requires at least one dataset id")
355
359
  r = self._client.post(f"/stream/{self.id}/processing/datasets", json=ids)
356
360
  validate_response(r, "Rerun processing failed")
361
+ target = f"{len(dataset_ids)} datasets" if dataset_ids is not None else "all datasets"
362
+ logger.debug(f"Reran processing for stream {self.name} ({target})")
357
363
 
358
364
  def delete(self) -> None:
359
365
  """
@@ -364,3 +370,4 @@ class DataStream(BaseModel):
364
370
  """
365
371
  r = self._client.post(f"/stream/{self.id}/delete")
366
372
  validate_response(r, "Delete stream failed")
373
+ logger.debug(f"Deleted stream {self.name}")
@@ -5,6 +5,7 @@ from typing import Any
5
5
 
6
6
  from pydantic import BaseModel, Field, PrivateAttr, field_validator
7
7
 
8
+ from marple.db.activity import logger
8
9
  from marple.utils import OMITTED, DBClient, Omitted, validate_response
9
10
 
10
11
 
@@ -132,12 +133,16 @@ class Script(BaseModel):
132
133
  payload["script"] = self.resolve_source(script)
133
134
 
134
135
  r = self._client.post(f"/script/{self.id}", json=payload)
135
- return Script(client=self._client, **validate_response(r, "Update script failed"))
136
+ updated = Script(client=self._client, **validate_response(r, "Update script failed"))
137
+ logger.debug(f"Updated script {self.name}: {payload}")
138
+ return updated
136
139
 
137
140
  def duplicate(self) -> "Script":
138
141
  """Create a copy of this script, including version history and stream attachments."""
139
142
  r = self._client.post(f"/script/{self.id}/duplicate")
140
- return Script(client=self._client, **validate_response(r, "Duplicate script failed"))
143
+ copy = Script(client=self._client, **validate_response(r, "Duplicate script failed"))
144
+ logger.debug(f"Duplicated script {self.name}")
145
+ return copy
141
146
 
142
147
  def delete(self) -> None:
143
148
  """
@@ -148,6 +153,7 @@ class Script(BaseModel):
148
153
  """
149
154
  r = self._client.delete(f"/script/{self.id}")
150
155
  validate_response(r, "Delete script failed")
156
+ logger.debug(f"Deleted script {self.name}")
151
157
 
152
158
  @staticmethod
153
159
  def resolve_source(script: Path | str) -> str:
@@ -7,6 +7,7 @@ from typing import Literal
7
7
  import pandas as pd
8
8
  from pydantic import BaseModel, PrivateAttr
9
9
 
10
+ from marple.db.activity import logger
10
11
  from marple.utils import DBClient, validate_response
11
12
 
12
13
 
@@ -150,3 +151,4 @@ class Signal(BaseModel):
150
151
  json={"signal_ids": [self.id]},
151
152
  )
152
153
  validate_response(r, "Delete signal failed")
154
+ logger.debug(f"Deleted signal {self.name}")
@@ -0,0 +1,52 @@
1
+ from io import StringIO
2
+ from unittest.mock import patch
3
+
4
+ import pytest
5
+
6
+ from marple.db import DB
7
+ from marple.db.activity import logger, verbose
8
+
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def _reset_verbose():
12
+ verbose(False)
13
+ yield
14
+ verbose(False)
15
+
16
+
17
+ def test_verbose_writes_debug_to_file():
18
+ buf = StringIO()
19
+ verbose(True, buf)
20
+ logger.debug("hello activity")
21
+ assert "hello activity" in buf.getvalue()
22
+
23
+
24
+ def test_verbose_true_is_idempotent():
25
+ buf = StringIO()
26
+ verbose(True, buf)
27
+ verbose(True, buf)
28
+ logger.debug("once")
29
+ assert buf.getvalue().count("once") == 1
30
+
31
+
32
+ def test_verbose_false_stops_output():
33
+ buf = StringIO()
34
+ verbose(True, buf)
35
+ logger.debug("before")
36
+ verbose(False)
37
+ logger.debug("after")
38
+ assert "before" in buf.getvalue()
39
+ assert "after" not in buf.getvalue()
40
+
41
+
42
+ def test_db_verbose_delegates():
43
+ with patch.object(DB, "check_connection", return_value=True):
44
+ with patch("marple.utils.DBClient"):
45
+ db = DB("token")
46
+ buf = StringIO()
47
+ db.verbose(True, buf)
48
+ logger.debug("via db")
49
+ assert "via db" in buf.getvalue()
50
+ db.verbose(False)
51
+ logger.debug("off")
52
+ assert "off" not in buf.getvalue()
@@ -1,5 +1,6 @@
1
1
  import random
2
2
  import re
3
+ from datetime import datetime
3
4
  from pathlib import Path
4
5
  from tempfile import TemporaryDirectory
5
6
  from typing import Literal
@@ -12,7 +13,7 @@ from requests import HTTPError
12
13
  import marple
13
14
  from marple import DB
14
15
  from marple.db import Dataset, DatasetList, DataStream
15
- from support import EXAMPLE_CSV, isolated_stream, unique_name
16
+ from support import EXAMPLE_CSV, ingest_dataset, isolated_stream, unique_name
16
17
 
17
18
  pytestmark = pytest.mark.integration
18
19
 
@@ -313,13 +314,16 @@ def test_push_file_plugin_args(example_stream: DataStream) -> None:
313
314
  assert dataset.plugin_args == "--use-index"
314
315
 
315
316
 
316
- def test_dataset_reingest(example_dataset: Dataset) -> None:
317
- reingested = example_dataset.reingest().wait_for_import(timeout=180)
318
- assert reingested.id == example_dataset.id
317
+ def test_dataset_reingest(example_stream: DataStream) -> None:
318
+ # Separate dataset on the shared session stream so reingest does not mutate
319
+ # example_dataset (Insight export and other shared consumers).
320
+ dataset = ingest_dataset(example_stream, metadata={"test": "reingest"})
321
+ reingested = dataset.reingest().wait_for_import(timeout=180)
322
+ assert reingested.id == dataset.id
319
323
  assert reingested.import_status == "FINISHED"
320
324
 
321
325
  with_args = reingested.reingest(plugin_args="--use-index").wait_for_import(timeout=180)
322
- assert with_args.id == example_dataset.id
326
+ assert with_args.id == dataset.id
323
327
  assert with_args.import_status == "FINISHED"
324
328
  assert with_args.plugin_args == "--use-index"
325
329
 
@@ -23,6 +23,7 @@ from marple.db.constants import (
23
23
  MAX_SIGNALS_PER_ADD,
24
24
  ROW_GROUP_SIZE,
25
25
  )
26
+ from marple.db.dataset import SCRIPTABLE_STATUSES
26
27
  from marple.db.signal_upload import (
27
28
  ParquetUploadMetadata,
28
29
  PresignedParquetFile,
@@ -328,10 +329,24 @@ def test_add_signals_rejects_too_many() -> None:
328
329
  Dataset.add_signals(dataset, too_many)
329
330
 
330
331
 
332
+ def test_add_signals_allows_scriptable_statuses(monkeypatch: pytest.MonkeyPatch) -> None:
333
+ monkeypatch.setattr("marple.db.dataset.run_signal_uploads", lambda *_args, **_kwargs: [1])
334
+ for status in SCRIPTABLE_STATUSES:
335
+ dataset = cast(Dataset, SimpleNamespace(id=1, path="demo", import_status=status, _signals={}))
336
+ assert Dataset.add_signals(dataset, [{"name": "s", "data": object()}]) == [1]
337
+
338
+
339
+ def test_add_signals_rejects_status_outside_scriptable() -> None:
340
+ dataset = cast(Dataset, SimpleNamespace(id=1, import_status="LIVE"))
341
+ with pytest.raises(ValueError, match="not in a writable state"):
342
+ Dataset.add_signals(dataset, [{"name": "s", "data": object()}])
343
+
344
+
331
345
  def test_delete_signals_posts_and_clears_cache() -> None:
332
346
  client = MagicMock()
333
347
  client.post.return_value = SimpleNamespace(status_code=200, json=lambda: {"status": "success"})
334
348
  dataset = SimpleNamespace(
349
+ path="dataset/10",
335
350
  id=10,
336
351
  datastream_id=5,
337
352
  n_signals=3,
@@ -833,7 +833,7 @@ wheels = [
833
833
 
834
834
  [[package]]
835
835
  name = "marpledata"
836
- version = "3.6.0.dev2"
836
+ version = "3.7.0"
837
837
  source = { editable = "." }
838
838
  dependencies = [
839
839
  { name = "azure-storage-blob" },
@@ -1 +0,0 @@
1
- *
File without changes
@@ -1 +0,0 @@
1
- Signature: 8a477f597d28d172789f06886806bc55
File without changes
@@ -1,33 +0,0 @@
1
- marple.Insight
2
- ==============
3
-
4
- .. currentmodule:: marple
5
-
6
- .. autoclass:: Insight
7
-
8
-
9
- .. automethod:: __init__
10
-
11
-
12
- .. rubric:: Methods
13
-
14
- .. autosummary::
15
-
16
- ~Insight.check_connection
17
- ~Insight.delete
18
- ~Insight.export_data
19
- ~Insight.export_data_mdb
20
- ~Insight.get
21
- ~Insight.get_dataset
22
- ~Insight.get_dataset_mdb
23
- ~Insight.get_datasets
24
- ~Insight.get_signals
25
- ~Insight.get_signals_mdb
26
- ~Insight.patch
27
- ~Insight.post
28
-
29
-
30
-
31
-
32
-
33
-
@@ -1,60 +0,0 @@
1
- marple.db.DB
2
- ============
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autoclass:: DB
7
-
8
-
9
- .. automethod:: __init__
10
-
11
-
12
- .. rubric:: Methods
13
-
14
- .. autosummary::
15
-
16
- ~DB.add_dataset
17
- ~DB.check_connection
18
- ~DB.connect_trino
19
- ~DB.create_script
20
- ~DB.create_stream
21
- ~DB.dataset_append
22
- ~DB.dataset_cool
23
- ~DB.delete
24
- ~DB.delete_dataset
25
- ~DB.delete_script
26
- ~DB.delete_stream
27
- ~DB.download_original
28
- ~DB.download_signal
29
- ~DB.get
30
- ~DB.get_dataset
31
- ~DB.get_datasets
32
- ~DB.get_script
33
- ~DB.get_scripts
34
- ~DB.get_signal
35
- ~DB.get_signals
36
- ~DB.get_status
37
- ~DB.get_stream
38
- ~DB.get_streams
39
- ~DB.patch
40
- ~DB.post
41
- ~DB.push_file
42
- ~DB.query
43
- ~DB.rerun_processing
44
- ~DB.run_script
45
- ~DB.update_metadata
46
- ~DB.update_stream
47
- ~DB.upsert_signals
48
-
49
-
50
-
51
-
52
-
53
- .. rubric:: Attributes
54
-
55
- .. autosummary::
56
-
57
- ~DB.trino_info
58
- ~DB.client
59
-
60
-
@@ -1,55 +0,0 @@
1
- marple.db.DataStream
2
- ====================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autoclass:: DataStream
7
-
8
-
9
- .. automethod:: __init__
10
-
11
-
12
- .. rubric:: Methods
13
-
14
- .. autosummary::
15
-
16
- ~DataStream.add_dataset
17
- ~DataStream.delete
18
- ~DataStream.fetch
19
- ~DataStream.get_dataset
20
- ~DataStream.get_datasets
21
- ~DataStream.push_file
22
- ~DataStream.refresh
23
- ~DataStream.rerun_processing
24
- ~DataStream.update
25
-
26
-
27
-
28
-
29
-
30
- .. rubric:: Attributes
31
-
32
- .. autosummary::
33
-
34
- ~DataStream.type
35
- ~DataStream.id
36
- ~DataStream.name
37
- ~DataStream.description
38
- ~DataStream.datapool
39
- ~DataStream.layer_shifts
40
- ~DataStream.version_id
41
- ~DataStream.insight_workspace
42
- ~DataStream.insight_project
43
- ~DataStream.created_at
44
- ~DataStream.last_updated
45
- ~DataStream.last_ingested
46
- ~DataStream.n_datasets
47
- ~DataStream.n_datapoints
48
- ~DataStream.cold_bytes
49
- ~DataStream.hot_bytes
50
- ~DataStream.plugin
51
- ~DataStream.plugin_args
52
- ~DataStream.signal_reduction
53
- ~DataStream.scripts
54
-
55
-
@@ -1,67 +0,0 @@
1
- marple.db.Dataset
2
- =================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autoclass:: Dataset
7
-
8
-
9
- .. automethod:: __init__
10
-
11
-
12
- .. rubric:: Methods
13
-
14
- .. autosummary::
15
-
16
- ~Dataset.add_signal
17
- ~Dataset.add_signals
18
- ~Dataset.append
19
- ~Dataset.cool
20
- ~Dataset.delete
21
- ~Dataset.download
22
- ~Dataset.fetch
23
- ~Dataset.get_data
24
- ~Dataset.get_debug_messages
25
- ~Dataset.get_signal
26
- ~Dataset.get_signals
27
- ~Dataset.reingest
28
- ~Dataset.rerun_processing
29
- ~Dataset.run
30
- ~Dataset.update_metadata
31
- ~Dataset.upsert_signals
32
- ~Dataset.wait_for_import
33
-
34
-
35
-
36
-
37
-
38
- .. rubric:: Attributes
39
-
40
- .. autosummary::
41
-
42
- ~Dataset.id
43
- ~Dataset.datastream_id
44
- ~Dataset.datastream_version
45
- ~Dataset.created_at
46
- ~Dataset.created_by
47
- ~Dataset.import_status
48
- ~Dataset.import_progress
49
- ~Dataset.import_message
50
- ~Dataset.import_time
51
- ~Dataset.path
52
- ~Dataset.metadata
53
- ~Dataset.cold_path
54
- ~Dataset.cold_bytes
55
- ~Dataset.hot_bytes
56
- ~Dataset.backup_path
57
- ~Dataset.backup_size
58
- ~Dataset.plugin
59
- ~Dataset.plugin_args
60
- ~Dataset.n_datapoints
61
- ~Dataset.n_signals
62
- ~Dataset.timestamp_start
63
- ~Dataset.timestamp_stop
64
- ~Dataset.import_speed
65
- ~Dataset.parquet_version
66
-
67
-
@@ -1,40 +0,0 @@
1
- marple.db.DatasetList
2
- =====================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autoclass:: DatasetList
7
-
8
-
9
- .. automethod:: __init__
10
-
11
-
12
- .. rubric:: Methods
13
-
14
- .. autosummary::
15
-
16
- ~DatasetList.append
17
- ~DatasetList.clear
18
- ~DatasetList.count
19
- ~DatasetList.extend
20
- ~DatasetList.from_dicts
21
- ~DatasetList.get_data
22
- ~DatasetList.index
23
- ~DatasetList.insert
24
- ~DatasetList.pop
25
- ~DatasetList.remove
26
- ~DatasetList.reverse
27
- ~DatasetList.sort
28
- ~DatasetList.to_pandas
29
- ~DatasetList.wait_for_import
30
- ~DatasetList.where
31
- ~DatasetList.where_dataset
32
- ~DatasetList.where_imported
33
- ~DatasetList.where_metadata
34
- ~DatasetList.where_signal
35
-
36
-
37
-
38
-
39
-
40
-
@@ -1,6 +0,0 @@
1
- marple.db.LAKE\_ARROW\_SCHEMA
2
- =============================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autodata:: LAKE_ARROW_SCHEMA
@@ -1,6 +0,0 @@
1
- marple.db.SCHEMA
2
- ================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autodata:: SCHEMA
@@ -1,43 +0,0 @@
1
- marple.db.SandboxJob
2
- ====================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autoclass:: SandboxJob
7
-
8
-
9
- .. automethod:: __init__
10
-
11
-
12
-
13
- .. rubric:: Methods
14
-
15
- .. autosummary::
16
-
17
- ~SandboxJob.fetch
18
-
19
-
20
-
21
-
22
-
23
- .. rubric:: Attributes
24
-
25
- .. autosummary::
26
-
27
- ~SandboxJob.id
28
- ~SandboxJob.dataset_id
29
- ~SandboxJob.stream_id
30
- ~SandboxJob.script_id
31
- ~SandboxJob.script_version
32
- ~SandboxJob.script_index
33
- ~SandboxJob.ingestion_id
34
- ~SandboxJob.status
35
- ~SandboxJob.batch_job_id
36
- ~SandboxJob.token_id
37
- ~SandboxJob.log
38
- ~SandboxJob.created_by
39
- ~SandboxJob.created_at
40
- ~SandboxJob.started_at
41
- ~SandboxJob.finished_at
42
-
43
-
@@ -1,28 +0,0 @@
1
- marple.db.SandboxJobStatus
2
- ==========================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autoclass:: SandboxJobStatus
7
-
8
-
9
- .. rubric:: Methods
10
-
11
- .. autosummary::
12
-
13
- ~SandboxJobStatus.is_terminal
14
-
15
-
16
-
17
-
18
-
19
- .. rubric:: Members
20
-
21
- .. autosummary::
22
-
23
- ~SandboxJobStatus.QUEUED
24
- ~SandboxJobStatus.RUNNING
25
- ~SandboxJobStatus.SUCCEEDED
26
- ~SandboxJobStatus.FAILED
27
-
28
-
@@ -1,41 +0,0 @@
1
- marple.db.Script
2
- ================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autoclass:: Script
7
-
8
-
9
- .. automethod:: __init__
10
-
11
-
12
- .. rubric:: Methods
13
-
14
- .. autosummary::
15
-
16
- ~Script.delete
17
- ~Script.duplicate
18
- ~Script.fetch
19
- ~Script.refresh
20
- ~Script.update
21
-
22
-
23
-
24
-
25
-
26
- .. rubric:: Attributes
27
-
28
- .. autosummary::
29
-
30
- ~Script.id
31
- ~Script.name
32
- ~Script.description
33
- ~Script.created_at
34
- ~Script.created_by
35
- ~Script.updated_at
36
- ~Script.updated_by
37
- ~Script.streams
38
- ~Script.versions
39
- ~Script.source
40
-
41
-
@@ -1,21 +0,0 @@
1
- marple.db.ScriptVersion
2
- =======================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autoclass:: ScriptVersion
7
-
8
-
9
- .. automethod:: __init__
10
-
11
-
12
- .. rubric:: Attributes
13
-
14
- .. autosummary::
15
-
16
- ~ScriptVersion.id
17
- ~ScriptVersion.script
18
- ~ScriptVersion.updated_at
19
- ~ScriptVersion.updated_by
20
-
21
-
@@ -1,47 +0,0 @@
1
- marple.db.Signal
2
- ================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autoclass:: Signal
7
-
8
-
9
- .. automethod:: __init__
10
-
11
-
12
- .. rubric:: Methods
13
-
14
- .. autosummary::
15
-
16
- ~Signal.cache_parquet
17
- ~Signal.get_data
18
- ~Signal.list_parquet_files
19
- ~Signal.wait_until_available
20
-
21
-
22
-
23
-
24
-
25
- .. rubric:: Attributes
26
-
27
- .. autosummary::
28
-
29
- ~Signal.id
30
- ~Signal.name
31
- ~Signal.unit
32
- ~Signal.description
33
- ~Signal.metadata
34
- ~Signal.storage_status
35
- ~Signal.cold_bytes
36
- ~Signal.hot_bytes
37
- ~Signal.count
38
- ~Signal.stats
39
- ~Signal.count_value
40
- ~Signal.count_text
41
- ~Signal.time_min
42
- ~Signal.time_max
43
- ~Signal.parquet_version
44
- ~Signal.datastream_id
45
- ~Signal.dataset_id
46
-
47
-
@@ -1,31 +0,0 @@
1
- marple.db.SignalUpload
2
- ======================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autoclass:: SignalUpload
7
-
8
-
9
- .. automethod:: __init__
10
-
11
-
12
- .. rubric:: Methods
13
-
14
- .. autosummary::
15
-
16
- ~SignalUpload.plan_upload
17
-
18
-
19
-
20
-
21
-
22
- .. rubric:: Attributes
23
-
24
- .. autosummary::
25
-
26
- ~SignalUpload.name
27
- ~SignalUpload.data
28
- ~SignalUpload.metadata
29
- ~SignalUpload.priority
30
-
31
-
@@ -1,6 +0,0 @@
1
- marple.db.SignalsAlreadyExistError
2
- ==================================
3
-
4
- .. currentmodule:: marple.db
5
-
6
- .. autoexception:: SignalsAlreadyExistError
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes