wizata-dsapi 2.2.0.dev3__tar.gz → 2.2.0.dev6__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.dev3 → wizata_dsapi-2.2.0.dev6}/PKG-INFO +1 -1
  2. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/__init__.py +3 -0
  3. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/datapoint.py +6 -0
  4. wizata_dsapi-2.2.0.dev6/wizata_dsapi/inventory.py +148 -0
  5. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/request.py +29 -2
  6. wizata_dsapi-2.2.0.dev6/wizata_dsapi/version.py +1 -0
  7. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/wizata_dsapi_client.py +59 -3
  8. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi.egg-info/PKG-INFO +1 -1
  9. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi.egg-info/SOURCES.txt +1 -0
  10. wizata_dsapi-2.2.0.dev3/wizata_dsapi/version.py +0 -1
  11. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/LICENSE.txt +0 -0
  12. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/README.rst +0 -0
  13. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/setup.cfg +0 -0
  14. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/setup.py +0 -0
  15. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/api_config.py +0 -0
  16. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/api_dto.py +0 -0
  17. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/api_interface.py +0 -0
  18. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/bucket.py +0 -0
  19. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/business_label.py +0 -0
  20. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/connection.py +0 -0
  21. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/connection_types.py +0 -0
  22. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/context.py +0 -0
  23. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/dashboard.py +0 -0
  24. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/dataframe_toolkit.py +0 -0
  25. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/datastore.py +0 -0
  26. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/deployment.py +0 -0
  27. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/ds_dataframe.py +0 -0
  28. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/dsapi_json_encoder.py +0 -0
  29. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/edge_config.py +0 -0
  30. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/edge_device.py +0 -0
  31. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/edge_module.py +0 -0
  32. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/evaluation.py +0 -0
  33. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/execution.py +0 -0
  34. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/execution_log.py +0 -0
  35. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/experiment.py +0 -0
  36. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/graylog_log.py +0 -0
  37. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/group_system.py +0 -0
  38. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/ilogger.py +0 -0
  39. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/insight.py +0 -0
  40. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/mlmodel.py +0 -0
  41. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/mobile_asset.py +0 -0
  42. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/model_toolkit.py +0 -0
  43. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/models/__init__.py +0 -0
  44. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/models/common.py +0 -0
  45. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/notification.py +0 -0
  46. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/paged_query_result.py +0 -0
  47. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/pipeline.py +0 -0
  48. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/pipeline_image.py +0 -0
  49. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/plot.py +0 -0
  50. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/plots/__init__.py +0 -0
  51. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/plots/common.py +0 -0
  52. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/plots/theme.py +0 -0
  53. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/product.py +0 -0
  54. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/script.py +0 -0
  55. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/scripts/__init__.py +0 -0
  56. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/scripts/common.py +0 -0
  57. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/search.py +0 -0
  58. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/solution_component.py +0 -0
  59. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/streamlit_utils.py +0 -0
  60. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/template.py +0 -0
  61. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/template_config.py +0 -0
  62. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/trigger.py +0 -0
  63. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/twin.py +0 -0
  64. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/twinregistration.py +0 -0
  65. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/user.py +0 -0
  66. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/wizard_function.py +0 -0
  67. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/wizard_request.py +0 -0
  68. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi/words.py +0 -0
  69. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
  70. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/wizata_dsapi.egg-info/requires.txt +0 -0
  71. {wizata_dsapi-2.2.0.dev3 → wizata_dsapi-2.2.0.dev6}/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.dev3
3
+ Version: 2.2.0.dev6
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"] = {
@@ -644,6 +650,17 @@ class Request(ApiDto):
644
650
 
645
651
  return query
646
652
 
653
+ @staticmethod
654
+ def _object(value, section: str) -> dict:
655
+ """
656
+ A section of the body that must be a JSON object. Anything else is the caller's
657
+ mistake, refused as a ValueError naming it (the DS API answers 400) - it used to
658
+ reach ``.keys()`` and fail as an AttributeError, answered 500.
659
+ """
660
+ if not isinstance(value, dict):
661
+ raise ValueError(f"'{section}' must be an object, got {type(value).__name__}: {value!r}"[:300])
662
+ return value
663
+
647
664
  def from_json(self, json_data):
648
665
  if "id" in json_data.keys():
649
666
  self.request_id = uuid.UUID(json_data["id"])
@@ -654,6 +671,7 @@ class Request(ApiDto):
654
671
  datapoints = []
655
672
  if "equipments_list" in json_data.keys():
656
673
  for equipment in json_data["equipments_list"]:
674
+ self._object(equipment, "equipments_list")
657
675
  if "datapoints" not in equipment.keys():
658
676
  raise KeyError("no 'datapoints' have been provided for equipment with id '" +
659
677
  str(equipment["id"]) + "'")
@@ -674,6 +692,7 @@ class Request(ApiDto):
674
692
  self._datapoints = []
675
693
 
676
694
  if "timeframe" in json_data.keys():
695
+ self._object(json_data["timeframe"], "timeframe")
677
696
  if "start" not in json_data["timeframe"].keys():
678
697
  raise KeyError("No 'start time' have been selected, please set it up and re-try.")
679
698
 
@@ -695,6 +714,7 @@ class Request(ApiDto):
695
714
 
696
715
  if "aggregations" not in json_data.keys():
697
716
  raise KeyError("No 'aggregations' have been selected, please set it up and re-try.")
717
+ self._object(json_data["aggregations"], "aggregations")
698
718
 
699
719
  if "agg_method" not in json_data["aggregations"].keys():
700
720
  self.aggregation = None
@@ -744,6 +764,13 @@ class Request(ApiDto):
744
764
  if "scopes" in json_data.keys():
745
765
  self.scopes = json_data["scopes"]
746
766
 
767
+ if "products" in json_data.keys():
768
+ products = json_data["products"]
769
+ if products is not None and (not isinstance(products, list)
770
+ or not all(isinstance(p, str) for p in products)):
771
+ raise ValueError(f"'products' must be a list of product keys, got {products!r}"[:300])
772
+ self.products = products
773
+
747
774
  if "connections" in json_data.keys():
748
775
  self.connections = json_data["connections"]
749
776
 
@@ -751,7 +778,7 @@ class Request(ApiDto):
751
778
  self.null = json_data["null"]
752
779
 
753
780
  if "target_feat" in json_data.keys():
754
- self.target_feat = json_data["target_feat"]
781
+ self.target_feat = self._object(json_data["target_feat"], "target_feat")
755
782
  if "sensor" not in self.target_feat.keys():
756
783
  raise KeyError("No 'sensor' have been declared inside the target feature, this is a technical error.")
757
784
  if "operator" not in self.target_feat.keys():
@@ -0,0 +1 @@
1
+ __version__ = "2.2.0.dev6"
@@ -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.dev3
3
+ Version: 2.2.0.dev6
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.dev3"