wizata-dsapi 2.2.0.dev29__tar.gz → 2.2.0.dev31__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 (78) hide show
  1. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/PKG-INFO +1 -1
  2. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/__init__.py +1 -0
  3. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/connection.py +33 -0
  4. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/connection_types.py +13 -0
  5. wizata_dsapi-2.2.0.dev31/wizata_dsapi/inventory_availability.py +138 -0
  6. wizata_dsapi-2.2.0.dev31/wizata_dsapi/version.py +1 -0
  7. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/wizata_dsapi_client.py +33 -0
  8. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi.egg-info/PKG-INFO +1 -1
  9. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi.egg-info/SOURCES.txt +1 -0
  10. wizata_dsapi-2.2.0.dev29/wizata_dsapi/version.py +0 -1
  11. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/LICENSE.txt +0 -0
  12. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/README.rst +0 -0
  13. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/setup.cfg +0 -0
  14. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/setup.py +0 -0
  15. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/api_config.py +0 -0
  16. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/api_dto.py +0 -0
  17. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/api_interface.py +0 -0
  18. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/bucket.py +0 -0
  19. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/business_label.py +0 -0
  20. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/connection_discovery.py +0 -0
  21. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/connection_policy.py +0 -0
  22. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/context.py +0 -0
  23. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/dashboard.py +0 -0
  24. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/dataframe_toolkit.py +0 -0
  25. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/datapoint.py +0 -0
  26. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/datastore.py +0 -0
  27. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/deployment.py +0 -0
  28. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/ds_dataframe.py +0 -0
  29. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/dsapi_json_encoder.py +0 -0
  30. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/edge_config.py +0 -0
  31. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/edge_device.py +0 -0
  32. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/edge_module.py +0 -0
  33. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/edge_package.py +0 -0
  34. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/evaluation.py +0 -0
  35. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/execution.py +0 -0
  36. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/execution_log.py +0 -0
  37. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/experiment.py +0 -0
  38. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/formula.py +0 -0
  39. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/graylog_log.py +0 -0
  40. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/group_system.py +0 -0
  41. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/ilogger.py +0 -0
  42. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/insight.py +0 -0
  43. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/inventory.py +0 -0
  44. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/mlmodel.py +0 -0
  45. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/mobile_asset.py +0 -0
  46. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/model_toolkit.py +0 -0
  47. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/models/__init__.py +0 -0
  48. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/models/common.py +0 -0
  49. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/notification.py +0 -0
  50. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/paged_query_result.py +0 -0
  51. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/pipeline.py +0 -0
  52. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/pipeline_image.py +0 -0
  53. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/planned_movement.py +0 -0
  54. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/plot.py +0 -0
  55. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/plots/__init__.py +0 -0
  56. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/plots/common.py +0 -0
  57. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/plots/theme.py +0 -0
  58. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/product.py +0 -0
  59. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/python_package.py +0 -0
  60. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/request.py +0 -0
  61. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/script.py +0 -0
  62. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/scripts/__init__.py +0 -0
  63. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/scripts/common.py +0 -0
  64. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/search.py +0 -0
  65. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/solution_component.py +0 -0
  66. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/streamlit_utils.py +0 -0
  67. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/template.py +0 -0
  68. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/template_config.py +0 -0
  69. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/trigger.py +0 -0
  70. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/twin.py +0 -0
  71. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/twinregistration.py +0 -0
  72. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/user.py +0 -0
  73. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/wizard_function.py +0 -0
  74. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/wizard_request.py +0 -0
  75. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi/words.py +0 -0
  76. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
  77. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/wizata_dsapi.egg-info/requires.txt +0 -0
  78. {wizata_dsapi-2.2.0.dev29 → wizata_dsapi-2.2.0.dev31}/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.dev29
3
+ Version: 2.2.0.dev31
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -84,6 +84,7 @@ from .inventory import (InventoryBalance, InventoryStorage, InventoryStorageData
84
84
  from .planned_movement import (PlannedMovement, PlannedMovementSet, PLANNED_MOVEMENT_TYPES,
85
85
  PLANNED_MOVEMENT_SNAPSHOT_TYPES, PLANNED_MOVEMENT_DATED_TYPES,
86
86
  planned_movement_direction, is_dated_planned_movement)
87
+ from .inventory_availability import InventoryAvailability, InventoryAvailabilityProduct, InventoryProjectionDay
87
88
  from .dashboard import Dashboard, DashboardType
88
89
  from .mobile_asset import MobileAssetTile, MobileAssetTileType, MobileAssetConfig
89
90
  from .graylog_log import GraylogLog
@@ -79,6 +79,9 @@ class Connection:
79
79
  # read-only summaries a search adds: [ConnectionChannel], without their rows
80
80
  self.readers: Optional[list] = None
81
81
  self.writers: Optional[list] = None
82
+ # read-only, set by ConnectionClient.mark_in_use (DEV-6165): whether its device runs
83
+ # it. False for an Edge store kept on a device that left Connectivity + Pipelines.
84
+ self.in_use: Optional[bool] = None
82
85
 
83
86
  @property
84
87
  def system(self) -> bool:
@@ -113,6 +116,8 @@ class Connection:
113
116
  if self.edge_device_id is not None:
114
117
  obj["edgeDeviceId"] = self.edge_device_id
115
118
  obj["system"] = self.system
119
+ if self.in_use is not None:
120
+ obj["inUse"] = self.in_use
116
121
  obj["config"] = dict(self.config or {})
117
122
  if include_secrets:
118
123
  obj["secrets"] = dict(self.secrets or {})
@@ -180,6 +185,10 @@ class ConnectionChannel:
180
185
  - ``signals``: on create only, the policy rows to write with it (read back from search).
181
186
  - ``signal_count`` / ``mapped_count``: read-only, how many rows and how many name a
182
187
  datapoint.
188
+ - ``received`` / ``dropped`` / ``last_seen`` / ``in_sync``: read-only, a reader's summary
189
+ of its seen names (DEV-6163): names received, dropped under the stored policy, the
190
+ newest time any was seen, and whether the edge runs the stored policy (None until it
191
+ has reported).
183
192
  """
184
193
 
185
194
  def __init__(self):
@@ -196,6 +205,10 @@ class ConnectionChannel:
196
205
  self.signals: Optional[list] = None
197
206
  self.signal_count: Optional[int] = None
198
207
  self.mapped_count: Optional[int] = None
208
+ self.received: Optional[int] = None
209
+ self.dropped: Optional[int] = None
210
+ self.last_seen: Optional[datetime] = None
211
+ self.in_sync: Optional[bool] = None
199
212
  self.created_at: Optional[datetime] = None
200
213
  self.updated_at: Optional[datetime] = None
201
214
 
@@ -226,6 +239,14 @@ class ConnectionChannel:
226
239
  obj["signalCount"] = self.signal_count
227
240
  if self.mapped_count is not None:
228
241
  obj["mappedCount"] = self.mapped_count
242
+ if self.received is not None:
243
+ obj["received"] = self.received
244
+ if self.dropped is not None:
245
+ obj["dropped"] = self.dropped
246
+ if self.last_seen is not None:
247
+ obj["lastSeen"] = _ts_out(self.last_seen)
248
+ if self.in_sync is not None:
249
+ obj["inSync"] = self.in_sync
229
250
  if self.created_at is not None:
230
251
  obj["createdAt"] = _ts_out(self.created_at)
231
252
  if self.updated_at is not None:
@@ -291,6 +312,9 @@ class ConnectionSignal:
291
312
  node, the name written). In a ``list`` policy a row with no ``tag`` lets the
292
313
  datapoint through.
293
314
  - A row with both always renames, whatever the mode.
315
+ - ``datapoint`` / ``status`` / ``last_received``: read-only, what a search with
316
+ ``include`` adds (DEV-6164): the datapoint the row produces, what becomes of it under
317
+ the device's cloud and edge writers, and when a reader last received its name.
294
318
  """
295
319
 
296
320
  def __init__(self, tag: str = None, hardware_id: str = None):
@@ -300,6 +324,9 @@ class ConnectionSignal:
300
324
  self.hardware_id: Optional[str] = hardware_id
301
325
  self.created_at: Optional[datetime] = None
302
326
  self.updated_at: Optional[datetime] = None
327
+ self.datapoint: Optional[str] = None
328
+ self.status: Optional[str] = None
329
+ self.last_received: Optional[datetime] = None
303
330
 
304
331
  def to_dict(self) -> dict:
305
332
  obj = {}
@@ -309,6 +336,12 @@ class ConnectionSignal:
309
336
  obj["channelId"] = str(self.channel_id)
310
337
  obj["tag"] = self.tag
311
338
  obj["hardwareId"] = self.hardware_id
339
+ if self.datapoint is not None:
340
+ obj["datapoint"] = self.datapoint
341
+ if self.status is not None:
342
+ obj["status"] = self.status
343
+ if self.last_received is not None:
344
+ obj["lastReceived"] = _ts_out(self.last_received)
312
345
  if self.created_at is not None:
313
346
  obj["createdAt"] = _ts_out(self.created_at)
314
347
  if self.updated_at is not None:
@@ -126,6 +126,19 @@ class ConnectionType:
126
126
  def directions(self) -> list:
127
127
  return ([_READ] if self.reader else []) + ([_WRITE] if self.writer else [])
128
128
 
129
+ def exists_on(self, device_type) -> bool:
130
+ """
131
+ Whether a device of ``device_type`` runs a connection of this type (DEV-6165). A type
132
+ that lists ``device_types`` exists on those only: the Edge store is kept on a device
133
+ that leaves Connectivity + Pipelines, but not rendered. The render and the payload's
134
+ ``inUse`` both read this.
135
+
136
+ :param device_type: an ``EdgeDeviceType`` or its value; None (unknown) counts as every type.
137
+ """
138
+ if not self.device_types or device_type is None:
139
+ return True
140
+ return getattr(device_type, "value", device_type) in self.device_types
141
+
129
142
  def channel(self, direction) -> Optional[ChannelType]:
130
143
  direction = getattr(direction, "value", direction)
131
144
  return self.reader if direction == _READ else self.writer if direction == _WRITE else None
@@ -0,0 +1,138 @@
1
+ """
2
+ Production Planner availability (DEV-6137, epic DEV-6044, release 12.2).
3
+
4
+ What a facility can still promise, per product, and how its stock is projected over the
5
+ next days. Computed on read from the stock (balances + movements) and the planned
6
+ movements (DEV-6136); nothing here is stored.
7
+
8
+ - ``total``: stock under the facility, read at the moment of the last availability
9
+ snapshot written (``snapshot_as_of``), so the figures below hold at one moment and ATP
10
+ matches the source's.
11
+ - ``allocated``, ``hold``, ``open_demand``: the snapshot's figures (``allocated``,
12
+ ``hold``, ``openDemand`` planned movements). Without a snapshot they come from the open
13
+ customer order lines (picked not shipped, ordered not picked) and ``from_snapshot`` is
14
+ False.
15
+ - ``available`` = total - allocated - hold; ``atp`` = available - open demand.
16
+ - ``dated``: the unpicked part of the open customer order lines, capped at the open
17
+ demand (lines are read more often than the snapshot); ``undated`` = open demand - dated,
18
+ due at once.
19
+ - ``projection``: one entry per day of the horizon, the level at the end of the day =
20
+ available - undated - dated due by then + supply (open work and purchase orders) due by
21
+ then. ``net_requirement`` is the lowest level when it is below zero.
22
+ """
23
+
24
+ from datetime import date, datetime
25
+ from typing import Optional
26
+
27
+ from .inventory import _ts_in, _ts_out, _uuid_str
28
+
29
+
30
+ def _date_in(value):
31
+ if value is None or isinstance(value, date):
32
+ return value
33
+ return date.fromisoformat(str(value)[:10])
34
+
35
+
36
+ class InventoryProjectionDay:
37
+ """The projected level at the end of ``day``, with what comes in and goes out that day."""
38
+
39
+ def __init__(self, day=None, incoming: float = None, outgoing: float = None, level: float = None):
40
+ self.day: Optional[date] = _date_in(day)
41
+ self.incoming = incoming
42
+ self.outgoing = outgoing
43
+ self.level = level
44
+
45
+ def to_dict(self) -> dict:
46
+ return {"date": self.day.isoformat() if self.day else None, "incoming": self.incoming,
47
+ "outgoing": self.outgoing, "level": self.level}
48
+
49
+ @classmethod
50
+ def from_dict(cls, data: dict) -> "InventoryProjectionDay":
51
+ return cls(day=data.get("date"), incoming=data.get("incoming"), outgoing=data.get("outgoing"),
52
+ level=data.get("level"))
53
+
54
+
55
+ class InventoryAvailabilityProduct:
56
+ """One product's availability at a facility, in ``unit`` (its stock total's unit)."""
57
+
58
+ def __init__(self, key: str = None, name: str = None, unit=None, unit_symbol: str = None):
59
+ self.key = key
60
+ self.name = name
61
+ self.unit: Optional[str] = _uuid_str(unit)
62
+ self.unit_symbol = unit_symbol
63
+ self.total: Optional[float] = None
64
+ self.allocated: Optional[float] = None
65
+ self.hold: Optional[float] = None
66
+ self.open_demand: Optional[float] = None
67
+ self.available: Optional[float] = None
68
+ self.atp: Optional[float] = None
69
+ self.dated: Optional[float] = None
70
+ self.undated: Optional[float] = None
71
+ self.demand_explained: Optional[float] = None
72
+ self.allocation_explained: Optional[float] = None
73
+ self.supply: Optional[float] = None
74
+ self.net_requirement: Optional[float] = None
75
+ self.lowest: Optional[InventoryProjectionDay] = None
76
+ self.projection: list = []
77
+ self.from_snapshot: bool = False
78
+
79
+ _NUMBERS = (("total", "total"), ("allocated", "allocated"), ("hold", "hold"), ("openDemand", "open_demand"),
80
+ ("available", "available"), ("atp", "atp"), ("dated", "dated"), ("undated", "undated"),
81
+ ("demandExplained", "demand_explained"), ("allocationExplained", "allocation_explained"),
82
+ ("supply", "supply"), ("netRequirement", "net_requirement"))
83
+
84
+ def to_dict(self) -> dict:
85
+ obj = {"product": self.key, "name": self.name, "unit": self.unit, "unitSymbol": self.unit_symbol}
86
+ for json_name, attr in self._NUMBERS:
87
+ obj[json_name] = getattr(self, attr)
88
+ obj["fromSnapshot"] = self.from_snapshot
89
+ obj["lowest"] = self.lowest.to_dict() if self.lowest is not None else None
90
+ obj["projection"] = [day.to_dict() for day in self.projection]
91
+ return obj
92
+
93
+ @classmethod
94
+ def from_dict(cls, data: dict) -> "InventoryAvailabilityProduct":
95
+ obj = cls(key=data.get("product"), name=data.get("name"), unit=data.get("unit"),
96
+ unit_symbol=data.get("unitSymbol"))
97
+ for json_name, attr in cls._NUMBERS:
98
+ setattr(obj, attr, data.get(json_name))
99
+ obj.from_snapshot = bool(data.get("fromSnapshot"))
100
+ obj.lowest = InventoryProjectionDay.from_dict(data["lowest"]) if data.get("lowest") else None
101
+ obj.projection = [InventoryProjectionDay.from_dict(d) for d in data.get("projection") or []]
102
+ return obj
103
+
104
+ def __repr__(self) -> str:
105
+ return (f"InventoryAvailabilityProduct({self.key!r}, total={self.total}, atp={self.atp}, "
106
+ f"net_requirement={self.net_requirement})")
107
+
108
+
109
+ class InventoryAvailability:
110
+ """
111
+ Availability of a facility's products at ``as_of`` (DEV-6137). ``snapshot_as_of`` is the
112
+ moment of the last availability snapshot written at or before it (None without one):
113
+ the stock and the snapshot's figures are read there.
114
+ """
115
+
116
+ def __init__(self, facility: str = None, as_of=None, snapshot_as_of=None, horizon_days: int = None):
117
+ self.facility = facility
118
+ self.as_of: Optional[datetime] = _ts_in(as_of)
119
+ self.snapshot_as_of: Optional[datetime] = _ts_in(snapshot_as_of)
120
+ self.horizon_days = horizon_days
121
+ self.products: list = []
122
+
123
+ def product(self, key: str) -> Optional[InventoryAvailabilityProduct]:
124
+ return next((p for p in self.products if p.key == key), None)
125
+
126
+ def to_dict(self) -> dict:
127
+ return {"facility": self.facility,
128
+ "asOf": _ts_out(self.as_of) if isinstance(self.as_of, datetime) else self.as_of,
129
+ "snapshotAsOf": _ts_out(self.snapshot_as_of) if isinstance(self.snapshot_as_of, datetime)
130
+ else self.snapshot_as_of,
131
+ "horizonDays": self.horizon_days, "products": [p.to_dict() for p in self.products]}
132
+
133
+ @classmethod
134
+ def from_dict(cls, data: dict) -> "InventoryAvailability":
135
+ obj = cls(facility=data.get("facility"), as_of=data.get("asOf"), snapshot_as_of=data.get("snapshotAsOf"),
136
+ horizon_days=data.get("horizonDays"))
137
+ obj.products = [InventoryAvailabilityProduct.from_dict(p) for p in data.get("products") or []]
138
+ return obj
@@ -0,0 +1 @@
1
+ __version__ = "2.2.0.dev31"
@@ -58,6 +58,7 @@ import ast
58
58
  from .deployment import Deployment, DeploymentStatus
59
59
  from .product import ProductKind, Product, ProductComponent
60
60
  from .inventory import InventoryBalance, InventoryStorage, InventoryStock, InventoryCount, InventoryHistory
61
+ from .inventory_availability import InventoryAvailability
61
62
  from .planned_movement import PlannedMovement, PlannedMovementSet
62
63
  from .connection import EdgeDeviceKey
63
64
  from .edge_config import EdgeConfig
@@ -3233,6 +3234,38 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
3233
3234
  expected=(200, 404))
3234
3235
  return response.json().get("deleted", 0) if response.status_code == 200 else 0
3235
3236
 
3237
+ def get_inventory_availability(self, facility: str, as_of=None, products: list = None,
3238
+ horizon_days: int = None, sources: list = None) -> InventoryAvailability:
3239
+ """
3240
+ What a facility can still promise per product, and its stock projected per day
3241
+ (DEV-6137). The stock under the facility twin and its children and the figures of the
3242
+ last availability snapshot (``allocated``, ``hold``, ``openDemand`` planned movements)
3243
+ are read at that snapshot's moment (``snapshot_as_of``): available = total - allocated
3244
+ - hold, ATP = available - open demand. The open customer order lines date the demand
3245
+ (capped at the open demand; the rest is due at once) and the open work and purchase
3246
+ orders come in when due. Without a snapshot, the lines give allocated and open demand.
3247
+
3248
+ :param facility: hardware id of the facility Twin.
3249
+ :param as_of: datetime, ISO 8601, epoch ms or a relative ``now-1d``; default now.
3250
+ :param products: product keys to keep.
3251
+ :param horizon_days: days projected (UTC days, today first); default 14, at most 120.
3252
+ :param sources: keep the planned movements of those sources.
3253
+ """
3254
+ body = {"facility": facility}
3255
+ if as_of is not None:
3256
+ if isinstance(as_of, datetime.datetime):
3257
+ if as_of.tzinfo is None:
3258
+ as_of = as_of.replace(tzinfo=datetime.timezone.utc)
3259
+ as_of = as_of.isoformat()
3260
+ body["asOf"] = as_of
3261
+ for name, value in (("products", products), ("sources", sources)):
3262
+ if value is not None:
3263
+ body[name] = list(value)
3264
+ if horizon_days is not None:
3265
+ body["horizonDays"] = horizon_days
3266
+ response = self._planner_request("POST", "planner/inventory/availability", body=body)
3267
+ return InventoryAvailability.from_dict(response.json())
3268
+
3236
3269
  # ------------------------------------------------------------ edge (DEV-6013/6014)
3237
3270
 
3238
3271
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wizata_dsapi
3
- Version: 2.2.0.dev29
3
+ Version: 2.2.0.dev31
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -33,6 +33,7 @@ wizata_dsapi/group_system.py
33
33
  wizata_dsapi/ilogger.py
34
34
  wizata_dsapi/insight.py
35
35
  wizata_dsapi/inventory.py
36
+ wizata_dsapi/inventory_availability.py
36
37
  wizata_dsapi/mlmodel.py
37
38
  wizata_dsapi/mobile_asset.py
38
39
  wizata_dsapi/model_toolkit.py
@@ -1 +0,0 @@
1
- __version__ = "2.2.0.dev29"