wizata-dsapi 2.2.0.dev9__tar.gz → 2.2.0.dev11__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.
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/PKG-INFO +1 -1
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/__init__.py +2 -1
- wizata_dsapi-2.2.0.dev11/wizata_dsapi/inventory.py +313 -0
- wizata_dsapi-2.2.0.dev11/wizata_dsapi/version.py +1 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/wizata_dsapi_client.py +43 -1
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi.egg-info/PKG-INFO +1 -1
- wizata_dsapi-2.2.0.dev9/wizata_dsapi/inventory.py +0 -157
- wizata_dsapi-2.2.0.dev9/wizata_dsapi/version.py +0 -1
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/LICENSE.txt +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/README.rst +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/setup.cfg +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/setup.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/api_config.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/api_dto.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/api_interface.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/bucket.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/business_label.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/connection.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/connection_types.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/context.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/dashboard.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/dataframe_toolkit.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/datapoint.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/datastore.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/deployment.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/ds_dataframe.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/dsapi_json_encoder.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/edge_config.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/edge_device.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/edge_module.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/evaluation.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/execution.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/execution_log.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/experiment.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/graylog_log.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/group_system.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/ilogger.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/insight.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/mlmodel.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/mobile_asset.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/model_toolkit.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/models/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/models/common.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/notification.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/paged_query_result.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/pipeline.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/pipeline_image.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/plot.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/plots/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/plots/common.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/plots/theme.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/product.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/request.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/script.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/scripts/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/scripts/common.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/search.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/solution_component.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/streamlit_utils.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/template.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/template_config.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/trigger.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/twin.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/twinregistration.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/user.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/wizard_function.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/wizard_request.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi/words.py +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi.egg-info/SOURCES.txt +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi.egg-info/requires.txt +0 -0
- {wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi.egg-info/top_level.txt +0 -0
|
@@ -66,7 +66,8 @@ 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,
|
|
69
|
+
from .inventory import (InventoryBalance, InventoryStorage, InventoryStorageDatapoint, inventory_mode,
|
|
70
|
+
InventoryStock, InventoryStockProduct, InventoryStockLine, InventoryStockCount, INVENTORY_OPTION, INVENTORY_MODES,
|
|
70
71
|
INVENTORY_MOVEMENTS, INVENTORY_BALANCES, INVENTORY_STOCK,
|
|
71
72
|
PRODUCT_TAG, PRODUCT_MESSAGE_KEY)
|
|
72
73
|
from .dashboard import Dashboard, DashboardType
|
|
@@ -0,0 +1,313 @@
|
|
|
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 ``id`` (DEV-6085); (``datapoint``, ``product``,
|
|
87
|
+
``timestamp``) is unique, and writing the same key again replaces the quantity.
|
|
88
|
+
``count`` is the key shared by every balance written in one call (one count).
|
|
89
|
+
|
|
90
|
+
On write only:
|
|
91
|
+
|
|
92
|
+
- ``twin`` (a Twin hardware id) may replace ``datapoint``. The DataPoint is then
|
|
93
|
+
the Twin's inventoryMovement DataPoint of the product's category.
|
|
94
|
+
- ``unit`` (a backend unit id of the product's category) converts ``quantity`` to
|
|
95
|
+
the DataPoint's unit. It is stored in that unit, so a read never carries ``unit``.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def route(cls):
|
|
100
|
+
return "planner/inventory/balances"
|
|
101
|
+
|
|
102
|
+
def __init__(self, datapoint: str = None, product: str = None, timestamp=None,
|
|
103
|
+
quantity: float = None, unit=None, twin: str = None, balance_id=None, count=None):
|
|
104
|
+
self.id: Optional[str] = _uuid_str(balance_id)
|
|
105
|
+
self.count: Optional[str] = _uuid_str(count)
|
|
106
|
+
self.datapoint: Optional[str] = datapoint
|
|
107
|
+
self.product: Optional[str] = product
|
|
108
|
+
self.timestamp: Optional[datetime] = _ts_in(timestamp)
|
|
109
|
+
self.quantity: Optional[float] = quantity
|
|
110
|
+
self.unit: Optional[str] = _uuid_str(unit)
|
|
111
|
+
self.twin: Optional[str] = twin
|
|
112
|
+
self.created_at: Optional[datetime] = None
|
|
113
|
+
self.created_by: Optional[str] = None
|
|
114
|
+
self.updated_at: Optional[datetime] = None
|
|
115
|
+
self.updated_by: Optional[str] = None
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict:
|
|
118
|
+
obj = {}
|
|
119
|
+
for name, value in (("id", self.id), ("count", self.count),
|
|
120
|
+
("datapoint", self.datapoint), ("product", self.product),
|
|
121
|
+
("quantity", self.quantity), ("unit", self.unit),
|
|
122
|
+
("twin", self.twin), ("createdBy", self.created_by),
|
|
123
|
+
("updatedBy", self.updated_by)):
|
|
124
|
+
if value is not None:
|
|
125
|
+
obj[name] = value
|
|
126
|
+
for name, value in (("timestamp", self.timestamp), ("createdAt", self.created_at),
|
|
127
|
+
("updatedAt", self.updated_at)):
|
|
128
|
+
if value is not None:
|
|
129
|
+
obj[name] = _ts_out(value) if isinstance(value, datetime) else value
|
|
130
|
+
return obj
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_dict(cls, data: dict) -> "InventoryBalance":
|
|
134
|
+
obj = cls()
|
|
135
|
+
obj.id = _uuid_str(data.get("id"))
|
|
136
|
+
obj.count = _uuid_str(data.get("count"))
|
|
137
|
+
obj.datapoint = data.get("datapoint")
|
|
138
|
+
obj.product = data.get("product")
|
|
139
|
+
obj.timestamp = _ts_in(data.get("timestamp"))
|
|
140
|
+
obj.quantity = data.get("quantity")
|
|
141
|
+
obj.unit = _uuid_str(data.get("unit"))
|
|
142
|
+
obj.twin = data.get("twin")
|
|
143
|
+
obj.created_at = _ts_in(data.get("createdAt") or data.get("created_at"))
|
|
144
|
+
obj.created_by = data.get("createdBy") or data.get("created_by")
|
|
145
|
+
obj.updated_at = _ts_in(data.get("updatedAt") or data.get("updated_at"))
|
|
146
|
+
obj.updated_by = data.get("updatedBy") or data.get("updated_by")
|
|
147
|
+
return obj
|
|
148
|
+
|
|
149
|
+
def api_id(self) -> str:
|
|
150
|
+
return self.id
|
|
151
|
+
|
|
152
|
+
def key(self) -> tuple:
|
|
153
|
+
return self.datapoint, self.product, self.timestamp
|
|
154
|
+
|
|
155
|
+
def __repr__(self) -> str:
|
|
156
|
+
return (f"InventoryBalance(datapoint={self.datapoint!r}, product={self.product!r}, "
|
|
157
|
+
f"timestamp={self.timestamp!r}, quantity={self.quantity!r})")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class InventoryStorageDatapoint:
|
|
161
|
+
"""One inventoryMovement DataPoint of a storage: its category and unit (DEV-6086)."""
|
|
162
|
+
|
|
163
|
+
def __init__(self, hardware_id: str = None, name: str = None, category=None, unit=None,
|
|
164
|
+
unit_symbol: str = None):
|
|
165
|
+
self.hardware_id: Optional[str] = hardware_id
|
|
166
|
+
self.name: Optional[str] = name
|
|
167
|
+
self.category: Optional[str] = _uuid_str(category)
|
|
168
|
+
self.unit: Optional[str] = _uuid_str(unit)
|
|
169
|
+
self.unit_symbol: Optional[str] = unit_symbol
|
|
170
|
+
|
|
171
|
+
def to_dict(self) -> dict:
|
|
172
|
+
return {"hardwareId": self.hardware_id, "name": self.name, "category": self.category,
|
|
173
|
+
"unit": self.unit, "unitSymbol": self.unit_symbol}
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def from_dict(cls, data: dict) -> "InventoryStorageDatapoint":
|
|
177
|
+
return cls(hardware_id=data.get("hardwareId") or data.get("hardware_id"), name=data.get("name"),
|
|
178
|
+
category=data.get("category"), unit=data.get("unit"),
|
|
179
|
+
unit_symbol=data.get("unitSymbol") or data.get("unit_symbol"))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class InventoryStorage:
|
|
183
|
+
"""
|
|
184
|
+
A storage place: a Twin holding at least one inventoryMovement DataPoint (DEV-6086).
|
|
185
|
+
Read only, from ``POST /dsapi/planner/inventory/storages/search``.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, twin_id=None, twin_hardware_id: str = None, twin_name: str = None,
|
|
189
|
+
datapoints: list = None):
|
|
190
|
+
self.twin_id: Optional[str] = _uuid_str(twin_id)
|
|
191
|
+
self.twin_hardware_id: Optional[str] = twin_hardware_id
|
|
192
|
+
self.twin_name: Optional[str] = twin_name
|
|
193
|
+
self.datapoints: list = list(datapoints or [])
|
|
194
|
+
|
|
195
|
+
def to_dict(self) -> dict:
|
|
196
|
+
return {"twin": {"id": self.twin_id, "hardwareId": self.twin_hardware_id, "name": self.twin_name},
|
|
197
|
+
"datapoints": [d.to_dict() for d in self.datapoints]}
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def from_dict(cls, data: dict) -> "InventoryStorage":
|
|
201
|
+
twin = data.get("twin") or {}
|
|
202
|
+
return cls(twin_id=twin.get("id"), twin_hardware_id=twin.get("hardwareId"), twin_name=twin.get("name"),
|
|
203
|
+
datapoints=[InventoryStorageDatapoint.from_dict(d) for d in data.get("datapoints") or []])
|
|
204
|
+
|
|
205
|
+
def __repr__(self) -> str:
|
|
206
|
+
return f"InventoryStorage({self.twin_hardware_id!r}, {[d.hardware_id for d in self.datapoints]})"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class InventoryStockCount:
|
|
210
|
+
"""The last count behind a stock line: the balance it starts from (DEV-6088)."""
|
|
211
|
+
|
|
212
|
+
def __init__(self, balance_id=None, count=None, timestamp=None, quantity: float = None, by: str = None):
|
|
213
|
+
self.id: Optional[str] = _uuid_str(balance_id)
|
|
214
|
+
self.count: Optional[str] = _uuid_str(count)
|
|
215
|
+
self.timestamp: Optional[datetime] = _ts_in(timestamp)
|
|
216
|
+
self.quantity: Optional[float] = quantity
|
|
217
|
+
self.by: Optional[str] = by
|
|
218
|
+
|
|
219
|
+
def to_dict(self) -> dict:
|
|
220
|
+
return {"id": self.id, "count": self.count,
|
|
221
|
+
"timestamp": _ts_out(self.timestamp) if isinstance(self.timestamp, datetime) else self.timestamp,
|
|
222
|
+
"quantity": self.quantity, "by": self.by}
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def from_dict(cls, data: dict) -> "InventoryStockCount":
|
|
226
|
+
return cls(balance_id=data.get("id"), count=data.get("count"), timestamp=data.get("timestamp"),
|
|
227
|
+
quantity=data.get("quantity"), by=data.get("by"))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class InventoryStockLine:
|
|
231
|
+
"""
|
|
232
|
+
One product on one storage datapoint at a moment: ``quantity`` in the datapoint's unit,
|
|
233
|
+
``last_count`` (None when never counted) and ``moved_since`` it (DEV-6088).
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def __init__(self, twin_id=None, twin_hardware_id: str = None, twin_name: str = None, datapoint: str = None,
|
|
237
|
+
unit=None, unit_symbol: str = None, quantity: float = None, moved_since: float = None,
|
|
238
|
+
last_count: InventoryStockCount = None):
|
|
239
|
+
self.twin_id: Optional[str] = _uuid_str(twin_id)
|
|
240
|
+
self.twin_hardware_id: Optional[str] = twin_hardware_id
|
|
241
|
+
self.twin_name: Optional[str] = twin_name
|
|
242
|
+
self.datapoint: Optional[str] = datapoint
|
|
243
|
+
self.unit: Optional[str] = _uuid_str(unit)
|
|
244
|
+
self.unit_symbol: Optional[str] = unit_symbol
|
|
245
|
+
self.quantity: Optional[float] = quantity
|
|
246
|
+
self.moved_since: Optional[float] = moved_since
|
|
247
|
+
self.last_count: Optional[InventoryStockCount] = last_count
|
|
248
|
+
|
|
249
|
+
def to_dict(self) -> dict:
|
|
250
|
+
return {"twin": {"id": self.twin_id, "hardwareId": self.twin_hardware_id, "name": self.twin_name},
|
|
251
|
+
"datapoint": self.datapoint, "unit": self.unit, "unitSymbol": self.unit_symbol,
|
|
252
|
+
"quantity": self.quantity, "movedSince": self.moved_since,
|
|
253
|
+
"lastCount": self.last_count.to_dict() if self.last_count is not None else None}
|
|
254
|
+
|
|
255
|
+
@classmethod
|
|
256
|
+
def from_dict(cls, data: dict) -> "InventoryStockLine":
|
|
257
|
+
twin = data.get("twin") or {}
|
|
258
|
+
last = data.get("lastCount")
|
|
259
|
+
return cls(twin_id=twin.get("id"), twin_hardware_id=twin.get("hardwareId"), twin_name=twin.get("name"),
|
|
260
|
+
datapoint=data.get("datapoint"), unit=data.get("unit"), unit_symbol=data.get("unitSymbol"),
|
|
261
|
+
quantity=data.get("quantity"), moved_since=data.get("movedSince"),
|
|
262
|
+
last_count=InventoryStockCount.from_dict(last) if last else None)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class InventoryStockProduct:
|
|
266
|
+
"""
|
|
267
|
+
A product's stock at a moment: one line per storage holding it, and a ``total`` in
|
|
268
|
+
``total_unit``: the product's default unit, else the unit most of its storages use
|
|
269
|
+
(DEV-6088). ``total`` is None when a line's unit cannot be converted.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
def __init__(self, key: str = None, name: str = None, kind: str = None, category=None, unit=None,
|
|
273
|
+
unit_symbol: str = None, total: float = None, total_unit=None, total_unit_symbol: str = None,
|
|
274
|
+
storages: list = None):
|
|
275
|
+
self.key: Optional[str] = key
|
|
276
|
+
self.name: Optional[str] = name
|
|
277
|
+
self.kind: Optional[str] = kind
|
|
278
|
+
self.category: Optional[str] = _uuid_str(category)
|
|
279
|
+
self.unit: Optional[str] = _uuid_str(unit)
|
|
280
|
+
self.unit_symbol: Optional[str] = unit_symbol
|
|
281
|
+
self.total: Optional[float] = total
|
|
282
|
+
self.total_unit: Optional[str] = _uuid_str(total_unit)
|
|
283
|
+
self.total_unit_symbol: Optional[str] = total_unit_symbol
|
|
284
|
+
self.storages: list = list(storages or [])
|
|
285
|
+
|
|
286
|
+
def to_dict(self) -> dict:
|
|
287
|
+
return {"key": self.key, "name": self.name, "kind": self.kind, "category": self.category,
|
|
288
|
+
"unit": self.unit, "unitSymbol": self.unit_symbol, "total": self.total,
|
|
289
|
+
"totalUnit": self.total_unit, "totalUnitSymbol": self.total_unit_symbol,
|
|
290
|
+
"storages": [s.to_dict() for s in self.storages]}
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def from_dict(cls, data: dict) -> "InventoryStockProduct":
|
|
294
|
+
return cls(key=data.get("key"), name=data.get("name"), kind=data.get("kind"), category=data.get("category"),
|
|
295
|
+
unit=data.get("unit"), unit_symbol=data.get("unitSymbol"), total=data.get("total"),
|
|
296
|
+
total_unit=data.get("totalUnit"), total_unit_symbol=data.get("totalUnitSymbol"),
|
|
297
|
+
storages=[InventoryStockLine.from_dict(s) for s in data.get("storages") or []])
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class InventoryStock:
|
|
301
|
+
"""Stock of every product × storage at ``as_of`` (``POST /dsapi/planner/inventory/stock``, DEV-6088)."""
|
|
302
|
+
|
|
303
|
+
def __init__(self, as_of=None, products: list = None):
|
|
304
|
+
self.as_of: Optional[datetime] = _ts_in(as_of)
|
|
305
|
+
self.products: list = list(products or [])
|
|
306
|
+
|
|
307
|
+
def to_dict(self) -> dict:
|
|
308
|
+
return {"asOf": _ts_out(self.as_of) if isinstance(self.as_of, datetime) else self.as_of,
|
|
309
|
+
"products": [p.to_dict() for p in self.products]}
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def from_dict(cls, data: dict) -> "InventoryStock":
|
|
313
|
+
return cls(as_of=data.get("asOf"), products=[InventoryStockProduct.from_dict(p) for p in data.get("products") or []])
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.2.0.dev11"
|
|
@@ -55,7 +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
|
+
from .inventory import InventoryBalance, InventoryStorage, InventoryStock
|
|
59
59
|
from .api_config import _registry
|
|
60
60
|
|
|
61
61
|
from .streamlit_utils import get_streamlit_token, get_streamlit_domain
|
|
@@ -2909,6 +2909,48 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
|
|
|
2909
2909
|
body=search_query.to_dict())
|
|
2910
2910
|
return PagedQueryResult.from_dict(response.json(), dto_class=InventoryBalance)
|
|
2911
2911
|
|
|
2912
|
+
def get_inventory_stock(self, as_of=None, twins: list = None, products: list = None,
|
|
2913
|
+
kinds: list = None) -> InventoryStock:
|
|
2914
|
+
"""
|
|
2915
|
+
Stock of every product × storage at a moment (DEV-6088): per storage datapoint the
|
|
2916
|
+
quantity in its unit, the last count and what moved since; per product a total in
|
|
2917
|
+
its default unit (else the unit most of its storages use).
|
|
2918
|
+
|
|
2919
|
+
:param as_of: datetime or ISO string; default now. Events at that moment count.
|
|
2920
|
+
:param twins: storage twins (hardware ids or ids) to keep.
|
|
2921
|
+
:param products: product keys to keep.
|
|
2922
|
+
:param kinds: product kind keys to keep.
|
|
2923
|
+
"""
|
|
2924
|
+
body = {}
|
|
2925
|
+
if as_of is not None:
|
|
2926
|
+
if isinstance(as_of, datetime.datetime):
|
|
2927
|
+
if as_of.tzinfo is None:
|
|
2928
|
+
as_of = as_of.replace(tzinfo=datetime.timezone.utc)
|
|
2929
|
+
as_of = as_of.isoformat()
|
|
2930
|
+
body["asOf"] = as_of
|
|
2931
|
+
for name, value in (("twins", twins), ("products", products), ("kinds", kinds)):
|
|
2932
|
+
if value is not None:
|
|
2933
|
+
body[name] = list(value)
|
|
2934
|
+
response = self._planner_request("POST", "planner/inventory/stock", body=body)
|
|
2935
|
+
return InventoryStock.from_dict(response.json())
|
|
2936
|
+
|
|
2937
|
+
def search_inventory_storages(self, search_query=None) -> PagedQueryResult:
|
|
2938
|
+
"""
|
|
2939
|
+
The storage places: Twins holding an inventoryMovement DataPoint, each with those
|
|
2940
|
+
datapoints' category, unit and unit symbol (DEV-6086).
|
|
2941
|
+
|
|
2942
|
+
:param search_query: SearchQuery or dict; filters ``twin`` (hardware id or id) and
|
|
2943
|
+
``category`` (eq / in), ``_search`` (``{"query": text}`` on names and hardware ids).
|
|
2944
|
+
:return: PagedQueryResult with InventoryStorage results, sorted by twin name.
|
|
2945
|
+
"""
|
|
2946
|
+
if search_query is None:
|
|
2947
|
+
search_query = SearchQuery()
|
|
2948
|
+
elif isinstance(search_query, dict):
|
|
2949
|
+
search_query = SearchQuery.from_dict(search_query)
|
|
2950
|
+
response = self._planner_request("POST", "planner/inventory/storages/search",
|
|
2951
|
+
body=search_query.to_dict())
|
|
2952
|
+
return PagedQueryResult.from_dict(response.json(), dto_class=InventoryStorage)
|
|
2953
|
+
|
|
2912
2954
|
@staticmethod
|
|
2913
2955
|
def _balance_id(value) -> str:
|
|
2914
2956
|
balance_id = value.id if isinstance(value, InventoryBalance) else value
|
|
@@ -1,157 +0,0 @@
|
|
|
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 ``id`` (DEV-6085); (``datapoint``, ``product``,
|
|
87
|
-
``timestamp``) is unique, and writing the same key again replaces the quantity.
|
|
88
|
-
``count`` is the key shared by every balance written in one call (one count).
|
|
89
|
-
|
|
90
|
-
On write only:
|
|
91
|
-
|
|
92
|
-
- ``twin`` (a Twin hardware id) may replace ``datapoint``. The DataPoint is then
|
|
93
|
-
the Twin's inventoryMovement DataPoint of the product's category.
|
|
94
|
-
- ``unit`` (a backend unit id of the product's category) converts ``quantity`` to
|
|
95
|
-
the DataPoint's unit. It is stored in that unit, so a read never carries ``unit``.
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
@classmethod
|
|
99
|
-
def route(cls):
|
|
100
|
-
return "planner/inventory/balances"
|
|
101
|
-
|
|
102
|
-
def __init__(self, datapoint: str = None, product: str = None, timestamp=None,
|
|
103
|
-
quantity: float = None, unit=None, twin: str = None, balance_id=None, count=None):
|
|
104
|
-
self.id: Optional[str] = _uuid_str(balance_id)
|
|
105
|
-
self.count: Optional[str] = _uuid_str(count)
|
|
106
|
-
self.datapoint: Optional[str] = datapoint
|
|
107
|
-
self.product: Optional[str] = product
|
|
108
|
-
self.timestamp: Optional[datetime] = _ts_in(timestamp)
|
|
109
|
-
self.quantity: Optional[float] = quantity
|
|
110
|
-
self.unit: Optional[str] = _uuid_str(unit)
|
|
111
|
-
self.twin: Optional[str] = twin
|
|
112
|
-
self.created_at: Optional[datetime] = None
|
|
113
|
-
self.created_by: Optional[str] = None
|
|
114
|
-
self.updated_at: Optional[datetime] = None
|
|
115
|
-
self.updated_by: Optional[str] = None
|
|
116
|
-
|
|
117
|
-
def to_dict(self) -> dict:
|
|
118
|
-
obj = {}
|
|
119
|
-
for name, value in (("id", self.id), ("count", self.count),
|
|
120
|
-
("datapoint", self.datapoint), ("product", self.product),
|
|
121
|
-
("quantity", self.quantity), ("unit", self.unit),
|
|
122
|
-
("twin", self.twin), ("createdBy", self.created_by),
|
|
123
|
-
("updatedBy", self.updated_by)):
|
|
124
|
-
if value is not None:
|
|
125
|
-
obj[name] = value
|
|
126
|
-
for name, value in (("timestamp", self.timestamp), ("createdAt", self.created_at),
|
|
127
|
-
("updatedAt", self.updated_at)):
|
|
128
|
-
if value is not None:
|
|
129
|
-
obj[name] = _ts_out(value) if isinstance(value, datetime) else value
|
|
130
|
-
return obj
|
|
131
|
-
|
|
132
|
-
@classmethod
|
|
133
|
-
def from_dict(cls, data: dict) -> "InventoryBalance":
|
|
134
|
-
obj = cls()
|
|
135
|
-
obj.id = _uuid_str(data.get("id"))
|
|
136
|
-
obj.count = _uuid_str(data.get("count"))
|
|
137
|
-
obj.datapoint = data.get("datapoint")
|
|
138
|
-
obj.product = data.get("product")
|
|
139
|
-
obj.timestamp = _ts_in(data.get("timestamp"))
|
|
140
|
-
obj.quantity = data.get("quantity")
|
|
141
|
-
obj.unit = _uuid_str(data.get("unit"))
|
|
142
|
-
obj.twin = data.get("twin")
|
|
143
|
-
obj.created_at = _ts_in(data.get("createdAt") or data.get("created_at"))
|
|
144
|
-
obj.created_by = data.get("createdBy") or data.get("created_by")
|
|
145
|
-
obj.updated_at = _ts_in(data.get("updatedAt") or data.get("updated_at"))
|
|
146
|
-
obj.updated_by = data.get("updatedBy") or data.get("updated_by")
|
|
147
|
-
return obj
|
|
148
|
-
|
|
149
|
-
def api_id(self) -> str:
|
|
150
|
-
return self.id
|
|
151
|
-
|
|
152
|
-
def key(self) -> tuple:
|
|
153
|
-
return self.datapoint, self.product, self.timestamp
|
|
154
|
-
|
|
155
|
-
def __repr__(self) -> str:
|
|
156
|
-
return (f"InventoryBalance(datapoint={self.datapoint!r}, product={self.product!r}, "
|
|
157
|
-
f"timestamp={self.timestamp!r}, quantity={self.quantity!r})")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.2.0.dev9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wizata_dsapi-2.2.0.dev9 → wizata_dsapi-2.2.0.dev11}/wizata_dsapi.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|