wizata-dsapi 2.1.0.dev17__tar.gz → 2.1.0.dev19__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. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/PKG-INFO +1 -1
  2. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/api_config.py +34 -6
  3. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/api_interface.py +20 -3
  4. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/deployment.py +6 -0
  5. wizata_dsapi-2.1.0.dev19/wizata_dsapi/version.py +1 -0
  6. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/wizata_dsapi_client.py +442 -35
  7. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi.egg-info/PKG-INFO +1 -1
  8. wizata_dsapi-2.1.0.dev17/wizata_dsapi/version.py +0 -1
  9. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/LICENSE.txt +0 -0
  10. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/README.rst +0 -0
  11. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/setup.cfg +0 -0
  12. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/setup.py +0 -0
  13. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/__init__.py +0 -0
  14. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/api_dto.py +0 -0
  15. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/bucket.py +0 -0
  16. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/business_label.py +0 -0
  17. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/context.py +0 -0
  18. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/dashboard.py +0 -0
  19. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/dataframe_toolkit.py +0 -0
  20. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/datapoint.py +0 -0
  21. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/datastore.py +0 -0
  22. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/ds_dataframe.py +0 -0
  23. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/dsapi_json_encoder.py +0 -0
  24. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/edge_config.py +0 -0
  25. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/edge_device.py +0 -0
  26. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/edge_module.py +0 -0
  27. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/evaluation.py +0 -0
  28. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/execution.py +0 -0
  29. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/execution_log.py +0 -0
  30. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/experiment.py +0 -0
  31. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/graylog_log.py +0 -0
  32. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/group_system.py +0 -0
  33. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/ilogger.py +0 -0
  34. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/insight.py +0 -0
  35. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/mlmodel.py +0 -0
  36. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/mobile_asset.py +0 -0
  37. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/model_toolkit.py +0 -0
  38. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/models/__init__.py +0 -0
  39. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/models/common.py +0 -0
  40. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/notification.py +0 -0
  41. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/paged_query_result.py +0 -0
  42. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/pipeline.py +0 -0
  43. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/pipeline_image.py +0 -0
  44. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/plot.py +0 -0
  45. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/plots/__init__.py +0 -0
  46. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/plots/common.py +0 -0
  47. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/plots/theme.py +0 -0
  48. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/request.py +0 -0
  49. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/script.py +0 -0
  50. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/scripts/__init__.py +0 -0
  51. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/scripts/common.py +0 -0
  52. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/search.py +0 -0
  53. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/solution_component.py +0 -0
  54. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/streamlit_utils.py +0 -0
  55. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/template.py +0 -0
  56. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/template_config.py +0 -0
  57. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/trigger.py +0 -0
  58. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/twin.py +0 -0
  59. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/twinregistration.py +0 -0
  60. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/user.py +0 -0
  61. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/wizard_function.py +0 -0
  62. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/wizard_request.py +0 -0
  63. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi/words.py +0 -0
  64. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi.egg-info/SOURCES.txt +0 -0
  65. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
  66. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi.egg-info/requires.txt +0 -0
  67. {wizata_dsapi-2.1.0.dev17 → wizata_dsapi-2.1.0.dev19}/wizata_dsapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wizata_dsapi
3
- Version: 2.1.0.dev17
3
+ Version: 2.1.0.dev19
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -17,10 +17,33 @@ from .trigger import Trigger
17
17
  from .twin import Twin, TwinType
18
18
  from .pipeline import Pipeline
19
19
  from .pipeline_image import PipelineImage
20
+ from .deployment import Deployment
20
21
  from .insight import Insight
21
22
  from .edge_device import EdgeDevice
22
23
 
23
24
 
25
+ # Entities withdrawn from the client, and what to say instead.
26
+ #
27
+ # Removing one from `_registry` alone would fail with "Api entity triggers is not
28
+ # supported", which reads as a bug in the client. These messages name the
29
+ # replacement, so the first person to hit it can act instead of filing.
30
+ #
31
+ # The DTO itself is NOT removed — `wizata_dsapi.Trigger` is still imported and used
32
+ # by the platform internally (the 12.1 adopt-triggers migration reads them through
33
+ # `ApiDataManager.backend_api`, which never consults this registry). What goes is
34
+ # the ability to schedule through the public client.
35
+ _retired = {
36
+ "triggers": (
37
+ Trigger,
38
+ "triggers are no longer managed through this client (12.1). A schedule now "
39
+ "belongs to a Deployment, which owns its trigger and knows how to pause, "
40
+ "resume and remove it: use api.deploy(...) and the deployment methods. "
41
+ "Deleting a trigger on its own would leave a Deployment record claiming a "
42
+ "schedule that no longer exists, and nothing would find it again."
43
+ ),
44
+ }
45
+
46
+
24
47
  # init APIs supported operations
25
48
  _registry = {
26
49
  "business_labels":
@@ -41,6 +64,17 @@ _registry = {
41
64
  "cloud_dsapi": ['lists', 'get_by_id', 'get_by_key', 'create', 'update', 'delete', 'search'],
42
65
  "cloud_context": ['get_by_id', 'get_by_key', 'create', 'update', 'delete', 'search']
43
66
  },
67
+ "deployments":
68
+ {
69
+ "class": Deployment,
70
+ # No 'create': a deployment is not posted into existence, it is the
71
+ # RESULT of deploying a pipeline — see WizataDSAPIClient.deploy, which
72
+ # runs merge, release, image and trigger as one call and returns the
73
+ # record to poll. Offering create here would invite a half-built record
74
+ # with no schedule behind it.
75
+ "cloud_dsapi": ['lists', 'get_by_id', 'update', 'delete', 'search'],
76
+ "cloud_context": []
77
+ },
44
78
  "datastores":
45
79
  {
46
80
  "class": DataStore,
@@ -137,12 +171,6 @@ _registry = {
137
171
  "cloud_dsapi": ['lists', 'get_by_id', 'create', 'update', 'delete'],
138
172
  "cloud_context": []
139
173
  },
140
- "triggers":
141
- {
142
- "class": Trigger,
143
- "cloud_dsapi": ['lists', 'get_by_id', 'create', 'update', 'delete'],
144
- "cloud_context": ['get_by_id']
145
- },
146
174
  "twins":
147
175
  {
148
176
  "class": Twin,
@@ -134,14 +134,31 @@ class ApiInterface:
134
134
  if entity in self._dto_registry.keys():
135
135
  self._verify_operation(entity, operation)
136
136
  return self._dto_registry[entity]["class"]
137
- else:
138
- raise TypeError(f"Api entity {entity} is not supported.")
137
+ self._refuse(entity)
139
138
  else:
140
139
  for entity_name in self._dto_registry:
141
140
  if self._dto_registry[entity_name]["class"] == entity:
142
141
  self._verify_operation(entity_name, operation)
143
142
  return entity
144
- raise TypeError(f"Api entity {entity} is not supported.")
143
+ self._refuse(entity)
144
+
145
+ @staticmethod
146
+ def _refuse(entity):
147
+ """
148
+ Raise for an entity this client does not serve, saying WHY when it is one
149
+ that was withdrawn rather than one that never existed.
150
+
151
+ "Api entity triggers is not supported" reads as a bug in the client, and the
152
+ person reading it has no way to know a schedule is now a Deployment. Both
153
+ the plural name and the DTO class are matched, since callers reach the
154
+ registry through either.
155
+ """
156
+ from .api_config import _retired
157
+
158
+ for name, (dto, message) in _retired.items():
159
+ if entity == name or entity is dto:
160
+ raise TypeError(f"Api entity {name} is no longer supported: {message}")
161
+ raise TypeError(f"Api entity {entity} is not supported.")
145
162
 
146
163
  def _verify_operation(self,
147
164
  entity,
@@ -210,6 +210,12 @@ class Deployment(ApiDto):
210
210
  if "updatedDate" in obj.keys() and obj["updatedDate"] is not None:
211
211
  self.updatedDate = obj["updatedDate"]
212
212
 
213
+ def to_dict(self) -> dict:
214
+ """Serialize the Deployment. Alias of :meth:`to_json`, which is the
215
+ canonical implementation here — the DTO convention accepts either
216
+ spelling, and the client calls ``to_dict``."""
217
+ return self.to_json()
218
+
213
219
  def to_json(self, target: str = None):
214
220
  obj = {
215
221
  "id": str(self.deployment_id),
@@ -0,0 +1 @@
1
+ __version__ = "2.1.0.dev19"
@@ -53,6 +53,7 @@ from .api_interface import ApiInterface
53
53
  from .mobile_asset import MobileAssetTile, MobileAssetConfig
54
54
  from .version import __version__
55
55
  import ast
56
+ from .deployment import Deployment, DeploymentStatus
56
57
  from .api_config import _registry
57
58
 
58
59
  from .streamlit_utils import get_streamlit_token, get_streamlit_domain
@@ -713,39 +714,28 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
713
714
  else:
714
715
  return self.create(obj)
715
716
 
716
- def query(self,
717
- datapoints: list = None,
718
- start: datetime = None,
719
- end: datetime = None,
720
- interval: int = None,
721
- agg_method: str = "mean",
722
- template: str = None,
723
- twin: str = None,
724
- null: str = None,
725
- filters: dict = None,
726
- options: dict = None,
727
- group = None,
728
- field=None,
729
- bucket: str = None,
730
- tags: dict = None,
731
- scopes: list = None) -> pandas.DataFrame:
732
- """
733
- Query a dataframe from API.
734
- :param agg_method:
735
- :param datapoints: list of datapoints to fetch.
736
- :param start: start datetime of range to fetch
737
- :param end: end datetime of range to fetch
738
- :param interval: interval in milliseconds.
739
- :param template: template to fetch.
740
- :param twin: hardware ID of twin to fetch based on template.
741
- :param null: By default at 'drop' and dropping NaN values. If not intended behavior please set it to 'ignore' or 'all'.
742
- :param group: can be use to set group system and event retrieving instructions (dict or list[dict]).
743
- :param filters: dict of filters.
744
- :param options: dict of options.
745
- :param field: by default 'value' if none, accept str or list (e.g. value, reliability, eventId, ...)
746
- :param bucket: specify on which bucket the query applies.
747
- :param scopes: list of scope filters for SCOPED datapoints (e.g. ["normal", {"vibration": {"gte": 0.6}}]).
748
- :return: dataframe
717
+ def _build_request(self,
718
+ datapoints: list = None,
719
+ start: datetime = None,
720
+ end: datetime = None,
721
+ interval: int = None,
722
+ agg_method: str = "mean",
723
+ template: str = None,
724
+ twin: str = None,
725
+ null: str = None,
726
+ filters: dict = None,
727
+ options: dict = None,
728
+ group=None,
729
+ field=None,
730
+ bucket: str = None,
731
+ tags: dict = None,
732
+ scopes: list = None) -> Request:
733
+ """
734
+ Build the Request behind a time-series query.
735
+
736
+ Shared by :meth:`query` and :meth:`query_json` so the two cannot disagree
737
+ about what an argument means. They differ only in what they ask the server
738
+ to send back — a DataFrame or described JSON — never in what they ask for.
749
739
  """
750
740
  request = Request()
751
741
 
@@ -796,7 +786,47 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
796
786
  if bucket is not None:
797
787
  request.bucket = bucket
798
788
 
799
- return self._query(request=request)
789
+ return request
790
+
791
+ def query(self,
792
+ datapoints: list = None,
793
+ start: datetime = None,
794
+ end: datetime = None,
795
+ interval: int = None,
796
+ agg_method: str = "mean",
797
+ template: str = None,
798
+ twin: str = None,
799
+ null: str = None,
800
+ filters: dict = None,
801
+ options: dict = None,
802
+ group = None,
803
+ field=None,
804
+ bucket: str = None,
805
+ tags: dict = None,
806
+ scopes: list = None) -> pandas.DataFrame:
807
+ """
808
+ Query a dataframe from API.
809
+ :param agg_method:
810
+ :param datapoints: list of datapoints to fetch.
811
+ :param start: start datetime of range to fetch
812
+ :param end: end datetime of range to fetch
813
+ :param interval: interval in milliseconds.
814
+ :param template: template to fetch.
815
+ :param twin: hardware ID of twin to fetch based on template.
816
+ :param null: By default at 'drop' and dropping NaN values. If not intended behavior please set it to 'ignore' or 'all'.
817
+ :param group: can be use to set group system and event retrieving instructions (dict or list[dict]).
818
+ :param filters: dict of filters.
819
+ :param options: dict of options.
820
+ :param field: by default 'value' if none, accept str or list (e.g. value, reliability, eventId, ...)
821
+ :param bucket: specify on which bucket the query applies.
822
+ :param scopes: list of scope filters for SCOPED datapoints (e.g. ["normal", {"vibration": {"gte": 0.6}}]).
823
+ :return: dataframe
824
+ """
825
+ return self._query(request=self._build_request(
826
+ datapoints=datapoints, start=start, end=end, interval=interval,
827
+ agg_method=agg_method, template=template, twin=twin, null=null,
828
+ filters=filters, options=options, group=group, field=field,
829
+ bucket=bucket, tags=tags, scopes=scopes))
800
830
 
801
831
  def batch_query(self,
802
832
  step: timedelta,
@@ -1124,7 +1154,12 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
1124
1154
  else:
1125
1155
  raise ValueError('No plot has been fetch.')
1126
1156
  elif figure is not None:
1127
- return plotly.io.from_json(plot.figure)
1157
+ # Read `figure`, not `plot.figure` — with only a figure passed there is no
1158
+ # plot to read it from. A live plot's envelope carries the figure as the
1159
+ # JSON string it was stored as, an experiment plot as a dict once decoded,
1160
+ # and both must render.
1161
+ return plotly.io.from_json(figure if isinstance(figure, str)
1162
+ else json.dumps(figure))
1128
1163
  elif plot_id is not None:
1129
1164
  plot = self.get(Plot(plot_id=plot_id))
1130
1165
  if plot.figure is not None:
@@ -1156,6 +1191,90 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
1156
1191
  else:
1157
1192
  raise TypeError("execution must be an ExecutionLog")
1158
1193
 
1194
+ @staticmethod
1195
+ def _pipeline_ref(pipeline) -> str:
1196
+ """
1197
+ Path segment addressing a pipeline, from a key, a UUID or a Pipeline.
1198
+
1199
+ The live-plot route accepts both the key and the UUID, so a caller passes
1200
+ whichever it already has. A Pipeline is asked for its key first: a deployed
1201
+ plot is addressed by pipeline KEY everywhere else it appears (the blob path,
1202
+ ExecutionLog.plots), so the key is the reference that matches what comes back.
1203
+ """
1204
+ if isinstance(pipeline, Pipeline):
1205
+ reference = pipeline.key if pipeline.key is not None else pipeline.pipeline_id
1206
+ else:
1207
+ reference = pipeline
1208
+ if reference is None:
1209
+ raise ValueError("a pipeline key, UUID or Pipeline is required")
1210
+ return urllib.parse.quote(str(reference), safe="")
1211
+
1212
+ def deployed_plots(self, pipeline, twin: str = None) -> list:
1213
+ """
1214
+ list a deployed pipeline's LIVE plots - what its deployments are currently
1215
+ drawing, one per (twin, plot step).
1216
+
1217
+ A live plot is overwritten on every triggered run, so there is exactly one
1218
+ current figure per plot and no history to page through. Fetch one with
1219
+ :meth:`deployed_plot`. Experiment plots are elsewhere and unchanged - one blob
1220
+ per execution, see :meth:`plots`.
1221
+
1222
+ :param pipeline: pipeline key, UUID or Pipeline - both references are accepted.
1223
+ :param twin: hardware id, to list only that twin's plots.
1224
+ :return: list of dict {pipelineKey, twin, script, dataframe, key, deploymentId,
1225
+ produced, lastModified, size}. 'produced' is False when the definition has
1226
+ this plot step but no run has written it yet.
1227
+ """
1228
+ response = self._process_request(
1229
+ method="GET",
1230
+ route=f"{Pipeline.route()}/{self._pipeline_ref(pipeline)}/plots/",
1231
+ params={"twin": twin} if twin is not None else None
1232
+ )
1233
+ return response.json().get("results", [])
1234
+
1235
+ def deployed_plot(self,
1236
+ pipeline,
1237
+ script: str,
1238
+ dataframe: str,
1239
+ twin: str = None,
1240
+ shape: str = "figure") -> dict:
1241
+ """
1242
+ fetch one of a deployed pipeline's LIVE plots.
1243
+
1244
+ The plot step is identified by BOTH its script and its input dataframe - that
1245
+ pair is the plot's identity, as :meth:`deployed_plots` lists it.
1246
+
1247
+ Returns the envelope, not the bare figure, because the provenance is the point:
1248
+ a live plot is overwritten on every run and 'generatedAt' / 'executionId' say
1249
+ which one wrote it. Render it with ``client.plot(figure=envelope["figure"])``.
1250
+
1251
+ :param pipeline: pipeline key, UUID or Pipeline - both references are accepted.
1252
+ :param script: the plot step's script.
1253
+ :param dataframe: the plot step's input dataframe.
1254
+ :param twin: hardware id - required on a templated pipeline.
1255
+ :param shape: 'figure' (default) for the stored Plotly figure, or 'data' for its
1256
+ traces projected to 'series'.
1257
+ :return: dict with {pipelineKey, twin, script, dataframe, name, executionId,
1258
+ release, sha, imageId, generatedAt} plus 'figure', or 'series' and
1259
+ 'notProjected' when shape is 'data'.
1260
+ """
1261
+ if script is None or dataframe is None:
1262
+ raise ValueError(
1263
+ "script and dataframe are both parts of a plot's identity - pass both"
1264
+ )
1265
+ if shape not in ("figure", "data"):
1266
+ raise ValueError(f"shape must be 'figure' or 'data', got {shape!r}")
1267
+
1268
+ params = {"script": script, "dataframe": dataframe, "format": shape}
1269
+ if twin is not None:
1270
+ params["twin"] = twin
1271
+ response = self._process_request(
1272
+ method="GET",
1273
+ route=f"{Pipeline.route()}/{self._pipeline_ref(pipeline)}/plots/",
1274
+ params=params
1275
+ )
1276
+ return response.json()
1277
+
1159
1278
  def upload_model(self,
1160
1279
  model_info: ModelInfo,
1161
1280
  bytes_content = None):
@@ -1924,6 +2043,294 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
1924
2043
  )
1925
2044
  return response.text
1926
2045
 
2046
+ def query_json(self, include_format: bool = True, **kwargs) -> dict:
2047
+ """
2048
+ query time-series as JSON, described.
2049
+
2050
+ Takes the same arguments as :meth:`query`. Where ``query`` hands back a
2051
+ DataFrame, this returns the rows as JSON — and by default wraps them in a
2052
+ description of their own shape, so a caller that is not pandas can render
2053
+ them without guessing:
2054
+
2055
+ {"columns": [...], "pivot": ..., "data": [...]}
2056
+
2057
+ That is what the front-end's query form is built on: an aggregated query
2058
+ comes back pivoted with one column per datapoint, a raw one comes back long
2059
+ with a 'value' column, and only the envelope says which you got (DEV-6031).
2060
+
2061
+ A separate method rather than a flag on :meth:`query`, deliberately — a
2062
+ parameter that changes the return type between a DataFrame and a dict makes
2063
+ every caller branch on what it asked for.
2064
+
2065
+ :param include_format: False to get the bare rows without the envelope.
2066
+ :return: dict with columns/pivot/data, or the rows alone when
2067
+ ``include_format`` is False.
2068
+ """
2069
+ request = self._build_request(**kwargs)
2070
+ response = self._process_request(
2071
+ method="POST",
2072
+ route="data/",
2073
+ params={"include-format": "true" if include_format else "false"},
2074
+ data=json.dumps(request.to_json(), cls=DSAPIEncoder)
2075
+ )
2076
+ return response.json()
2077
+
2078
+ # ------------------------------------------------------------------ git branches
2079
+
2080
+ def branches(self, pipeline) -> dict:
2081
+ """
2082
+ list the branches relevant to a pipeline.
2083
+
2084
+ :param pipeline: pipeline key, UUID or Pipeline.
2085
+ :return: dict with 'default', 'feature' and 'release' - feature branches are
2086
+ this pipeline's drafts, release branches are the frozen refs deployments
2087
+ run from.
2088
+ """
2089
+ response = self._process_request(
2090
+ method="GET",
2091
+ route=f"{Pipeline.route()}/{self._pipeline_ref(pipeline)}/branches/"
2092
+ )
2093
+ return response.json()
2094
+
2095
+ def create_branch(self, pipeline, name: str = None, source: str = None) -> Pipeline:
2096
+ """
2097
+ create a branch for a pipeline and return the pipeline as it stands on it.
2098
+
2099
+ :param pipeline: pipeline key, UUID or Pipeline.
2100
+ :param name: branch name. Leave None for a draft, which is named for you.
2101
+ A release is named ``release/<name>``.
2102
+ :param source: ref to branch from; the default branch when None.
2103
+ :return: the Pipeline read at the new branch, with 'branch' set.
2104
+ """
2105
+ payload = {}
2106
+ if name is not None:
2107
+ payload["name"] = name
2108
+ if source is not None:
2109
+ payload["source"] = source
2110
+ response = self._process_request(
2111
+ method="POST",
2112
+ route=f"{Pipeline.route()}/{self._pipeline_ref(pipeline)}/branches/",
2113
+ data=json.dumps(payload)
2114
+ )
2115
+ return Pipeline.from_dict(response.json())
2116
+
2117
+ def delete_branch(self, pipeline, branch: str) -> None:
2118
+ """
2119
+ discard a branch.
2120
+
2121
+ A draft goes on demand. A release is refused with 409 while a deployment or a
2122
+ trigger still runs from it - pause or redeploy those first, rather than
2123
+ leaving a schedule pointing at a ref that no longer exists.
2124
+
2125
+ :param pipeline: pipeline key, UUID or Pipeline.
2126
+ :param branch: branch ref.
2127
+ """
2128
+ self._process_request(
2129
+ method="DELETE",
2130
+ route=f"{Pipeline.route()}/{self._pipeline_ref(pipeline)}/branches/{branch}/"
2131
+ )
2132
+
2133
+ def merge_branch(self, pipeline, branch: str) -> dict:
2134
+ """
2135
+ merge a draft into the default branch and delete it.
2136
+
2137
+ :param pipeline: pipeline key, UUID or Pipeline.
2138
+ :param branch: the feature branch to publish.
2139
+ :return: dict with 'merged_sha', 'deleted_branch' and 'files_changed'.
2140
+ """
2141
+ response = self._process_request(
2142
+ method="POST",
2143
+ route=f"{Pipeline.route()}/{self._pipeline_ref(pipeline)}/branches/{branch}/merge"
2144
+ )
2145
+ return response.json()
2146
+
2147
+ def get_pipeline_at(self, pipeline, branch: str) -> Pipeline:
2148
+ """
2149
+ read a pipeline as it stands on a branch, rather than on the default one.
2150
+
2151
+ :param pipeline: pipeline key, UUID or Pipeline.
2152
+ :param branch: branch ref.
2153
+ :return: Pipeline.
2154
+ """
2155
+ response = self._process_request(
2156
+ method="GET",
2157
+ route=f"{Pipeline.route()}/{self._pipeline_ref(pipeline)}/branches/{branch}/"
2158
+ )
2159
+ return Pipeline.from_dict(response.json())
2160
+
2161
+ def get_script_at(self, name: str, branch: str) -> Script:
2162
+ """
2163
+ read a RAW script's source as it stands on a branch.
2164
+
2165
+ A branch run executes the branch's script, so this is what actually runs
2166
+ there - the default-branch copy may differ.
2167
+
2168
+ :param name: script name.
2169
+ :param branch: branch ref.
2170
+ :return: Script.
2171
+ """
2172
+ response = self._process_request(
2173
+ method="GET",
2174
+ route=f"{Script.route()}/{name}/branches/{branch}/"
2175
+ )
2176
+ return Script.from_dict(response.json())
2177
+
2178
+ def save_script_at(self, script: Script, branch: str) -> None:
2179
+ """
2180
+ save a RAW script on a branch, leaving the default branch untouched until the
2181
+ branch is merged.
2182
+
2183
+ :param script: Script with its source set.
2184
+ :param branch: branch ref.
2185
+ """
2186
+ self._process_request(
2187
+ method="PUT",
2188
+ route=f"{Script.route()}/{script.name}/branches/{branch}/",
2189
+ files={"payload": (None, json.dumps(script.to_dict(), cls=DSAPIEncoder))}
2190
+ )
2191
+
2192
+ # ------------------------------------------------------------------ deployments
2193
+
2194
+ def deploy(self,
2195
+ pipeline,
2196
+ twins: list = None,
2197
+ interval: int = None,
2198
+ delay: int = None,
2199
+ packaging: str = None,
2200
+ source: str = None,
2201
+ properties: dict = None,
2202
+ plot: bool = None,
2203
+ train: bool = None,
2204
+ write: bool = None) -> Deployment:
2205
+ """
2206
+ deploy a pipeline: merge the draft, cut the release, build the image and create
2207
+ the schedule - the whole sequence, as one call.
2208
+
2209
+ Returns immediately with a Deployment to poll; an image build outlasts the
2210
+ gateway timeout, so the record carries the progress instead of the request.
2211
+ Poll it with :meth:`get` on the returned id, or :meth:`await_deployment`.
2212
+
2213
+ :param pipeline: pipeline key, UUID or Pipeline.
2214
+ :param twins: hardware ids or twin UUIDs this schedule covers - both forms are
2215
+ accepted - or ``{twin, target, deviceId}`` entries where target is "cloud"
2216
+ (default) or "edge".
2217
+ :param interval: milliseconds between runs.
2218
+ :param delay: milliseconds to wait after each interval boundary.
2219
+ :param packaging: "image" (default) or "release".
2220
+ :param source: branch or ref to deploy from.
2221
+ :param properties: pipeline properties for every run.
2222
+ :param plot: produce plots on each run. Refused on edge.
2223
+ :param train: train models on each run. Refused on edge, and on an
2224
+ image-packaged deployment - the model would be trained and silently
2225
+ discarded, because the image's frozen model is what predict uses.
2226
+ :param write: write results back. Defaults on.
2227
+ :return: Deployment to poll.
2228
+ """
2229
+ payload = {}
2230
+ if twins is not None:
2231
+ payload["twins"] = twins
2232
+ for name, value in (("interval", interval), ("delay", delay),
2233
+ ("packaging", packaging), ("source", source),
2234
+ ("properties", properties)):
2235
+ if value is not None:
2236
+ payload[name] = value
2237
+ for name, value in (("plot", plot), ("train", train), ("write", write)):
2238
+ if value is not None:
2239
+ payload[name] = value
2240
+ response = self._process_request(
2241
+ method="POST",
2242
+ route=f"{Pipeline.route()}/{self._pipeline_ref(pipeline)}/deploy/",
2243
+ data=json.dumps(payload)
2244
+ )
2245
+ return Deployment.from_dict(response.json())
2246
+
2247
+ def deploy_preview(self, pipeline, **kwargs) -> dict:
2248
+ """
2249
+ plan a deploy without performing it: what would be merged, cut, built and
2250
+ scheduled, and every reason it would be refused.
2251
+
2252
+ Writes nothing, and always answers 200 - read ``ok`` and ``problems`` rather
2253
+ than catching an exception. Takes the same arguments as :meth:`deploy`.
2254
+
2255
+ :param pipeline: pipeline key, UUID or Pipeline.
2256
+ :return: the plan, including 'ok' and 'problems'.
2257
+ """
2258
+ payload = {k: v for k, v in kwargs.items() if v is not None}
2259
+ response = self._process_request(
2260
+ method="POST",
2261
+ route=f"{Pipeline.route()}/{self._pipeline_ref(pipeline)}/deploy/preview/",
2262
+ data=json.dumps(payload)
2263
+ )
2264
+ return response.json()
2265
+
2266
+ def _deployment_action(self, deployment, action: str) -> Deployment:
2267
+ deployment_id = (deployment.deployment_id
2268
+ if isinstance(deployment, Deployment) else deployment)
2269
+ response = self._process_request(
2270
+ method="POST",
2271
+ route=f"{Deployment.route()}/{deployment_id}/{action}/"
2272
+ )
2273
+ return Deployment.from_dict(response.json())
2274
+
2275
+ def pause_deployment(self, deployment) -> Deployment:
2276
+ """
2277
+ pause a deployment: delete its trigger, keep the record.
2278
+
2279
+ Synchronous. The manifest is kept, so :meth:`resume_deployment` rebuilds the
2280
+ same schedule rather than asking for it again.
2281
+
2282
+ :param deployment: Deployment or its id.
2283
+ :return: the updated Deployment.
2284
+ """
2285
+ return self._deployment_action(deployment, "pause")
2286
+
2287
+ def resume_deployment(self, deployment) -> Deployment:
2288
+ """
2289
+ resume a paused deployment, re-creating its trigger from the stored manifest.
2290
+
2291
+ :param deployment: Deployment or its id.
2292
+ :return: the updated Deployment.
2293
+ """
2294
+ return self._deployment_action(deployment, "resume")
2295
+
2296
+ def retry_deployment(self, deployment) -> Deployment:
2297
+ """
2298
+ continue a failed or interrupted deployment from where it stopped.
2299
+
2300
+ Safe to call repeatedly: a merge or a release cut cannot be undone, so
2301
+ completed steps are skipped rather than replayed.
2302
+
2303
+ :param deployment: Deployment or its id.
2304
+ :return: the updated Deployment.
2305
+ """
2306
+ return self._deployment_action(deployment, "retry")
2307
+
2308
+ def await_deployment(self,
2309
+ deployment,
2310
+ timeout: int = 900,
2311
+ interval: int = 5) -> Deployment:
2312
+ """
2313
+ poll a deployment until it stops moving.
2314
+
2315
+ :param deployment: Deployment or its id.
2316
+ :param timeout: seconds before giving up waiting.
2317
+ :param interval: seconds between polls.
2318
+ :return: the Deployment in its final state - check ``status``, which is
2319
+ 'deployed' on success and 'failed' otherwise.
2320
+ """
2321
+ import time
2322
+ deployment_id = (deployment.deployment_id
2323
+ if isinstance(deployment, Deployment) else deployment)
2324
+ deadline = time.time() + timeout
2325
+ current = self.get(deployment_id=deployment_id)
2326
+ while time.time() < deadline:
2327
+ if current.status in (DeploymentStatus.DEPLOYED, DeploymentStatus.FAILED,
2328
+ DeploymentStatus.PAUSED):
2329
+ return current
2330
+ time.sleep(interval)
2331
+ current = self.get(deployment_id=deployment_id)
2332
+ return current
2333
+
1927
2334
  def build_image(self, key: str, branch: str = None) -> str:
1928
2335
  """
1929
2336
  build an image of a pipeline, store it on the image repository and return its pipeline image id.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wizata_dsapi
3
- Version: 2.1.0.dev17
3
+ Version: 2.1.0.dev19
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -1 +0,0 @@
1
- __version__ = "2.1.0.dev17"