wizata-dsapi 2.2.0.dev19__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.
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/PKG-INFO +1 -1
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/__init__.py +11 -5
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/connection.py +154 -44
- wizata_dsapi-2.2.0.dev21/wizata_dsapi/connection_discovery.py +196 -0
- wizata_dsapi-2.2.0.dev21/wizata_dsapi/connection_policy.py +194 -0
- wizata_dsapi-2.2.0.dev21/wizata_dsapi/connection_types.py +399 -0
- wizata_dsapi-2.2.0.dev21/wizata_dsapi/formula.py +493 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/pipeline.py +70 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/request.py +12 -1
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/scripts/common.py +26 -56
- wizata_dsapi-2.2.0.dev21/wizata_dsapi/version.py +1 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/wizata_dsapi_client.py +42 -3
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi.egg-info/PKG-INFO +1 -1
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi.egg-info/SOURCES.txt +3 -0
- wizata_dsapi-2.2.0.dev19/wizata_dsapi/connection_types.py +0 -325
- wizata_dsapi-2.2.0.dev19/wizata_dsapi/version.py +0 -1
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/LICENSE.txt +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/README.rst +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/setup.cfg +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/setup.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/api_config.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/api_dto.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/api_interface.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/bucket.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/business_label.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/context.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/dashboard.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/dataframe_toolkit.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/datapoint.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/datastore.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/deployment.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/ds_dataframe.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/dsapi_json_encoder.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/edge_config.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/edge_device.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/edge_module.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/edge_package.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/evaluation.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/execution.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/execution_log.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/experiment.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/graylog_log.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/group_system.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/ilogger.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/insight.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/inventory.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/mlmodel.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/mobile_asset.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/model_toolkit.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/models/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/models/common.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/notification.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/paged_query_result.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/pipeline_image.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/plot.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/plots/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/plots/common.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/plots/theme.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/product.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/script.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/scripts/__init__.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/search.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/solution_component.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/streamlit_utils.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/template.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/template_config.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/trigger.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/twin.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/twinregistration.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/user.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/wizard_function.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/wizard_request.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi/words.py +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi.egg-info/dependency_links.txt +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi.egg-info/requires.txt +0 -0
- {wizata_dsapi-2.2.0.dev19 → wizata_dsapi-2.2.0.dev21}/wizata_dsapi.egg-info/top_level.txt +0 -0
|
@@ -5,6 +5,8 @@ from .paged_query_result import PagedQueryResult
|
|
|
5
5
|
from .plot import Plot
|
|
6
6
|
from .mlmodel import ModelInfo, ModelList, MLModelConfig, ModelFile, ModelIdentifierInfo
|
|
7
7
|
from .request import Request, filter_map, RequestGroup, RequestGroupMap, DynamicSelector
|
|
8
|
+
from .formula import (FormulaError, apply_formulas, evaluate_formula, normalize_formulas, check_result_name,
|
|
9
|
+
formula_catalog, FORMULA_OPERATORS, FORMULA_FUNCTIONS, FORMULA_CONSTANTS)
|
|
8
10
|
from .execution import Execution, ExecutionStatus, ExecutionStepLog, AbortedException
|
|
9
11
|
from .execution_log import ExecutionLog
|
|
10
12
|
from .experiment import Experiment
|
|
@@ -45,7 +47,8 @@ from .wizard_function import WizardStep, WizardFunction
|
|
|
45
47
|
from .wizard_request import WizardRequest
|
|
46
48
|
|
|
47
49
|
# Pipeline Entities (Dto)
|
|
48
|
-
from .pipeline import Pipeline, PipelineStep, StepType, WriteConfig, AlertConfig, PipelineIO, RunnerStatus,
|
|
50
|
+
from .pipeline import Pipeline, PipelineStep, StepType, WriteConfig, AlertConfig, FormulaConfig, PipelineIO, RunnerStatus, \
|
|
51
|
+
AlertType
|
|
49
52
|
from .context import Context
|
|
50
53
|
from .ilogger import ILogger
|
|
51
54
|
from .pipeline_image import PipelineImage
|
|
@@ -62,10 +65,13 @@ from .edge_device import EdgeDevice, EdgeDeviceStatus, EdgeDeviceType, EdgeDevic
|
|
|
62
65
|
from .edge_module import EdgeModule, EdgeModuleType, WIZATA_MODULES
|
|
63
66
|
from .edge_config import EdgeConsumer, EdgeWriter, EdgeTrigger, EdgeFile, EdgeConfig, EdgeCommand
|
|
64
67
|
from .edge_package import EdgePackage, EdgePackageOperation, EdgeRelease
|
|
65
|
-
from .connection import Connection,
|
|
66
|
-
EdgeDeviceKey, EdgeDeviceKeyStatus
|
|
67
|
-
from .connection_types import ConnectionType,
|
|
68
|
-
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
|
|
69
75
|
from .product import ProductKind, Product, ProductComponent, BUILTIN_PRODUCT_KINDS, is_valid_product_key
|
|
70
76
|
from .inventory import (InventoryBalance, InventoryStorage, InventoryStorageDatapoint, inventory_mode,
|
|
71
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
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
165
|
+
class ConnectionChannel:
|
|
148
166
|
"""
|
|
149
|
-
|
|
150
|
-
PK ``id`` (UUID)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
- ``
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
- ``
|
|
158
|
-
``
|
|
159
|
-
- ``
|
|
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.
|
|
167
|
-
self.
|
|
168
|
-
self.
|
|
169
|
-
self.
|
|
170
|
-
self.
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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) -> "
|
|
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.
|
|
206
|
-
obj.
|
|
207
|
-
|
|
208
|
-
obj.
|
|
209
|
-
obj.
|
|
210
|
-
|
|
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.
|
|
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
|
|
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})"
|