wizata-dsapi 2.2.0.dev5__tar.gz → 2.2.0.dev7__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 (71) hide show
  1. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/PKG-INFO +1 -1
  2. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/__init__.py +3 -0
  3. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/datapoint.py +6 -0
  4. wizata_dsapi-2.2.0.dev7/wizata_dsapi/inventory.py +148 -0
  5. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/request.py +15 -1
  6. wizata_dsapi-2.2.0.dev7/wizata_dsapi/version.py +1 -0
  7. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/wizata_dsapi_client.py +59 -3
  8. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi.egg-info/PKG-INFO +1 -1
  9. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi.egg-info/SOURCES.txt +1 -0
  10. wizata_dsapi-2.2.0.dev5/wizata_dsapi/version.py +0 -1
  11. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/LICENSE.txt +0 -0
  12. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/README.rst +0 -0
  13. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/setup.cfg +0 -0
  14. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/setup.py +0 -0
  15. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/api_config.py +0 -0
  16. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/api_dto.py +0 -0
  17. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/api_interface.py +0 -0
  18. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/bucket.py +0 -0
  19. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/business_label.py +0 -0
  20. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/connection.py +0 -0
  21. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/connection_types.py +0 -0
  22. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/context.py +0 -0
  23. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/dashboard.py +0 -0
  24. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/dataframe_toolkit.py +0 -0
  25. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/datastore.py +0 -0
  26. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/deployment.py +0 -0
  27. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/ds_dataframe.py +0 -0
  28. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/dsapi_json_encoder.py +0 -0
  29. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/edge_config.py +0 -0
  30. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/edge_device.py +0 -0
  31. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/edge_module.py +0 -0
  32. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/evaluation.py +0 -0
  33. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/execution.py +0 -0
  34. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/execution_log.py +0 -0
  35. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/experiment.py +0 -0
  36. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/graylog_log.py +0 -0
  37. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/group_system.py +0 -0
  38. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/ilogger.py +0 -0
  39. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/insight.py +0 -0
  40. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/mlmodel.py +0 -0
  41. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/mobile_asset.py +0 -0
  42. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/model_toolkit.py +0 -0
  43. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/models/__init__.py +0 -0
  44. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/models/common.py +0 -0
  45. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/notification.py +0 -0
  46. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/paged_query_result.py +0 -0
  47. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/pipeline.py +0 -0
  48. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/pipeline_image.py +0 -0
  49. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/plot.py +0 -0
  50. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/plots/__init__.py +0 -0
  51. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/plots/common.py +0 -0
  52. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/plots/theme.py +0 -0
  53. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/product.py +0 -0
  54. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/script.py +0 -0
  55. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/scripts/__init__.py +0 -0
  56. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/scripts/common.py +0 -0
  57. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/search.py +0 -0
  58. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/solution_component.py +0 -0
  59. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/streamlit_utils.py +0 -0
  60. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/template.py +0 -0
  61. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/template_config.py +0 -0
  62. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/trigger.py +0 -0
  63. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/twin.py +0 -0
  64. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/twinregistration.py +0 -0
  65. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/user.py +0 -0
  66. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/wizard_function.py +0 -0
  67. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/wizard_request.py +0 -0
  68. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi/words.py +0 -0
  69. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
  70. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/wizata_dsapi.egg-info/requires.txt +0 -0
  71. {wizata_dsapi-2.2.0.dev5 → wizata_dsapi-2.2.0.dev7}/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.2.0.dev5
3
+ Version: 2.2.0.dev7
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -66,6 +66,9 @@ from .connection import Connection, ConnectionMapping, ConnectionDirection, Tran
66
66
  from .connection_types import ConnectionType, CONNECTION_TYPES, TRANSFORM_SCHEMAS, get_connection_type, \
67
67
  list_connection_types
68
68
  from .product import ProductKind, Product, ProductComponent, BUILTIN_PRODUCT_KINDS, is_valid_product_key
69
+ from .inventory import (InventoryBalance, inventory_mode, INVENTORY_OPTION, INVENTORY_MODES,
70
+ INVENTORY_MOVEMENTS, INVENTORY_BALANCES, INVENTORY_STOCK,
71
+ PRODUCT_TAG, PRODUCT_MESSAGE_KEY)
69
72
  from .dashboard import Dashboard, DashboardType
70
73
  from .mobile_asset import MobileAssetTile, MobileAssetTileType, MobileAssetConfig
71
74
  from .graylog_log import GraylogLog
@@ -198,6 +198,11 @@ class BusinessType(Enum):
198
198
  - "event" represents a state or probability that have a start and stop timestamp.
199
199
  - "text" represents a time-series emitted as text/string value.
200
200
  - "scoped" represents a time-series with values split by a scope tag (e.g. anomaly probabilities per category).
201
+ - "inventoryMovement" represents stock movements of a storage twin: each point is a signed quantity
202
+ (+ in, - out) of one product, named by its ``productKey`` tag (Production Planner).
203
+
204
+ The order is the backend's: the SQL selector sends a value's position as the stored
205
+ integer, so a new value is always appended last.
201
206
  """
202
207
  TELEMETRY = "telemetry"
203
208
  SET_POINTS = "setPoint"
@@ -206,6 +211,7 @@ class BusinessType(Enum):
206
211
  EVENT = "event"
207
212
  TEXT = "text"
208
213
  SCOPED = "scoped"
214
+ INVENTORY_MOVEMENT = "inventoryMovement"
209
215
 
210
216
 
211
217
  class InputModeType(Enum):
@@ -0,0 +1,148 @@
1
+ """
2
+ Production Planner inventory (epic DEV-6044, release 12.2).
3
+
4
+ Two facts, and stock is derived from both:
5
+
6
+ - **Movements** are points on a DataPoint of business type ``inventoryMovement``, one
7
+ per storage Twin and category. Each point is a signed quantity (+ in, - out) in the
8
+ DataPoint's unit, and its ``productKey`` tag names the product.
9
+ - **Balances** (:class:`InventoryBalance`) are absolute levels counted at a moment
10
+ ("460 L of coke in the tank at 16:00"), stored in PostgreSQL on the same DataPoint.
11
+
12
+ A query chooses what it reads with ``options={"inventory": ...}``:
13
+
14
+ - ``movements`` (the default): the DataPoint's own points, one series per product.
15
+ - ``balances``: the recorded balances, one series per product.
16
+ - ``stock``: the level. Take the last balance before each moment, then add the
17
+ movements after it. A balance at T already counts every movement at or before T.
18
+ A product never counted starts from 0.
19
+
20
+ ``Request.products`` keeps only some products. Columns are named
21
+ ``<hardwareId>|<productKey>``.
22
+ """
23
+
24
+ from datetime import datetime, timezone
25
+ from typing import Optional
26
+
27
+ #: Request option that chooses what an inventoryMovement DataPoint returns.
28
+ INVENTORY_OPTION = "inventory"
29
+ INVENTORY_MOVEMENTS = "movements"
30
+ INVENTORY_BALANCES = "balances"
31
+ INVENTORY_STOCK = "stock"
32
+ INVENTORY_MODES = (INVENTORY_MOVEMENTS, INVENTORY_BALANCES, INVENTORY_STOCK)
33
+
34
+ #: Time-series tag that carries the product key on a movement point.
35
+ PRODUCT_TAG = "productKey"
36
+ #: Message key that carries it to the writers (``Scope``, ``EventType`` style).
37
+ PRODUCT_MESSAGE_KEY = "ProductKey"
38
+
39
+
40
+ def inventory_mode(options) -> str:
41
+ """The inventory mode a request's options ask for; ``movements`` when unset."""
42
+ mode = None
43
+ if isinstance(options, dict):
44
+ mode = options.get(INVENTORY_OPTION)
45
+ if mode is None:
46
+ return INVENTORY_MOVEMENTS
47
+ if mode not in INVENTORY_MODES:
48
+ raise ValueError(f"options.{INVENTORY_OPTION} must be one of {', '.join(INVENTORY_MODES)} "
49
+ f"but received {mode!r}"[:300])
50
+ return mode
51
+
52
+
53
+ def _ts_out(value):
54
+ if value is None:
55
+ return None
56
+ if value.tzinfo is None:
57
+ value = value.replace(tzinfo=timezone.utc)
58
+ return value.isoformat()
59
+
60
+
61
+ def _ts_in(value):
62
+ """A datetime from ISO 8601 or epoch milliseconds; anything else is kept as sent,
63
+ for the server to refuse with a message that names it."""
64
+ if value is None or isinstance(value, datetime):
65
+ return value
66
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
67
+ return datetime.fromtimestamp(value / 1000, tz=timezone.utc)
68
+ try:
69
+ return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
70
+ except ValueError:
71
+ return value
72
+
73
+
74
+ def _uuid_str(value):
75
+ if value is None or value == "":
76
+ return None
77
+ return str(value).lower()
78
+
79
+
80
+ class InventoryBalance:
81
+ """
82
+ An absolute stock level: ``quantity`` of ``product`` on the inventoryMovement
83
+ DataPoint ``datapoint`` (a hardware id), true at ``timestamp`` (when it was
84
+ counted, not when it was recorded: that is ``created_at``).
85
+
86
+ PostgreSQL ``"InventoryBalance"``, PK (``datapoint``, ``product``, ``timestamp``).
87
+ Writing the same key again replaces the quantity.
88
+
89
+ On write only:
90
+
91
+ - ``twin`` (a Twin hardware id) may replace ``datapoint``. The DataPoint is then
92
+ the Twin's inventoryMovement DataPoint of the product's category.
93
+ - ``unit`` (a backend unit id of the product's category) converts ``quantity`` to
94
+ the DataPoint's unit. It is stored in that unit, so a read never carries ``unit``.
95
+ """
96
+
97
+ @classmethod
98
+ def route(cls):
99
+ return "planner/inventory/balances"
100
+
101
+ def __init__(self, datapoint: str = None, product: str = None, timestamp=None,
102
+ quantity: float = None, unit=None, twin: str = None):
103
+ self.datapoint: Optional[str] = datapoint
104
+ self.product: Optional[str] = product
105
+ self.timestamp: Optional[datetime] = _ts_in(timestamp)
106
+ self.quantity: Optional[float] = quantity
107
+ self.unit: Optional[str] = _uuid_str(unit)
108
+ self.twin: Optional[str] = twin
109
+ self.created_at: Optional[datetime] = None
110
+ self.created_by: Optional[str] = None
111
+ self.updated_at: Optional[datetime] = None
112
+ self.updated_by: Optional[str] = None
113
+
114
+ def to_dict(self) -> dict:
115
+ obj = {}
116
+ for name, value in (("datapoint", self.datapoint), ("product", self.product),
117
+ ("quantity", self.quantity), ("unit", self.unit),
118
+ ("twin", self.twin), ("createdBy", self.created_by),
119
+ ("updatedBy", self.updated_by)):
120
+ if value is not None:
121
+ obj[name] = value
122
+ for name, value in (("timestamp", self.timestamp), ("createdAt", self.created_at),
123
+ ("updatedAt", self.updated_at)):
124
+ if value is not None:
125
+ obj[name] = _ts_out(value) if isinstance(value, datetime) else value
126
+ return obj
127
+
128
+ @classmethod
129
+ def from_dict(cls, data: dict) -> "InventoryBalance":
130
+ obj = cls()
131
+ obj.datapoint = data.get("datapoint")
132
+ obj.product = data.get("product")
133
+ obj.timestamp = _ts_in(data.get("timestamp"))
134
+ obj.quantity = data.get("quantity")
135
+ obj.unit = _uuid_str(data.get("unit"))
136
+ obj.twin = data.get("twin")
137
+ obj.created_at = _ts_in(data.get("createdAt") or data.get("created_at"))
138
+ obj.created_by = data.get("createdBy") or data.get("created_by")
139
+ obj.updated_at = _ts_in(data.get("updatedAt") or data.get("updated_at"))
140
+ obj.updated_by = data.get("updatedBy") or data.get("updated_by")
141
+ return obj
142
+
143
+ def key(self) -> tuple:
144
+ return self.datapoint, self.product, self.timestamp
145
+
146
+ def __repr__(self) -> str:
147
+ return (f"InventoryBalance(datapoint={self.datapoint!r}, product={self.product!r}, "
148
+ f"timestamp={self.timestamp!r}, quantity={self.quantity!r})")
@@ -164,6 +164,8 @@ class Request(ApiDto):
164
164
  :ivar uuid.UUID twin_id: technical id of wizata_dsapi.Twin registered on template_id.
165
165
  :ivar dict filters: filters to apply to the query pre-aggregation. nested dictionary using as first key=datapoint then value=another dictionary with key=operator (gt,lt,gte,lte,eq,neq) and value is the float value to compare too).
166
166
  :ivar dict options: dict representing query options.
167
+ :ivar list scopes: scope filters for SCOPED datapoints.
168
+ :ivar list products: product keys to keep on inventoryMovement datapoints (all when None).
167
169
  """
168
170
 
169
171
  @classmethod
@@ -198,7 +200,8 @@ class Request(ApiDto):
198
200
  field=None,
199
201
  bucket=None,
200
202
  tags=None,
201
- scopes=None):
203
+ scopes=None,
204
+ products=None):
202
205
  if request_id is None:
203
206
  request_id = uuid.uuid4()
204
207
  self.request_id = request_id
@@ -242,6 +245,7 @@ class Request(ApiDto):
242
245
  self.bucket = bucket
243
246
  self.tags = tags
244
247
  self.scopes = scopes
248
+ self.products = products
245
249
 
246
250
  self.null = null
247
251
 
@@ -623,6 +627,8 @@ class Request(ApiDto):
623
627
  query['tags'] = self.tags
624
628
  if self.scopes is not None:
625
629
  query['scopes'] = self.scopes
630
+ if self.products is not None:
631
+ query['products'] = self.products
626
632
 
627
633
  if self.target_feat is not None:
628
634
  query["target_feat"] = {
@@ -758,6 +764,14 @@ class Request(ApiDto):
758
764
  if "scopes" in json_data.keys():
759
765
  self.scopes = json_data["scopes"]
760
766
 
767
+ if "products" in json_data.keys():
768
+ products = json_data["products"]
769
+ if products is not None and (not isinstance(products, list) or not products
770
+ or not all(isinstance(p, str) for p in products)):
771
+ raise ValueError(f"'products' must be a non-empty list of product keys (leave it out "
772
+ f"for every product), got {products!r}"[:300])
773
+ self.products = products
774
+
761
775
  if "connections" in json_data.keys():
762
776
  self.connections = json_data["connections"]
763
777
 
@@ -0,0 +1 @@
1
+ __version__ = "2.2.0.dev7"
@@ -55,6 +55,7 @@ from .version import __version__
55
55
  import ast
56
56
  from .deployment import Deployment, DeploymentStatus
57
57
  from .product import ProductKind, Product, ProductComponent
58
+ from .inventory import InventoryBalance
58
59
  from .api_config import _registry
59
60
 
60
61
  from .streamlit_utils import get_streamlit_token, get_streamlit_domain
@@ -773,7 +774,8 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
773
774
  field=None,
774
775
  bucket: str = None,
775
776
  tags: dict = None,
776
- scopes: list = None) -> Request:
777
+ scopes: list = None,
778
+ products: list = None) -> Request:
777
779
  """
778
780
  Build the Request behind a time-series query.
779
781
 
@@ -816,6 +818,9 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
816
818
  if scopes is not None:
817
819
  request.scopes = scopes
818
820
 
821
+ if products is not None:
822
+ request.products = products
823
+
819
824
  if options is not None:
820
825
  request.options = options
821
826
 
@@ -847,7 +852,8 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
847
852
  field=None,
848
853
  bucket: str = None,
849
854
  tags: dict = None,
850
- scopes: list = None) -> pandas.DataFrame:
855
+ scopes: list = None,
856
+ products: list = None) -> pandas.DataFrame:
851
857
  """
852
858
  Query a dataframe from API.
853
859
  :param agg_method:
@@ -864,13 +870,15 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
864
870
  :param field: by default 'value' if none, accept str or list (e.g. value, reliability, eventId, ...)
865
871
  :param bucket: specify on which bucket the query applies.
866
872
  :param scopes: list of scope filters for SCOPED datapoints (e.g. ["normal", {"vibration": {"gte": 0.6}}]).
873
+ :param products: product keys to keep on inventoryMovement datapoints (all when None).
874
+ ``options={"inventory": "movements" | "balances" | "stock"}`` chooses what they return.
867
875
  :return: dataframe
868
876
  """
869
877
  return self._query(request=self._build_request(
870
878
  datapoints=datapoints, start=start, end=end, interval=interval,
871
879
  agg_method=agg_method, template=template, twin=twin, null=null,
872
880
  filters=filters, options=options, group=group, field=field,
873
- bucket=bucket, tags=tags, scopes=scopes))
881
+ bucket=bucket, tags=tags, scopes=scopes, products=products))
874
882
 
875
883
  def batch_query(self,
876
884
  step: timedelta,
@@ -2867,6 +2875,54 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
2867
2875
  "PUT", f"planner/products/{self._planner_key(product)}/components/", body=body)
2868
2876
  return [ProductComponent.from_dict(c) for c in response.json()]
2869
2877
 
2878
+ # Production Planner - inventory balances (epic DEV-6044)
2879
+
2880
+ def set_inventory_balances(self, balances: list) -> list:
2881
+ """
2882
+ Record absolute stock levels (a count, an ERP figure). Writing the same
2883
+ (datapoint, product, timestamp) again replaces the quantity.
2884
+
2885
+ :param balances: list of InventoryBalance (or dicts). Each names its
2886
+ inventoryMovement DataPoint by ``datapoint``, or by ``twin`` (the DataPoint
2887
+ of the product's category on that Twin). ``unit`` converts the quantity to
2888
+ the DataPoint's unit.
2889
+ :return: the stored balances, quantities in the DataPoint's unit.
2890
+ """
2891
+ body = [b.to_dict() if isinstance(b, InventoryBalance) else b for b in balances]
2892
+ response = self._planner_request("POST", "planner/inventory/balances/", body=body)
2893
+ return [InventoryBalance.from_dict(b) for b in response.json()]
2894
+
2895
+ def search_inventory_balances(self, search_query=None) -> PagedQueryResult:
2896
+ """
2897
+ List recorded balances, newest first by default.
2898
+
2899
+ :param search_query: SearchQuery or dict; filters on ``datapoint`` and
2900
+ ``product`` (eq / in) and ``timestamp`` (gte / gt / lt / lte).
2901
+ :return: PagedQueryResult with InventoryBalance results.
2902
+ """
2903
+ if search_query is None:
2904
+ search_query = SearchQuery()
2905
+ elif isinstance(search_query, dict):
2906
+ search_query = SearchQuery.from_dict(search_query)
2907
+ response = self._planner_request("POST", "planner/inventory/balances/search",
2908
+ body=search_query.to_dict())
2909
+ return PagedQueryResult.from_dict(response.json(), dto_class=InventoryBalance)
2910
+
2911
+ def delete_inventory_balance(self, datapoint: str, product: str, timestamp):
2912
+ """
2913
+ Delete one recorded balance (a count entered by mistake).
2914
+
2915
+ :param timestamp: datetime or ISO string, exactly as stored.
2916
+ """
2917
+ if isinstance(timestamp, datetime.datetime):
2918
+ if timestamp.tzinfo is None:
2919
+ timestamp = timestamp.replace(tzinfo=datetime.timezone.utc)
2920
+ timestamp = timestamp.isoformat()
2921
+ self._planner_request("DELETE", "planner/inventory/balances/",
2922
+ params={"datapoint": datapoint, "product": product,
2923
+ "timestamp": timestamp},
2924
+ expected=(204,))
2925
+
2870
2926
 
2871
2927
  def api() -> WizataDSAPIClient:
2872
2928
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wizata_dsapi
3
- Version: 2.2.0.dev5
3
+ Version: 2.2.0.dev7
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -28,6 +28,7 @@ wizata_dsapi/graylog_log.py
28
28
  wizata_dsapi/group_system.py
29
29
  wizata_dsapi/ilogger.py
30
30
  wizata_dsapi/insight.py
31
+ wizata_dsapi/inventory.py
31
32
  wizata_dsapi/mlmodel.py
32
33
  wizata_dsapi/mobile_asset.py
33
34
  wizata_dsapi/model_toolkit.py
@@ -1 +0,0 @@
1
- __version__ = "2.2.0.dev5"