wizata-dsapi 2.2.0.dev22__tar.gz → 2.2.0.dev24__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 (75) hide show
  1. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/PKG-INFO +1 -1
  2. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/__init__.py +2 -0
  3. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/inventory.py +257 -0
  4. wizata_dsapi-2.2.0.dev24/wizata_dsapi/version.py +1 -0
  5. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/wizata_dsapi_client.py +68 -1
  6. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi.egg-info/PKG-INFO +1 -1
  7. wizata_dsapi-2.2.0.dev22/wizata_dsapi/version.py +0 -1
  8. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/LICENSE.txt +0 -0
  9. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/README.rst +0 -0
  10. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/setup.cfg +0 -0
  11. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/setup.py +0 -0
  12. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/api_config.py +0 -0
  13. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/api_dto.py +0 -0
  14. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/api_interface.py +0 -0
  15. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/bucket.py +0 -0
  16. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/business_label.py +0 -0
  17. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/connection.py +0 -0
  18. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/connection_discovery.py +0 -0
  19. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/connection_policy.py +0 -0
  20. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/connection_types.py +0 -0
  21. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/context.py +0 -0
  22. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/dashboard.py +0 -0
  23. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/dataframe_toolkit.py +0 -0
  24. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/datapoint.py +0 -0
  25. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/datastore.py +0 -0
  26. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/deployment.py +0 -0
  27. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/ds_dataframe.py +0 -0
  28. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/dsapi_json_encoder.py +0 -0
  29. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/edge_config.py +0 -0
  30. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/edge_device.py +0 -0
  31. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/edge_module.py +0 -0
  32. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/edge_package.py +0 -0
  33. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/evaluation.py +0 -0
  34. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/execution.py +0 -0
  35. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/execution_log.py +0 -0
  36. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/experiment.py +0 -0
  37. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/formula.py +0 -0
  38. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/graylog_log.py +0 -0
  39. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/group_system.py +0 -0
  40. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/ilogger.py +0 -0
  41. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/insight.py +0 -0
  42. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/mlmodel.py +0 -0
  43. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/mobile_asset.py +0 -0
  44. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/model_toolkit.py +0 -0
  45. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/models/__init__.py +0 -0
  46. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/models/common.py +0 -0
  47. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/notification.py +0 -0
  48. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/paged_query_result.py +0 -0
  49. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/pipeline.py +0 -0
  50. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/pipeline_image.py +0 -0
  51. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/plot.py +0 -0
  52. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/plots/__init__.py +0 -0
  53. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/plots/common.py +0 -0
  54. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/plots/theme.py +0 -0
  55. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/product.py +0 -0
  56. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/request.py +0 -0
  57. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/script.py +0 -0
  58. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/scripts/__init__.py +0 -0
  59. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/scripts/common.py +0 -0
  60. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/search.py +0 -0
  61. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/solution_component.py +0 -0
  62. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/streamlit_utils.py +0 -0
  63. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/template.py +0 -0
  64. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/template_config.py +0 -0
  65. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/trigger.py +0 -0
  66. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/twin.py +0 -0
  67. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/twinregistration.py +0 -0
  68. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/user.py +0 -0
  69. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/wizard_function.py +0 -0
  70. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/wizard_request.py +0 -0
  71. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi/words.py +0 -0
  72. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi.egg-info/SOURCES.txt +0 -0
  73. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
  74. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/wizata_dsapi.egg-info/requires.txt +0 -0
  75. {wizata_dsapi-2.2.0.dev22 → wizata_dsapi-2.2.0.dev24}/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.dev22
3
+ Version: 2.2.0.dev24
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -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,260 @@ 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. With no
338
+ count before it there is nothing to check: ``expected``, ``difference`` and
339
+ ``verified`` are None (this count is the opening level).
340
+ """
341
+
342
+ def __init__(self, balance: InventoryBalance = None, twin_id=None, twin_hardware_id: str = None,
343
+ twin_name: str = None, unit=None, unit_symbol: str = None, expected: float = None):
344
+ self.balance: InventoryBalance = balance if balance is not None else InventoryBalance()
345
+ self.twin_id: Optional[str] = _uuid_str(twin_id)
346
+ self.twin_hardware_id: Optional[str] = twin_hardware_id
347
+ self.twin_name: Optional[str] = twin_name
348
+ self.unit: Optional[str] = _uuid_str(unit)
349
+ self.unit_symbol: Optional[str] = unit_symbol
350
+ self.expected: Optional[float] = expected
351
+
352
+ @property
353
+ def difference(self) -> Optional[float]:
354
+ """Counted minus expected: negative when stock went missing."""
355
+ if self.expected is None or self.balance.quantity is None:
356
+ return None
357
+ return round_quantity(round_quantity(self.balance.quantity) - round_quantity(self.expected))
358
+
359
+ @property
360
+ def verified(self) -> Optional[bool]:
361
+ """The count matches the movements exactly (after unit rounding)."""
362
+ if self.expected is None or self.balance.quantity is None:
363
+ return None
364
+ return round_quantity(self.balance.quantity) == round_quantity(self.expected)
365
+
366
+ def to_dict(self) -> dict:
367
+ obj = self.balance.to_dict()
368
+ obj.update({"twin": _twin_dict(self.twin_id, self.twin_hardware_id, self.twin_name),
369
+ "unit": self.unit, "unitSymbol": self.unit_symbol, "expected": self.expected,
370
+ "difference": self.difference, "verified": self.verified})
371
+ return obj
372
+
373
+ @classmethod
374
+ def from_dict(cls, data: dict) -> "InventoryCountLine":
375
+ twin = data.get("twin") or {}
376
+ balance = InventoryBalance.from_dict({k: v for k, v in data.items() if k != "unit"})
377
+ return cls(balance=balance, twin_id=twin.get("id"), twin_hardware_id=twin.get("hardwareId"),
378
+ twin_name=twin.get("name"), unit=data.get("unit"), unit_symbol=data.get("unitSymbol"),
379
+ expected=data.get("expected"))
380
+
381
+
382
+ class InventoryCount:
383
+ """
384
+ A count as one object (DEV-6128): every balance written by one call shares its
385
+ ``count`` key. ``timestamp`` is the moment they were all counted, or None when an
386
+ API sync wrote several moments in one call (each line then carries its own).
387
+
388
+ Read with ``GET /dsapi/planner/inventory/counts/<count>/``, replaced with ``PUT``
389
+ (the count becomes exactly the lines sent, all or none), deleted with ``DELETE``.
390
+ """
391
+
392
+ def __init__(self, count=None, lines: list = None):
393
+ self.count: Optional[str] = _uuid_str(count)
394
+ self.lines: list = list(lines or [])
395
+
396
+ @property
397
+ def timestamp(self) -> Optional[datetime]:
398
+ moments = {line.balance.timestamp for line in self.lines}
399
+ return moments.pop() if len(moments) == 1 else None
400
+
401
+ def _first(self, attribute: str, latest: bool = False):
402
+ stamped = [(getattr(l.balance, attribute + "_at"), getattr(l.balance, attribute + "_by"))
403
+ for l in self.lines if getattr(l.balance, attribute + "_at") is not None]
404
+ if not stamped:
405
+ return None, None
406
+ return (max if latest else min)(stamped, key=lambda s: s[0])
407
+
408
+ def to_dict(self) -> dict:
409
+ created_at, created_by = self._first("created")
410
+ updated_at, updated_by = self._first("updated", latest=True)
411
+ timestamp = self.timestamp
412
+ return {"count": self.count,
413
+ "timestamp": _ts_out(timestamp) if isinstance(timestamp, datetime) else timestamp,
414
+ "createdAt": _ts_out(created_at) if isinstance(created_at, datetime) else created_at,
415
+ "createdBy": created_by,
416
+ "updatedAt": _ts_out(updated_at) if isinstance(updated_at, datetime) else updated_at,
417
+ "updatedBy": updated_by,
418
+ "lines": [line.to_dict() for line in self.lines]}
419
+
420
+ @classmethod
421
+ def from_dict(cls, data: dict) -> "InventoryCount":
422
+ return cls(count=data.get("count"), lines=[InventoryCountLine.from_dict(l) for l in data.get("lines") or []])
423
+
424
+ def __repr__(self) -> str:
425
+ return f"InventoryCount({self.count!r}, {len(self.lines)} lines)"
426
+
427
+
428
+ class InventoryCountControl:
429
+ """
430
+ How a count compares with the movements (DEV-6129): the ``previous`` count, the net
431
+ quantity ``moved`` since it, so ``expected`` = previous + moved, then ``counted``, their
432
+ ``difference`` and whether it is ``verified`` (difference 0 after unit rounding).
433
+
434
+ A count with no count before it cannot be checked: it is the opening level, and
435
+ ``previous``, ``moved``, ``expected``, ``difference`` and ``verified`` are None.
436
+ """
437
+
438
+ def __init__(self, previous: InventoryStockCount = None, moved: float = None, expected: float = None,
439
+ counted: float = None):
440
+ self.previous: Optional[InventoryStockCount] = previous
441
+ self.moved: Optional[float] = moved
442
+ self.expected: Optional[float] = expected
443
+ self.counted: Optional[float] = counted
444
+
445
+ @property
446
+ def difference(self) -> Optional[float]:
447
+ if self.expected is None or self.counted is None:
448
+ return None
449
+ return round_quantity(round_quantity(self.counted) - round_quantity(self.expected))
450
+
451
+ @property
452
+ def verified(self) -> Optional[bool]:
453
+ if self.expected is None or self.counted is None:
454
+ return None
455
+ return round_quantity(self.counted) == round_quantity(self.expected)
456
+
457
+ def to_dict(self) -> dict:
458
+ return {"previous": self.previous.to_dict() if self.previous is not None else None,
459
+ "moved": self.moved, "expected": self.expected, "counted": self.counted,
460
+ "difference": self.difference, "verified": self.verified}
461
+
462
+ @classmethod
463
+ def from_dict(cls, data: dict) -> "InventoryCountControl":
464
+ previous = data.get("previous")
465
+ return cls(previous=InventoryStockCount.from_dict(previous) if previous else None,
466
+ moved=data.get("moved"), expected=data.get("expected"), counted=data.get("counted"))
467
+
468
+
469
+ #: The two kinds of event a stock history holds.
470
+ HISTORY_MOVEMENT = "movement"
471
+ HISTORY_COUNT = "count"
472
+
473
+
474
+ class InventoryHistoryEvent:
475
+ """
476
+ One event of a stock history, in time order (DEV-6129): a ``movement`` (signed
477
+ ``quantity``) or a ``count`` (counted ``quantity``, its ``count`` key, balance ``id``,
478
+ who recorded it and its ``control``). ``stock`` is the level right after it.
479
+ """
480
+
481
+ def __init__(self, timestamp=None, type: str = None, quantity: float = None, stock: float = None,
482
+ count=None, balance_id=None, by: str = None, control: InventoryCountControl = None):
483
+ self.timestamp: Optional[datetime] = _ts_in(timestamp)
484
+ self.type: Optional[str] = type
485
+ self.quantity: Optional[float] = quantity
486
+ self.stock: Optional[float] = stock
487
+ self.count: Optional[str] = _uuid_str(count)
488
+ self.id: Optional[str] = _uuid_str(balance_id)
489
+ self.by: Optional[str] = by
490
+ self.control: Optional[InventoryCountControl] = control
491
+
492
+ def to_dict(self) -> dict:
493
+ obj = {"timestamp": _ts_out(self.timestamp) if isinstance(self.timestamp, datetime) else self.timestamp,
494
+ "type": self.type, "quantity": self.quantity, "stock": self.stock}
495
+ if self.type == HISTORY_COUNT:
496
+ obj.update({"count": self.count, "id": self.id, "by": self.by,
497
+ "control": self.control.to_dict() if self.control is not None else None})
498
+ return obj
499
+
500
+ @classmethod
501
+ def from_dict(cls, data: dict) -> "InventoryHistoryEvent":
502
+ control = data.get("control")
503
+ return cls(timestamp=data.get("timestamp"), type=data.get("type"), quantity=data.get("quantity"),
504
+ stock=data.get("stock"), count=data.get("count"), balance_id=data.get("id"), by=data.get("by"),
505
+ control=InventoryCountControl.from_dict(control) if control else None)
506
+
507
+
508
+ class InventoryHistory:
509
+ """
510
+ How one product's stock moved on one storage datapoint over a period, and whether
511
+ each count matched (DEV-6129, ``POST /dsapi/planner/inventory/history``).
512
+
513
+ ``opening`` is the stock at ``start``, ``closing`` at ``end``; ``events`` are every
514
+ movement and count in between, oldest first, each with the stock after it. A count
515
+ carries its control against the count before it, even when that one is before
516
+ ``start``. A count with no count before it is the opening level and is not checked.
517
+ ``summary`` gives the ``counts``, how many could be ``checked``, how many of those are
518
+ ``verified``, and the ``unexplained`` quantity (the sum of their differences). Past the
519
+ event limit, ``truncated`` is true and only the latest events are kept; the summary
520
+ still covers the whole period.
521
+ """
522
+
523
+ def __init__(self, datapoint: str = None, product: str = None, start=None, end=None, unit=None,
524
+ unit_symbol: str = None, opening: float = None, closing: float = None, events: list = None,
525
+ truncated: bool = False):
526
+ self.datapoint: Optional[str] = datapoint
527
+ self.product: Optional[str] = product
528
+ self.start: Optional[datetime] = _ts_in(start)
529
+ self.end: Optional[datetime] = _ts_in(end)
530
+ self.unit: Optional[str] = _uuid_str(unit)
531
+ self.unit_symbol: Optional[str] = unit_symbol
532
+ self.opening: Optional[float] = opening
533
+ self.closing: Optional[float] = closing
534
+ self.events: list = list(events or [])
535
+ self.truncated: bool = truncated
536
+ self.counts: int = 0
537
+ self.checked: int = 0
538
+ self.verified: int = 0
539
+ self.unexplained: float = 0.0
540
+
541
+ def to_dict(self) -> dict:
542
+ def ts(value):
543
+ return _ts_out(value) if isinstance(value, datetime) else value
544
+ return {"datapoint": self.datapoint, "product": self.product, "unit": self.unit,
545
+ "unitSymbol": self.unit_symbol,
546
+ "opening": {"timestamp": ts(self.start), "quantity": self.opening},
547
+ "closing": {"timestamp": ts(self.end), "quantity": self.closing},
548
+ "events": [e.to_dict() for e in self.events],
549
+ "summary": {"counts": self.counts, "checked": self.checked, "verified": self.verified,
550
+ "unexplained": self.unexplained},
551
+ "truncated": self.truncated}
552
+
553
+ @classmethod
554
+ def from_dict(cls, data: dict) -> "InventoryHistory":
555
+ opening, closing = data.get("opening") or {}, data.get("closing") or {}
556
+ summary = data.get("summary") or {}
557
+ obj = cls(datapoint=data.get("datapoint"), product=data.get("product"), start=opening.get("timestamp"),
558
+ end=closing.get("timestamp"), unit=data.get("unit"), unit_symbol=data.get("unitSymbol"),
559
+ opening=opening.get("quantity"), closing=closing.get("quantity"),
560
+ events=[InventoryHistoryEvent.from_dict(e) for e in data.get("events") or []],
561
+ truncated=bool(data.get("truncated")))
562
+ obj.counts = summary.get("counts", 0)
563
+ obj.checked = summary.get("checked", 0)
564
+ obj.verified = summary.get("verified", 0)
565
+ obj.unexplained = summary.get("unexplained", 0.0)
566
+ return obj
567
+
568
+ def __repr__(self) -> str:
569
+ return (f"InventoryHistory({self.datapoint!r}, {self.product!r}, {len(self.events)} events, "
570
+ f"{self.verified}/{self.checked} checked counts verified)")
@@ -0,0 +1 @@
1
+ __version__ = "2.2.0.dev24"
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wizata_dsapi
3
- Version: 2.2.0.dev22
3
+ Version: 2.2.0.dev24
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -1 +0,0 @@
1
- __version__ = "2.2.0.dev22"