wizata-dsapi 2.2.0.dev22__tar.gz → 2.2.0.dev23__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.dev22 → wizata_dsapi-2.2.0.dev23}/PKG-INFO +1 -1
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/__init__.py +2 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/inventory.py +248 -0
- wizata_dsapi-2.2.0.dev23/wizata_dsapi/version.py +1 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/wizata_dsapi_client.py +68 -1
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi.egg-info/PKG-INFO +1 -1
- wizata_dsapi-2.2.0.dev22/wizata_dsapi/version.py +0 -1
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/LICENSE.txt +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/README.rst +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/setup.cfg +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/setup.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/api_config.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/api_dto.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/api_interface.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/bucket.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/business_label.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/connection.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/connection_discovery.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/connection_policy.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/connection_types.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/context.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/dashboard.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/dataframe_toolkit.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/datapoint.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/datastore.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/deployment.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/ds_dataframe.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/dsapi_json_encoder.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/edge_config.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/edge_device.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/edge_module.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/edge_package.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/evaluation.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/execution.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/execution_log.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/experiment.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/formula.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/graylog_log.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/group_system.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/ilogger.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/insight.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/mlmodel.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/mobile_asset.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/model_toolkit.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/models/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/models/common.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/notification.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/paged_query_result.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/pipeline.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/pipeline_image.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/plot.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/plots/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/plots/common.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/plots/theme.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/product.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/request.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/script.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/scripts/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/scripts/common.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/search.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/solution_component.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/streamlit_utils.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/template.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/template_config.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/trigger.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/twin.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/twinregistration.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/user.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/wizard_function.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/wizard_request.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi/words.py +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi.egg-info/SOURCES.txt +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi.egg-info/requires.txt +0 -0
- {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi.egg-info/top_level.txt +0 -0
|
@@ -75,6 +75,8 @@ from .connection_discovery import ConnectionSeenName, ConnectionSeenReport, Conn
|
|
|
75
75
|
from .product import ProductKind, Product, ProductComponent, BUILTIN_PRODUCT_KINDS, is_valid_product_key
|
|
76
76
|
from .inventory import (InventoryBalance, InventoryStorage, InventoryStorageDatapoint, inventory_mode,
|
|
77
77
|
InventoryStock, InventoryStockProduct, InventoryStockLine, InventoryStockCount, INVENTORY_OPTION, INVENTORY_MODES,
|
|
78
|
+
InventoryCount, InventoryCountLine, InventoryCountControl, InventoryHistory,
|
|
79
|
+
InventoryHistoryEvent, HISTORY_MOVEMENT, HISTORY_COUNT, round_quantity,
|
|
78
80
|
INVENTORY_MOVEMENTS, INVENTORY_BALANCES, INVENTORY_STOCK,
|
|
79
81
|
PRODUCT_TAG, PRODUCT_MESSAGE_KEY)
|
|
80
82
|
from .dashboard import Dashboard, DashboardType
|
|
@@ -311,3 +311,251 @@ class InventoryStock:
|
|
|
311
311
|
@classmethod
|
|
312
312
|
def from_dict(cls, data: dict) -> "InventoryStock":
|
|
313
313
|
return cls(as_of=data.get("asOf"), products=[InventoryStockProduct.from_dict(p) for p in data.get("products") or []])
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def round_quantity(value):
|
|
317
|
+
"""
|
|
318
|
+
A quantity kept to 12 significant digits: every real digit, none of the binary noise
|
|
319
|
+
that sums and unit conversions leave (0.1 + 0.2, 540000 mL in L). Expected and counted
|
|
320
|
+
levels are compared after this, so a count that matches is never off by 1e-13.
|
|
321
|
+
"""
|
|
322
|
+
if value is None:
|
|
323
|
+
return None
|
|
324
|
+
return float(f"{float(value):.12g}")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _twin_dict(twin_id, hardware_id, name) -> Optional[dict]:
|
|
328
|
+
if twin_id is None and hardware_id is None:
|
|
329
|
+
return None
|
|
330
|
+
return {"id": twin_id, "hardwareId": hardware_id, "name": name}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class InventoryCountLine:
|
|
334
|
+
"""
|
|
335
|
+
One line of a count (DEV-6128): the balance, its storage, and what the movements
|
|
336
|
+
predicted at that moment. ``expected`` is the stock just before this count, without
|
|
337
|
+
it: the count before it plus the movements since, in the datapoint's unit.
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
def __init__(self, balance: InventoryBalance = None, twin_id=None, twin_hardware_id: str = None,
|
|
341
|
+
twin_name: str = None, unit=None, unit_symbol: str = None, expected: float = None):
|
|
342
|
+
self.balance: InventoryBalance = balance if balance is not None else InventoryBalance()
|
|
343
|
+
self.twin_id: Optional[str] = _uuid_str(twin_id)
|
|
344
|
+
self.twin_hardware_id: Optional[str] = twin_hardware_id
|
|
345
|
+
self.twin_name: Optional[str] = twin_name
|
|
346
|
+
self.unit: Optional[str] = _uuid_str(unit)
|
|
347
|
+
self.unit_symbol: Optional[str] = unit_symbol
|
|
348
|
+
self.expected: Optional[float] = expected
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def difference(self) -> Optional[float]:
|
|
352
|
+
"""Counted minus expected: negative when stock went missing."""
|
|
353
|
+
if self.expected is None or self.balance.quantity is None:
|
|
354
|
+
return None
|
|
355
|
+
return round_quantity(round_quantity(self.balance.quantity) - round_quantity(self.expected))
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def verified(self) -> Optional[bool]:
|
|
359
|
+
"""The count matches the movements exactly (after unit rounding)."""
|
|
360
|
+
if self.expected is None or self.balance.quantity is None:
|
|
361
|
+
return None
|
|
362
|
+
return round_quantity(self.balance.quantity) == round_quantity(self.expected)
|
|
363
|
+
|
|
364
|
+
def to_dict(self) -> dict:
|
|
365
|
+
obj = self.balance.to_dict()
|
|
366
|
+
obj.update({"twin": _twin_dict(self.twin_id, self.twin_hardware_id, self.twin_name),
|
|
367
|
+
"unit": self.unit, "unitSymbol": self.unit_symbol, "expected": self.expected,
|
|
368
|
+
"difference": self.difference, "verified": self.verified})
|
|
369
|
+
return obj
|
|
370
|
+
|
|
371
|
+
@classmethod
|
|
372
|
+
def from_dict(cls, data: dict) -> "InventoryCountLine":
|
|
373
|
+
twin = data.get("twin") or {}
|
|
374
|
+
balance = InventoryBalance.from_dict({k: v for k, v in data.items() if k != "unit"})
|
|
375
|
+
return cls(balance=balance, twin_id=twin.get("id"), twin_hardware_id=twin.get("hardwareId"),
|
|
376
|
+
twin_name=twin.get("name"), unit=data.get("unit"), unit_symbol=data.get("unitSymbol"),
|
|
377
|
+
expected=data.get("expected"))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class InventoryCount:
|
|
381
|
+
"""
|
|
382
|
+
A count as one object (DEV-6128): every balance written by one call shares its
|
|
383
|
+
``count`` key. ``timestamp`` is the moment they were all counted, or None when an
|
|
384
|
+
API sync wrote several moments in one call (each line then carries its own).
|
|
385
|
+
|
|
386
|
+
Read with ``GET /dsapi/planner/inventory/counts/<count>/``, replaced with ``PUT``
|
|
387
|
+
(the count becomes exactly the lines sent, all or none), deleted with ``DELETE``.
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
def __init__(self, count=None, lines: list = None):
|
|
391
|
+
self.count: Optional[str] = _uuid_str(count)
|
|
392
|
+
self.lines: list = list(lines or [])
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def timestamp(self) -> Optional[datetime]:
|
|
396
|
+
moments = {line.balance.timestamp for line in self.lines}
|
|
397
|
+
return moments.pop() if len(moments) == 1 else None
|
|
398
|
+
|
|
399
|
+
def _first(self, attribute: str, latest: bool = False):
|
|
400
|
+
stamped = [(getattr(l.balance, attribute + "_at"), getattr(l.balance, attribute + "_by"))
|
|
401
|
+
for l in self.lines if getattr(l.balance, attribute + "_at") is not None]
|
|
402
|
+
if not stamped:
|
|
403
|
+
return None, None
|
|
404
|
+
return (max if latest else min)(stamped, key=lambda s: s[0])
|
|
405
|
+
|
|
406
|
+
def to_dict(self) -> dict:
|
|
407
|
+
created_at, created_by = self._first("created")
|
|
408
|
+
updated_at, updated_by = self._first("updated", latest=True)
|
|
409
|
+
timestamp = self.timestamp
|
|
410
|
+
return {"count": self.count,
|
|
411
|
+
"timestamp": _ts_out(timestamp) if isinstance(timestamp, datetime) else timestamp,
|
|
412
|
+
"createdAt": _ts_out(created_at) if isinstance(created_at, datetime) else created_at,
|
|
413
|
+
"createdBy": created_by,
|
|
414
|
+
"updatedAt": _ts_out(updated_at) if isinstance(updated_at, datetime) else updated_at,
|
|
415
|
+
"updatedBy": updated_by,
|
|
416
|
+
"lines": [line.to_dict() for line in self.lines]}
|
|
417
|
+
|
|
418
|
+
@classmethod
|
|
419
|
+
def from_dict(cls, data: dict) -> "InventoryCount":
|
|
420
|
+
return cls(count=data.get("count"), lines=[InventoryCountLine.from_dict(l) for l in data.get("lines") or []])
|
|
421
|
+
|
|
422
|
+
def __repr__(self) -> str:
|
|
423
|
+
return f"InventoryCount({self.count!r}, {len(self.lines)} lines)"
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class InventoryCountControl:
|
|
427
|
+
"""
|
|
428
|
+
How a count compares with the movements (DEV-6129): ``previous`` count (None when
|
|
429
|
+
never counted before), the net quantity ``moved`` since it, so ``expected`` =
|
|
430
|
+
previous + moved, then ``counted``, their ``difference`` and whether it is ``verified``
|
|
431
|
+
(difference 0 after unit rounding).
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
def __init__(self, previous: InventoryStockCount = None, moved: float = None, expected: float = None,
|
|
435
|
+
counted: float = None):
|
|
436
|
+
self.previous: Optional[InventoryStockCount] = previous
|
|
437
|
+
self.moved: Optional[float] = moved
|
|
438
|
+
self.expected: Optional[float] = expected
|
|
439
|
+
self.counted: Optional[float] = counted
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def difference(self) -> Optional[float]:
|
|
443
|
+
if self.expected is None or self.counted is None:
|
|
444
|
+
return None
|
|
445
|
+
return round_quantity(round_quantity(self.counted) - round_quantity(self.expected))
|
|
446
|
+
|
|
447
|
+
@property
|
|
448
|
+
def verified(self) -> Optional[bool]:
|
|
449
|
+
if self.expected is None or self.counted is None:
|
|
450
|
+
return None
|
|
451
|
+
return round_quantity(self.counted) == round_quantity(self.expected)
|
|
452
|
+
|
|
453
|
+
def to_dict(self) -> dict:
|
|
454
|
+
return {"previous": self.previous.to_dict() if self.previous is not None else None,
|
|
455
|
+
"moved": self.moved, "expected": self.expected, "counted": self.counted,
|
|
456
|
+
"difference": self.difference, "verified": self.verified}
|
|
457
|
+
|
|
458
|
+
@classmethod
|
|
459
|
+
def from_dict(cls, data: dict) -> "InventoryCountControl":
|
|
460
|
+
previous = data.get("previous")
|
|
461
|
+
return cls(previous=InventoryStockCount.from_dict(previous) if previous else None,
|
|
462
|
+
moved=data.get("moved"), expected=data.get("expected"), counted=data.get("counted"))
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
#: The two kinds of event a stock history holds.
|
|
466
|
+
HISTORY_MOVEMENT = "movement"
|
|
467
|
+
HISTORY_COUNT = "count"
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class InventoryHistoryEvent:
|
|
471
|
+
"""
|
|
472
|
+
One event of a stock history, in time order (DEV-6129): a ``movement`` (signed
|
|
473
|
+
``quantity``) or a ``count`` (counted ``quantity``, its ``count`` key, balance ``id``,
|
|
474
|
+
who recorded it and its ``control``). ``stock`` is the level right after it.
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
def __init__(self, timestamp=None, type: str = None, quantity: float = None, stock: float = None,
|
|
478
|
+
count=None, balance_id=None, by: str = None, control: InventoryCountControl = None):
|
|
479
|
+
self.timestamp: Optional[datetime] = _ts_in(timestamp)
|
|
480
|
+
self.type: Optional[str] = type
|
|
481
|
+
self.quantity: Optional[float] = quantity
|
|
482
|
+
self.stock: Optional[float] = stock
|
|
483
|
+
self.count: Optional[str] = _uuid_str(count)
|
|
484
|
+
self.id: Optional[str] = _uuid_str(balance_id)
|
|
485
|
+
self.by: Optional[str] = by
|
|
486
|
+
self.control: Optional[InventoryCountControl] = control
|
|
487
|
+
|
|
488
|
+
def to_dict(self) -> dict:
|
|
489
|
+
obj = {"timestamp": _ts_out(self.timestamp) if isinstance(self.timestamp, datetime) else self.timestamp,
|
|
490
|
+
"type": self.type, "quantity": self.quantity, "stock": self.stock}
|
|
491
|
+
if self.type == HISTORY_COUNT:
|
|
492
|
+
obj.update({"count": self.count, "id": self.id, "by": self.by,
|
|
493
|
+
"control": self.control.to_dict() if self.control is not None else None})
|
|
494
|
+
return obj
|
|
495
|
+
|
|
496
|
+
@classmethod
|
|
497
|
+
def from_dict(cls, data: dict) -> "InventoryHistoryEvent":
|
|
498
|
+
control = data.get("control")
|
|
499
|
+
return cls(timestamp=data.get("timestamp"), type=data.get("type"), quantity=data.get("quantity"),
|
|
500
|
+
stock=data.get("stock"), count=data.get("count"), balance_id=data.get("id"), by=data.get("by"),
|
|
501
|
+
control=InventoryCountControl.from_dict(control) if control else None)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
class InventoryHistory:
|
|
505
|
+
"""
|
|
506
|
+
How one product's stock moved on one storage datapoint over a period, and whether
|
|
507
|
+
each count matched (DEV-6129, ``POST /dsapi/planner/inventory/history``).
|
|
508
|
+
|
|
509
|
+
``opening`` is the stock at ``start``, ``closing`` at ``end``; ``events`` are every
|
|
510
|
+
movement and count in between, oldest first, each with the stock after it. A count
|
|
511
|
+
carries its control against the count before it, even when that one is before
|
|
512
|
+
``start``. ``summary`` counts the counts, the verified ones and the ``unexplained``
|
|
513
|
+
quantity (the sum of the differences). Past the event limit, ``truncated`` is true
|
|
514
|
+
and only the latest events are kept; the summary still covers the whole period.
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
def __init__(self, datapoint: str = None, product: str = None, start=None, end=None, unit=None,
|
|
518
|
+
unit_symbol: str = None, opening: float = None, closing: float = None, events: list = None,
|
|
519
|
+
truncated: bool = False):
|
|
520
|
+
self.datapoint: Optional[str] = datapoint
|
|
521
|
+
self.product: Optional[str] = product
|
|
522
|
+
self.start: Optional[datetime] = _ts_in(start)
|
|
523
|
+
self.end: Optional[datetime] = _ts_in(end)
|
|
524
|
+
self.unit: Optional[str] = _uuid_str(unit)
|
|
525
|
+
self.unit_symbol: Optional[str] = unit_symbol
|
|
526
|
+
self.opening: Optional[float] = opening
|
|
527
|
+
self.closing: Optional[float] = closing
|
|
528
|
+
self.events: list = list(events or [])
|
|
529
|
+
self.truncated: bool = truncated
|
|
530
|
+
self.counts: int = 0
|
|
531
|
+
self.verified: int = 0
|
|
532
|
+
self.unexplained: float = 0.0
|
|
533
|
+
|
|
534
|
+
def to_dict(self) -> dict:
|
|
535
|
+
def ts(value):
|
|
536
|
+
return _ts_out(value) if isinstance(value, datetime) else value
|
|
537
|
+
return {"datapoint": self.datapoint, "product": self.product, "unit": self.unit,
|
|
538
|
+
"unitSymbol": self.unit_symbol,
|
|
539
|
+
"opening": {"timestamp": ts(self.start), "quantity": self.opening},
|
|
540
|
+
"closing": {"timestamp": ts(self.end), "quantity": self.closing},
|
|
541
|
+
"events": [e.to_dict() for e in self.events],
|
|
542
|
+
"summary": {"counts": self.counts, "verified": self.verified, "unexplained": self.unexplained},
|
|
543
|
+
"truncated": self.truncated}
|
|
544
|
+
|
|
545
|
+
@classmethod
|
|
546
|
+
def from_dict(cls, data: dict) -> "InventoryHistory":
|
|
547
|
+
opening, closing = data.get("opening") or {}, data.get("closing") or {}
|
|
548
|
+
summary = data.get("summary") or {}
|
|
549
|
+
obj = cls(datapoint=data.get("datapoint"), product=data.get("product"), start=opening.get("timestamp"),
|
|
550
|
+
end=closing.get("timestamp"), unit=data.get("unit"), unit_symbol=data.get("unitSymbol"),
|
|
551
|
+
opening=opening.get("quantity"), closing=closing.get("quantity"),
|
|
552
|
+
events=[InventoryHistoryEvent.from_dict(e) for e in data.get("events") or []],
|
|
553
|
+
truncated=bool(data.get("truncated")))
|
|
554
|
+
obj.counts = summary.get("counts", 0)
|
|
555
|
+
obj.verified = summary.get("verified", 0)
|
|
556
|
+
obj.unexplained = summary.get("unexplained", 0.0)
|
|
557
|
+
return obj
|
|
558
|
+
|
|
559
|
+
def __repr__(self) -> str:
|
|
560
|
+
return (f"InventoryHistory({self.datapoint!r}, {self.product!r}, {len(self.events)} events, "
|
|
561
|
+
f"{self.verified}/{self.counts} counts verified)")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.2.0.dev23"
|
|
@@ -56,7 +56,7 @@ from .version import __version__
|
|
|
56
56
|
import ast
|
|
57
57
|
from .deployment import Deployment, DeploymentStatus
|
|
58
58
|
from .product import ProductKind, Product, ProductComponent
|
|
59
|
-
from .inventory import InventoryBalance, InventoryStorage, InventoryStock
|
|
59
|
+
from .inventory import InventoryBalance, InventoryStorage, InventoryStock, InventoryCount, InventoryHistory
|
|
60
60
|
from .connection import EdgeDeviceKey
|
|
61
61
|
from .edge_config import EdgeConfig
|
|
62
62
|
from .api_config import _registry
|
|
@@ -3001,6 +3001,73 @@ class WizataDSAPIClient(ApiInterface, ApiDtoInterface):
|
|
|
3001
3001
|
self._planner_request("DELETE", f"planner/inventory/balances/{self._balance_id(balance)}/",
|
|
3002
3002
|
expected=(204,))
|
|
3003
3003
|
|
|
3004
|
+
@staticmethod
|
|
3005
|
+
def _count_key(value) -> str:
|
|
3006
|
+
key = value.count if isinstance(value, (InventoryCount, InventoryBalance)) else value
|
|
3007
|
+
if not key:
|
|
3008
|
+
raise ValueError("please provide a count key")
|
|
3009
|
+
return urllib.parse.quote(str(key), safe="")
|
|
3010
|
+
|
|
3011
|
+
def get_inventory_count(self, count):
|
|
3012
|
+
"""
|
|
3013
|
+
A count as one object (DEV-6128): its lines (balances) with their storage, unit, and
|
|
3014
|
+
the level the movements predicted for each (``expected``, ``difference``,
|
|
3015
|
+
``verified``).
|
|
3016
|
+
|
|
3017
|
+
:param count: the count key, or an InventoryBalance / InventoryCount carrying it.
|
|
3018
|
+
:return: InventoryCount, or None when no balance carries that key.
|
|
3019
|
+
"""
|
|
3020
|
+
response = self._planner_request("GET", f"planner/inventory/counts/{self._count_key(count)}/",
|
|
3021
|
+
expected=(200, 404))
|
|
3022
|
+
return InventoryCount.from_dict(response.json()) if response.status_code == 200 else None
|
|
3023
|
+
|
|
3024
|
+
def set_inventory_count(self, count, balances: list, timestamp=None) -> InventoryCount:
|
|
3025
|
+
"""
|
|
3026
|
+
Replace a count, all or none: afterwards it holds exactly ``balances``.
|
|
3027
|
+
|
|
3028
|
+
A balance with an ``id`` changes that line (quantity, time; never its datapoint or
|
|
3029
|
+
product). One without takes the count's line of the same datapoint and product, or
|
|
3030
|
+
joins the count. A line left out is deleted. ``timestamp`` (datetime or ISO 8601) is
|
|
3031
|
+
the time of every balance that has none. 409 when another count holds one of those
|
|
3032
|
+
products at that time.
|
|
3033
|
+
|
|
3034
|
+
:param count: the count key, or an InventoryCount.
|
|
3035
|
+
:param balances: InventoryBalance list (or dicts).
|
|
3036
|
+
:return: the count afterwards, as :meth:`get_inventory_count` returns it.
|
|
3037
|
+
"""
|
|
3038
|
+
body = {"balances": [b.to_dict() if isinstance(b, InventoryBalance) else b for b in balances]}
|
|
3039
|
+
if timestamp is not None:
|
|
3040
|
+
body["timestamp"] = timestamp.isoformat() if isinstance(timestamp, datetime.datetime) else timestamp
|
|
3041
|
+
response = self._planner_request("PUT", f"planner/inventory/counts/{self._count_key(count)}/", body=body)
|
|
3042
|
+
return InventoryCount.from_dict(response.json())
|
|
3043
|
+
|
|
3044
|
+
def delete_inventory_count(self, count):
|
|
3045
|
+
"""
|
|
3046
|
+
Delete every balance of a count.
|
|
3047
|
+
|
|
3048
|
+
:param count: the count key, or an InventoryCount / InventoryBalance carrying it.
|
|
3049
|
+
"""
|
|
3050
|
+
self._planner_request("DELETE", f"planner/inventory/counts/{self._count_key(count)}/", expected=(204,))
|
|
3051
|
+
|
|
3052
|
+
def get_inventory_history(self, datapoint: str, product: str, start=None, end=None) -> InventoryHistory:
|
|
3053
|
+
"""
|
|
3054
|
+
How one product's stock moved on one storage datapoint (DEV-6129): every movement and
|
|
3055
|
+
count in time order with the stock after each, and each count compared with the count
|
|
3056
|
+
before it plus the movements since (``control``: expected, counted, difference,
|
|
3057
|
+
verified). Default period: the last 7 days.
|
|
3058
|
+
|
|
3059
|
+
:param datapoint: hardware id of the storage's inventoryMovement datapoint.
|
|
3060
|
+
:param product: product key.
|
|
3061
|
+
:param start: datetime, ISO 8601, epoch ms or a relative ``now-7d``.
|
|
3062
|
+
:param end: same; default now.
|
|
3063
|
+
"""
|
|
3064
|
+
body = {"datapoint": datapoint, "product": product}
|
|
3065
|
+
for name, value in (("start", start), ("end", end)):
|
|
3066
|
+
if value is not None:
|
|
3067
|
+
body[name] = value.isoformat() if isinstance(value, datetime.datetime) else value
|
|
3068
|
+
response = self._planner_request("POST", "planner/inventory/history", body=body)
|
|
3069
|
+
return InventoryHistory.from_dict(response.json())
|
|
3070
|
+
|
|
3004
3071
|
# ------------------------------------------------------------ edge (DEV-6013/6014)
|
|
3005
3072
|
|
|
3006
3073
|
@staticmethod
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.2.0.dev22"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev23}/wizata_dsapi.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|