wizata-dsapi 2.2.0.dev20__tar.gz → 2.2.0.dev21__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 (76) hide show
  1. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/PKG-INFO +1 -1
  2. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/__init__.py +7 -4
  3. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/connection.py +154 -44
  4. wizata_dsapi-2.2.0.dev21/wizata_dsapi/connection_discovery.py +196 -0
  5. wizata_dsapi-2.2.0.dev21/wizata_dsapi/connection_policy.py +194 -0
  6. wizata_dsapi-2.2.0.dev21/wizata_dsapi/connection_types.py +399 -0
  7. wizata_dsapi-2.2.0.dev21/wizata_dsapi/version.py +1 -0
  8. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/wizata_dsapi_client.py +31 -0
  9. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi.egg-info/PKG-INFO +1 -1
  10. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi.egg-info/SOURCES.txt +2 -0
  11. wizata_dsapi-2.2.0.dev20/wizata_dsapi/connection_types.py +0 -325
  12. wizata_dsapi-2.2.0.dev20/wizata_dsapi/version.py +0 -1
  13. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/LICENSE.txt +0 -0
  14. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/README.rst +0 -0
  15. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/setup.cfg +0 -0
  16. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/setup.py +0 -0
  17. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/api_config.py +0 -0
  18. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/api_dto.py +0 -0
  19. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/api_interface.py +0 -0
  20. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/bucket.py +0 -0
  21. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/business_label.py +0 -0
  22. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/context.py +0 -0
  23. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/dashboard.py +0 -0
  24. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/dataframe_toolkit.py +0 -0
  25. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/datapoint.py +0 -0
  26. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/datastore.py +0 -0
  27. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/deployment.py +0 -0
  28. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/ds_dataframe.py +0 -0
  29. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/dsapi_json_encoder.py +0 -0
  30. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/edge_config.py +0 -0
  31. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/edge_device.py +0 -0
  32. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/edge_module.py +0 -0
  33. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/edge_package.py +0 -0
  34. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/evaluation.py +0 -0
  35. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/execution.py +0 -0
  36. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/execution_log.py +0 -0
  37. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/experiment.py +0 -0
  38. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/formula.py +0 -0
  39. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/graylog_log.py +0 -0
  40. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/group_system.py +0 -0
  41. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/ilogger.py +0 -0
  42. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/insight.py +0 -0
  43. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/inventory.py +0 -0
  44. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/mlmodel.py +0 -0
  45. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/mobile_asset.py +0 -0
  46. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/model_toolkit.py +0 -0
  47. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/models/__init__.py +0 -0
  48. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/models/common.py +0 -0
  49. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/notification.py +0 -0
  50. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/paged_query_result.py +0 -0
  51. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/pipeline.py +0 -0
  52. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/pipeline_image.py +0 -0
  53. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/plot.py +0 -0
  54. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/plots/__init__.py +0 -0
  55. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/plots/common.py +0 -0
  56. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/plots/theme.py +0 -0
  57. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/product.py +0 -0
  58. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/request.py +0 -0
  59. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/script.py +0 -0
  60. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/scripts/__init__.py +0 -0
  61. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/scripts/common.py +0 -0
  62. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/search.py +0 -0
  63. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/solution_component.py +0 -0
  64. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/streamlit_utils.py +0 -0
  65. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/template.py +0 -0
  66. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/template_config.py +0 -0
  67. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/trigger.py +0 -0
  68. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/twin.py +0 -0
  69. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/twinregistration.py +0 -0
  70. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/user.py +0 -0
  71. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/wizard_function.py +0 -0
  72. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/wizard_request.py +0 -0
  73. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/words.py +0 -0
  74. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
  75. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi.egg-info/requires.txt +0 -0
  76. {wizata_dsapi-2.2.0.dev20 → wizata_dsapi-2.2.0.dev21}/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.dev20
3
+ Version: 2.2.0.dev21
4
4
  Summary: Wizata Data Science Toolkit
5
5
  Author: Wizata S.A.
6
6
  Author-email: info@wizata.com
@@ -65,10 +65,13 @@ from .edge_device import EdgeDevice, EdgeDeviceStatus, EdgeDeviceType, EdgeDevic
65
65
  from .edge_module import EdgeModule, EdgeModuleType, WIZATA_MODULES
66
66
  from .edge_config import EdgeConsumer, EdgeWriter, EdgeTrigger, EdgeFile, EdgeConfig, EdgeCommand
67
67
  from .edge_package import EdgePackage, EdgePackageOperation, EdgeRelease
68
- from .connection import Connection, ConnectionMapping, ConnectionDirection, TransformKind, \
69
- EdgeDeviceKey, EdgeDeviceKeyStatus
70
- from .connection_types import ConnectionType, CONNECTION_TYPES, TRANSFORM_SCHEMAS, get_connection_type, \
71
- list_connection_types
68
+ from .connection import Connection, ConnectionChannel, ConnectionSignal, ConnectionDirection, \
69
+ TransformKind, EdgeDeviceKey, EdgeDeviceKeyStatus
70
+ from .connection_types import ConnectionType, ChannelType, CONNECTION_TYPES, SYSTEM_TYPES, \
71
+ TRANSFORM_SCHEMAS, get_connection_type, list_connection_types
72
+ from .connection_policy import DatapointPolicy, PolicyError, canonical_node_id
73
+ from .connection_discovery import ConnectionSeenName, ConnectionSeenReport, ConnectionBrowse, \
74
+ SEEN_OUTCOMES, BROWSE_REQUESTED, BROWSE_STATUSES, BROWSE_EXPIRES_SECONDS
72
75
  from .product import ProductKind, Product, ProductComponent, BUILTIN_PRODUCT_KINDS, is_valid_product_key
73
76
  from .inventory import (InventoryBalance, InventoryStorage, InventoryStorageDatapoint, inventory_mode,
74
77
  InventoryStock, InventoryStockProduct, InventoryStockLine, InventoryStockCount, INVENTORY_OPTION, INVENTORY_MODES,
@@ -2,16 +2,18 @@
2
2
  Edge connection DTOs (DEV-6013).
3
3
 
4
4
  An edge connector used to be one untyped dict in the device's ``consumers.json``,
5
- credentials inline. It is now split in two entities, the same for every type:
5
+ credentials inline. It is now (12.2 redesign) three entities, the same for every type:
6
6
 
7
7
  Connection — how to reach a source or a target (endpoint, credentials, certificates),
8
- bound to one edge device. ``config`` is clear; ``secrets`` holds one sealed envelope
9
- per secret field, readable by the owning edge only.
8
+ bound to one edge device and entered once. ``config`` is clear; ``secrets`` holds one
9
+ sealed envelope per secret field, readable by the owning edge only. ``cloud`` (hub) and
10
+ ``edge`` (tsdb) are the device's built-in connections.
10
11
 
11
- ConnectionMapping — one thing to read or write through a connection (an OPC node, an
12
- MQTT topic, a Modbus register, an S7 block entry, a queue). ``address`` and
13
- ``options`` are shaped by the connection type (see ``connection_types``); the other
14
- fields are common to every type.
12
+ ConnectionChannel — a reader or a writer of a connection: what it reads or writes
13
+ (``spec``, shaped by the type) and its datapoint policy (all / list / mapping).
14
+
15
+ ConnectionSignal — one row of a policy: a reader's name and the datapoint it goes on as,
16
+ or a datapoint and where a writer sends it.
15
17
 
16
18
  EdgeDeviceKey — the public half of the key pair an edge generated for itself. Secrets
17
19
  are sealed for the edge's active key; rotating adds a row and retires the previous.
@@ -74,6 +76,17 @@ class Connection:
74
76
  self.secrets: dict = {}
75
77
  self.created_at: Optional[datetime] = None
76
78
  self.updated_at: Optional[datetime] = None
79
+ # read-only summaries a search adds: [ConnectionChannel], without their rows
80
+ self.readers: Optional[list] = None
81
+ self.writers: Optional[list] = None
82
+
83
+ @property
84
+ def system(self) -> bool:
85
+ """A device's built-in ``cloud`` / ``edge`` connection (DEV-6013): created by the
86
+ platform, never deleted, only its writer's policy edited."""
87
+ from .connection_types import get_connection_type
88
+ connection_type = get_connection_type(self.connection_type)
89
+ return bool(connection_type and connection_type.system)
77
90
 
78
91
  def secrets_set(self) -> list:
79
92
  """Names of the secret fields holding a value."""
@@ -99,6 +112,7 @@ class Connection:
99
112
  obj["connectionType"] = self.connection_type
100
113
  if self.edge_device_id is not None:
101
114
  obj["edgeDeviceId"] = self.edge_device_id
115
+ obj["system"] = self.system
102
116
  obj["config"] = dict(self.config or {})
103
117
  if include_secrets:
104
118
  obj["secrets"] = dict(self.secrets or {})
@@ -113,6 +127,10 @@ class Connection:
113
127
  obj["createdAt"] = _ts_out(self.created_at)
114
128
  if self.updated_at is not None:
115
129
  obj["updatedAt"] = _ts_out(self.updated_at)
130
+ if self.readers is not None:
131
+ obj["readers"] = [c.to_dict() for c in self.readers]
132
+ if self.writers is not None:
133
+ obj["writers"] = [c.to_dict() for c in self.writers]
116
134
  return obj
117
135
 
118
136
  @classmethod
@@ -144,50 +162,70 @@ class Connection:
144
162
  f"type={self.connection_type!r}, edge={self.edge_device_id!r})")
145
163
 
146
164
 
147
- class ConnectionMapping:
165
+ class ConnectionChannel:
148
166
  """
149
- ConnectionMapping entity stored in PostgreSQL ``"ConnectionMapping"`` table.
150
- PK ``id`` (UUID). FK ``connection_id`` → ``Connection.id``, cascade on delete.
151
-
152
- - ``direction``: READ (source → platform) or WRITE (platform → target).
153
- - ``address``: where, shaped by the type (``{"node_id": ...}``, ``{"topic": ...}``).
154
- - ``hardware_id``: the datapoint. Optional where the source names it (OPC browse
155
- with ``children``, a topic function fanning out to many datapoints).
156
- - ``poll_interval_ms``: polling cadence for polled types; ignored when pushed.
157
- - ``transform``: ``{"kind": "expression", "expression": "x / 10"}`` or
158
- ``{"kind": "function", "filepath": ..., "function_name": ...}``.
159
- - ``options``: type-specific tuning (``qos``, ``batch_size``, ``pack``...).
167
+ A reader or a writer of a connection (DEV-6013, 12.2 redesign), stored in PostgreSQL
168
+ ``"ConnectionChannel"``. PK ``id`` (UUID); FK ``connection_id`` → ``Connection.id``,
169
+ cascade on delete.
170
+
171
+ - ``direction``: READ (a reader, a consumers.json entry) or WRITE (a writer, a
172
+ writers.json entry).
173
+ - ``key``: unique per device and direction; it is the consumer or writer ``id`` on the
174
+ edge, so it is what the edge's logs and seen names call it.
175
+ - ``spec``: what it reads or writes, shaped by the connection type
176
+ (``connection_types``): OPC nodes and cadence, MQTT topics and function, a queue...
177
+ - ``policy_mode`` / ``prefix``: its datapoint policy (``connection_policy``); the rows
178
+ of the policy are its ``ConnectionSignal`` s.
179
+ - ``enabled``: a disabled reader or writer is not rendered.
180
+ - ``signals``: on create only, the policy rows to write with it (read back from search).
181
+ - ``signal_count`` / ``mapped_count``: read-only, how many rows and how many name a
182
+ datapoint.
160
183
  """
161
184
 
162
185
  def __init__(self):
163
186
  self.id: Optional[str] = None
164
187
  self.connection_id: Optional[str] = None
188
+ self.edge_device_id: Optional[str] = None
165
189
  self.direction: ConnectionDirection = ConnectionDirection.READ
166
- self.address: dict = {}
167
- self.hardware_id: Optional[str] = None
168
- self.poll_interval_ms: Optional[int] = None
169
- self.transform: Optional[dict] = None
170
- self.options: dict = {}
190
+ self.key: Optional[str] = None
191
+ self.name: Optional[str] = None
192
+ self.enabled: bool = True
193
+ self.spec: dict = {}
194
+ self.policy_mode: Optional[str] = None
195
+ self.prefix: Optional[str] = None
196
+ self.signals: Optional[list] = None
197
+ self.signal_count: Optional[int] = None
198
+ self.mapped_count: Optional[int] = None
171
199
  self.created_at: Optional[datetime] = None
172
200
  self.updated_at: Optional[datetime] = None
173
201
 
202
+ @property
203
+ def is_reader(self) -> bool:
204
+ return ConnectionDirection(self.direction) == ConnectionDirection.READ
205
+
174
206
  def to_dict(self) -> dict:
175
207
  obj = {}
176
208
  if self.id is not None:
177
209
  obj["id"] = str(self.id)
178
210
  if self.connection_id is not None:
179
211
  obj["connectionId"] = str(self.connection_id)
180
- obj["direction"] = (self.direction.value
181
- if isinstance(self.direction, ConnectionDirection)
212
+ if self.edge_device_id is not None:
213
+ obj["edgeDeviceId"] = self.edge_device_id
214
+ obj["direction"] = (self.direction.value if isinstance(self.direction, ConnectionDirection)
182
215
  else self.direction)
183
- obj["address"] = dict(self.address or {})
184
- if self.hardware_id is not None:
185
- obj["hardwareId"] = self.hardware_id
186
- if self.poll_interval_ms is not None:
187
- obj["pollIntervalMs"] = self.poll_interval_ms
188
- if self.transform is not None:
189
- obj["transform"] = dict(self.transform)
190
- obj["options"] = dict(self.options or {})
216
+ if self.key is not None:
217
+ obj["key"] = self.key
218
+ if self.name is not None:
219
+ obj["name"] = self.name
220
+ obj["enabled"] = bool(self.enabled)
221
+ obj["spec"] = dict(self.spec or {})
222
+ obj["policy"] = {"mode": self.policy_mode, "prefix": self.prefix or ""}
223
+ if self.signals is not None:
224
+ obj["signals"] = [s.to_dict() if hasattr(s, "to_dict") else dict(s) for s in self.signals]
225
+ if self.signal_count is not None:
226
+ obj["signalCount"] = self.signal_count
227
+ if self.mapped_count is not None:
228
+ obj["mappedCount"] = self.mapped_count
191
229
  if self.created_at is not None:
192
230
  obj["createdAt"] = _ts_out(self.created_at)
193
231
  if self.updated_at is not None:
@@ -195,19 +233,32 @@ class ConnectionMapping:
195
233
  return obj
196
234
 
197
235
  @classmethod
198
- def from_dict(cls, data: dict) -> "ConnectionMapping":
236
+ def from_dict(cls, data: dict) -> "ConnectionChannel":
199
237
  obj = cls()
200
238
  obj.id = data.get("id")
201
239
  obj.connection_id = data.get("connectionId") or data.get("connection_id")
240
+ obj.edge_device_id = data.get("edgeDeviceId") or data.get("edge_device_id")
202
241
  raw_direction = data.get("direction")
203
242
  if raw_direction is not None:
204
243
  obj.direction = ConnectionDirection(str(raw_direction).lower())
205
- obj.address = data.get("address") or {}
206
- obj.hardware_id = data.get("hardwareId") or data.get("hardware_id")
207
- poll = data.get("pollIntervalMs", data.get("poll_interval_ms"))
208
- obj.poll_interval_ms = int(poll) if poll is not None else None
209
- obj.transform = data.get("transform")
210
- obj.options = data.get("options") or {}
244
+ obj.key = data.get("key")
245
+ obj.name = data.get("name")
246
+ enabled = data.get("enabled")
247
+ obj.enabled = True if enabled is None else bool(enabled)
248
+ obj.spec = data.get("spec") or {}
249
+ policy = data.get("policy")
250
+ if isinstance(policy, dict):
251
+ obj.policy_mode = policy.get("mode")
252
+ obj.prefix = policy.get("prefix")
253
+ else:
254
+ obj.policy_mode = data.get("policyMode") or data.get("policy_mode")
255
+ obj.prefix = data.get("prefix")
256
+ signals = data.get("signals")
257
+ if signals is not None:
258
+ obj.signals = [s if isinstance(s, ConnectionSignal) else ConnectionSignal.from_dict(s)
259
+ for s in signals]
260
+ obj.signal_count = data.get("signalCount")
261
+ obj.mapped_count = data.get("mappedCount")
211
262
  obj.created_at = _ts_in(data.get("createdAt") or data.get("created_at"))
212
263
  obj.updated_at = _ts_in(data.get("updatedAt") or data.get("updated_at"))
213
264
  return obj
@@ -216,14 +267,73 @@ class ConnectionMapping:
216
267
  return str(self.id) if self.id else None
217
268
 
218
269
  def api_key(self) -> str:
219
- return self.api_id()
270
+ return self.key
271
+
272
+ def set_id(self, value):
273
+ self.id = str(value)
274
+
275
+ def __repr__(self) -> str:
276
+ return (f"ConnectionChannel(id={self.id!r}, key={self.key!r}, direction={self.direction!r}, "
277
+ f"connection={self.connection_id!r}, policy={self.policy_mode!r})")
278
+
279
+
280
+ class ConnectionSignal:
281
+ """
282
+ One row of a reader's or writer's datapoint policy (DEV-6013, 12.2 redesign), stored in
283
+ PostgreSQL ``"ConnectionSignal"``. PK ``id`` (UUID); FK ``channel_id`` →
284
+ ``ConnectionChannel.id``, cascade on delete. Unique per channel: ``tag``, and
285
+ ``hardware_id`` when set.
286
+
287
+ - For a **reader**, ``tag`` is the reader's own name for what it read (an OPC node id,
288
+ a point name, a function's output) and ``hardware_id`` the datapoint it goes on as.
289
+ In a ``list`` policy a row with no ``hardware_id`` lets the tag through (prefixed).
290
+ - For a **writer**, ``hardware_id`` is the datapoint and ``tag`` where it goes (the OPC
291
+ node, the name written). In a ``list`` policy a row with no ``tag`` lets the
292
+ datapoint through.
293
+ - A row with both always renames, whatever the mode.
294
+ """
295
+
296
+ def __init__(self, tag: str = None, hardware_id: str = None):
297
+ self.id: Optional[str] = None
298
+ self.channel_id: Optional[str] = None
299
+ self.tag: Optional[str] = tag
300
+ self.hardware_id: Optional[str] = hardware_id
301
+ self.created_at: Optional[datetime] = None
302
+ self.updated_at: Optional[datetime] = None
303
+
304
+ def to_dict(self) -> dict:
305
+ obj = {}
306
+ if self.id is not None:
307
+ obj["id"] = str(self.id)
308
+ if self.channel_id is not None:
309
+ obj["channelId"] = str(self.channel_id)
310
+ obj["tag"] = self.tag
311
+ obj["hardwareId"] = self.hardware_id
312
+ if self.created_at is not None:
313
+ obj["createdAt"] = _ts_out(self.created_at)
314
+ if self.updated_at is not None:
315
+ obj["updatedAt"] = _ts_out(self.updated_at)
316
+ return obj
317
+
318
+ @classmethod
319
+ def from_dict(cls, data: dict) -> "ConnectionSignal":
320
+ obj = cls()
321
+ obj.id = data.get("id")
322
+ obj.channel_id = data.get("channelId") or data.get("channel_id")
323
+ obj.tag = data.get("tag")
324
+ obj.hardware_id = data.get("hardwareId") or data.get("hardware_id")
325
+ obj.created_at = _ts_in(data.get("createdAt") or data.get("created_at"))
326
+ obj.updated_at = _ts_in(data.get("updatedAt") or data.get("updated_at"))
327
+ return obj
328
+
329
+ def api_id(self) -> str:
330
+ return str(self.id) if self.id else None
220
331
 
221
332
  def set_id(self, value):
222
333
  self.id = str(value)
223
334
 
224
335
  def __repr__(self) -> str:
225
- return (f"ConnectionMapping(id={self.id!r}, connection={self.connection_id!r}, "
226
- f"direction={self.direction!r}, hardware_id={self.hardware_id!r})")
336
+ return f"ConnectionSignal(tag={self.tag!r}, hardware_id={self.hardware_id!r})"
227
337
 
228
338
 
229
339
  class EdgeDeviceKey:
@@ -0,0 +1,196 @@
1
+ """
2
+ What an edge tells the platform about its connections (DEV-6013, release 12.2):
3
+
4
+ - **seen names**: every name a reader produced, and what its policy did with it. A new
5
+ reader lets nothing through (an empty list), so this is how a user sees what to allow.
6
+ **Names, counts and times, never values**: what a policy dropped must not leave the edge
7
+ through the report.
8
+ - **browse results**: the nodes of an OPC-UA server, browsed by the edge on a platform
9
+ request (``browse_opc`` command) or by hand on a manual edge (``browse.py``).
10
+
11
+ Stored in PostgreSQL (alembic 0029): ``"ConnectionSeenName"``, ``"ConnectionSeenReport"``,
12
+ ``"ConnectionBrowse"``.
13
+ """
14
+ import uuid
15
+ from datetime import datetime
16
+ from typing import Optional
17
+
18
+ from .connection import _ts_in, _ts_out
19
+
20
+ SEEN_OUTCOMES = ("passed", "renamed", "dropped")
21
+
22
+ BROWSE_REQUESTED = "requested"
23
+ BROWSE_STATUSES = (BROWSE_REQUESTED, "ok", "partial", "failed", "refused")
24
+ #: A request the edge never answered reads as expired after this (the command is served
25
+ #: 15 minutes, a browse runs up to 5, and its upload is retried up to 15).
26
+ BROWSE_EXPIRES_SECONDS = 3600
27
+
28
+
29
+ class ConnectionSeenName:
30
+ """
31
+ One name a reader produced (PK ``channel_id`` + ``name``), as the edge last reported it.
32
+
33
+ - ``name``: the reader's native name (OPC node id or display name, a point name, what a
34
+ function emitted), before its policy.
35
+ - ``hardware_id`` / ``outcome``: what the policy the edge ran made of it (``passed``,
36
+ ``renamed``, ``dropped``); ``hardware_id`` is None when dropped.
37
+ - ``count``: messages since the edge's reader started; ``first_seen`` / ``last_seen``.
38
+ - ``value_type``: ``number``, ``text``, ``bool``... never the value.
39
+ - ``current_hardware_id`` / ``current_outcome``: read-only, what the policy stored now
40
+ makes of the name (the edge applies it on its next sync).
41
+ """
42
+
43
+ def __init__(self, channel_id: str = None, name: str = None):
44
+ self.channel_id: Optional[str] = channel_id
45
+ self.name: Optional[str] = name
46
+ self.hardware_id: Optional[str] = None
47
+ self.outcome: Optional[str] = None
48
+ self.count: int = 0
49
+ self.first_seen: Optional[datetime] = None
50
+ self.last_seen: Optional[datetime] = None
51
+ self.value_type: Optional[str] = None
52
+ self.reported_at: Optional[datetime] = None
53
+ self.current_hardware_id: Optional[str] = None
54
+ self.current_outcome: Optional[str] = None
55
+
56
+ def to_dict(self) -> dict:
57
+ obj = {"channelId": str(self.channel_id) if self.channel_id else None, "name": self.name,
58
+ "hardwareId": self.hardware_id, "outcome": self.outcome, "count": self.count,
59
+ "firstSeen": _ts_out(self.first_seen), "lastSeen": _ts_out(self.last_seen),
60
+ "valueType": self.value_type, "reportedAt": _ts_out(self.reported_at)}
61
+ if self.current_outcome is not None:
62
+ obj["current"] = {"hardwareId": self.current_hardware_id, "outcome": self.current_outcome}
63
+ return obj
64
+
65
+ @classmethod
66
+ def from_dict(cls, data: dict) -> "ConnectionSeenName":
67
+ obj = cls(channel_id=data.get("channelId"), name=data.get("name"))
68
+ obj.hardware_id = data.get("hardwareId")
69
+ obj.outcome = data.get("outcome")
70
+ obj.count = int(data.get("count") or 0)
71
+ obj.first_seen = _ts_in(data.get("firstSeen"))
72
+ obj.last_seen = _ts_in(data.get("lastSeen"))
73
+ obj.value_type = data.get("valueType")
74
+ obj.reported_at = _ts_in(data.get("reportedAt"))
75
+ return obj
76
+
77
+ def __repr__(self) -> str:
78
+ return f"ConnectionSeenName(name={self.name!r}, outcome={self.outcome!r})"
79
+
80
+
81
+ class ConnectionSeenReport:
82
+ """
83
+ The last report of one reader (PK ``channel_id``): the digest of the policy the edge
84
+ ran, so a user sees whether the edge applies the stored one yet; names beyond the
85
+ edge's cap (``overflow``) and messages it could not name (``invalid``).
86
+ """
87
+
88
+ def __init__(self, channel_id: str = None):
89
+ self.channel_id: Optional[str] = channel_id
90
+ self.policy_digest: Optional[str] = None
91
+ self.overflow: int = 0
92
+ self.invalid: int = 0
93
+ self.reported_at: Optional[datetime] = None
94
+
95
+ def to_dict(self) -> dict:
96
+ return {"channelId": str(self.channel_id) if self.channel_id else None,
97
+ "policyDigest": self.policy_digest, "overflow": self.overflow,
98
+ "invalid": self.invalid, "reportedAt": _ts_out(self.reported_at)}
99
+
100
+ @classmethod
101
+ def from_dict(cls, data: dict) -> "ConnectionSeenReport":
102
+ obj = cls(channel_id=data.get("channelId"))
103
+ obj.policy_digest = data.get("policyDigest")
104
+ obj.overflow = int(data.get("overflow") or 0)
105
+ obj.invalid = int(data.get("invalid") or 0)
106
+ obj.reported_at = _ts_in(data.get("reportedAt"))
107
+ return obj
108
+
109
+
110
+ class ConnectionBrowse:
111
+ """
112
+ One OPC-UA browse of a connection (PK ``id``, the command id the edge answers with).
113
+
114
+ - ``source``: ``platform`` (requested here, sent as a ``browse_opc`` command) or
115
+ ``edge`` (run by hand on a manual edge and uploaded).
116
+ - ``status``: ``requested`` until the edge answers, then the edge's ``ok``,
117
+ ``partial`` (deadline or node cap reached), ``failed`` or ``refused``. Read as
118
+ ``expired`` when a request got no answer within ``BROWSE_EXPIRES_SECONDS``.
119
+ - ``roots``, ``depth``, ``max_nodes``, ``timeout_seconds``: what was asked (the edge
120
+ clamps them).
121
+ - ``server``: ``{url, namespaces}``; ``nodes``: what the browser returned (node id,
122
+ names, class, data type, access, ``readable``, ``hasChildren``, parent, depth).
123
+ - ``pages`` / ``complete``: the edge uploads at most 1000 nodes per page; ``complete``
124
+ once its last page arrived.
125
+ """
126
+
127
+ def __init__(self, id: str = None, connection_id: str = None, edge_device_id: str = None):
128
+ self.id: Optional[str] = id
129
+ self.connection_id: Optional[str] = connection_id
130
+ self.edge_device_id: Optional[str] = edge_device_id
131
+ self.source: str = "platform"
132
+ self.status: str = BROWSE_REQUESTED
133
+ self.roots: Optional[list] = None
134
+ self.depth: Optional[int] = None
135
+ self.max_nodes: Optional[int] = None
136
+ self.timeout_seconds: Optional[int] = None
137
+ self.requested_by: Optional[str] = None
138
+ self.requested_at: Optional[datetime] = None
139
+ self.started_at: Optional[str] = None
140
+ self.finished_at: Optional[str] = None
141
+ self.error: Optional[str] = None
142
+ self.truncated: bool = False
143
+ self.server: Optional[dict] = None
144
+ self.nodes: list = []
145
+ self.node_count: int = 0
146
+ self.pages: int = 0
147
+ self.complete: bool = False
148
+
149
+ def api_id(self) -> str:
150
+ return str(self.id) if self.id else None
151
+
152
+ def set_id(self, value):
153
+ self.id = str(value)
154
+
155
+ def get_id_type(self) -> type:
156
+ return uuid.UUID
157
+
158
+ def to_dict(self, include_nodes: bool = True) -> dict:
159
+ obj = {"id": str(self.id) if self.id else None,
160
+ "connectionId": str(self.connection_id) if self.connection_id else None,
161
+ "edgeDeviceId": self.edge_device_id, "source": self.source, "status": self.status,
162
+ "roots": self.roots, "depth": self.depth, "maxNodes": self.max_nodes,
163
+ "timeoutSeconds": self.timeout_seconds, "requestedBy": self.requested_by,
164
+ "requestedAt": _ts_out(self.requested_at), "startedAt": self.started_at,
165
+ "finishedAt": self.finished_at, "error": self.error, "truncated": bool(self.truncated),
166
+ "server": self.server, "nodeCount": self.node_count, "pages": self.pages,
167
+ "complete": bool(self.complete)}
168
+ if include_nodes:
169
+ obj["nodes"] = list(self.nodes or [])
170
+ return obj
171
+
172
+ @classmethod
173
+ def from_dict(cls, data: dict) -> "ConnectionBrowse":
174
+ obj = cls(id=data.get("id"), connection_id=data.get("connectionId"),
175
+ edge_device_id=data.get("edgeDeviceId"))
176
+ obj.source = data.get("source") or "platform"
177
+ obj.status = data.get("status") or BROWSE_REQUESTED
178
+ obj.roots = data.get("roots")
179
+ obj.depth = data.get("depth")
180
+ obj.max_nodes = data.get("maxNodes")
181
+ obj.timeout_seconds = data.get("timeoutSeconds")
182
+ obj.requested_by = data.get("requestedBy")
183
+ obj.requested_at = _ts_in(data.get("requestedAt"))
184
+ obj.started_at = data.get("startedAt")
185
+ obj.finished_at = data.get("finishedAt")
186
+ obj.error = data.get("error")
187
+ obj.truncated = bool(data.get("truncated"))
188
+ obj.server = data.get("server")
189
+ obj.nodes = list(data.get("nodes") or [])
190
+ obj.node_count = int(data.get("nodeCount") or len(obj.nodes))
191
+ obj.pages = int(data.get("pages") or 0)
192
+ obj.complete = bool(data.get("complete"))
193
+ return obj
194
+
195
+ def __repr__(self) -> str:
196
+ return f"ConnectionBrowse(id={self.id!r}, status={self.status!r}, nodes={self.node_count})"