Add ENLYZE live data gateway
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
"""Verified ENLYZE adapter for production-run and timeseries access."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
|
||||||
|
from production_analytics.calculations.material_consumption import MaterialSample
|
||||||
|
from production_analytics.enlyze.exploration import ExplorationClient
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class EnlyzeProductionRun:
|
||||||
|
uuid: str
|
||||||
|
machine_id: str
|
||||||
|
product_id: str | None
|
||||||
|
production_order: str
|
||||||
|
start: datetime
|
||||||
|
end: datetime | None
|
||||||
|
|
||||||
|
|
||||||
|
class EnlyzeApiGateway:
|
||||||
|
def __init__(self, client: ExplorationClient) -> None:
|
||||||
|
self._client = client
|
||||||
|
|
||||||
|
def get_open_production_run(self, machine_id: str) -> EnlyzeProductionRun | None:
|
||||||
|
response = self._client.get(
|
||||||
|
"/v2/production-runs",
|
||||||
|
{"machine": machine_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
for item in response.body.get("data", []):
|
||||||
|
if item.get("end") is not None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return EnlyzeProductionRun(
|
||||||
|
uuid=str(item["uuid"]),
|
||||||
|
machine_id=str(item["machine"]),
|
||||||
|
product_id=(
|
||||||
|
str(item["product"])
|
||||||
|
if item.get("product") is not None
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
production_order=str(item["production_order"]),
|
||||||
|
start=_parse_timestamp(item["start"]),
|
||||||
|
end=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_material_samples(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
machine_id: str,
|
||||||
|
rate_variable_id: str,
|
||||||
|
gate_variable_id: str,
|
||||||
|
start: datetime,
|
||||||
|
end: datetime,
|
||||||
|
) -> list[MaterialSample]:
|
||||||
|
response = self._client.post_json(
|
||||||
|
"/v2/timeseries",
|
||||||
|
{
|
||||||
|
"machine": machine_id,
|
||||||
|
"start": start.astimezone(UTC).isoformat(),
|
||||||
|
"end": end.astimezone(UTC).isoformat(),
|
||||||
|
"variables": [
|
||||||
|
{"uuid": rate_variable_id},
|
||||||
|
{"uuid": gate_variable_id},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
data = response.body["data"]
|
||||||
|
columns = data["columns"]
|
||||||
|
|
||||||
|
time_index = columns.index("time")
|
||||||
|
rate_index = columns.index(rate_variable_id)
|
||||||
|
gate_index = columns.index(gate_variable_id)
|
||||||
|
|
||||||
|
return [
|
||||||
|
MaterialSample(
|
||||||
|
timestamp=_parse_timestamp(record[time_index]),
|
||||||
|
material_rate_kg_per_hour=float(record[rate_index]),
|
||||||
|
gate_value=float(record[gate_index]),
|
||||||
|
)
|
||||||
|
for record in data["records"]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_timestamp(value: str) -> datetime:
|
||||||
|
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(UTC)
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
from datetime import UTC, datetime
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from production_analytics.enlyze.gateway import EnlyzeApiGateway
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_open_production_run_returns_current_run() -> None:
|
||||||
|
client = Mock()
|
||||||
|
client.get.return_value.body = {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"uuid": "closed-run",
|
||||||
|
"machine": "machine-1",
|
||||||
|
"product": "product-a",
|
||||||
|
"production_order": "ORDER-1",
|
||||||
|
"start": "2026-09-03T04:00:00Z",
|
||||||
|
"end": "2026-09-03T05:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "open-run",
|
||||||
|
"machine": "machine-1",
|
||||||
|
"product": "product-b",
|
||||||
|
"production_order": "ORDER-2",
|
||||||
|
"start": "2026-09-03T05:07:47Z",
|
||||||
|
"end": None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
gateway = EnlyzeApiGateway(client)
|
||||||
|
|
||||||
|
run = gateway.get_open_production_run("machine-1")
|
||||||
|
|
||||||
|
assert run is not None
|
||||||
|
assert run.uuid == "open-run"
|
||||||
|
assert run.machine_id == "machine-1"
|
||||||
|
assert run.product_id == "product-b"
|
||||||
|
assert run.production_order == "ORDER-2"
|
||||||
|
assert run.start == datetime(2026, 9, 3, 5, 7, 47, tzinfo=UTC)
|
||||||
|
assert run.end is None
|
||||||
|
|
||||||
|
client.get.assert_called_once_with(
|
||||||
|
"/v2/production-runs",
|
||||||
|
{"machine": "machine-1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_open_production_run_returns_none_without_open_run() -> None:
|
||||||
|
client = Mock()
|
||||||
|
client.get.return_value.body = {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"uuid": "closed-run",
|
||||||
|
"machine": "machine-1",
|
||||||
|
"product": "product-a",
|
||||||
|
"production_order": "ORDER-1",
|
||||||
|
"start": "2026-09-03T04:00:00Z",
|
||||||
|
"end": "2026-09-03T05:00:00Z",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
gateway = EnlyzeApiGateway(client)
|
||||||
|
|
||||||
|
assert gateway.get_open_production_run("machine-1") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_material_samples_requests_rate_and_gate_together() -> None:
|
||||||
|
client = Mock()
|
||||||
|
client.post_json.return_value.body = {
|
||||||
|
"data": {
|
||||||
|
"columns": ["time", "rate-variable", "gate-variable"],
|
||||||
|
"records": [
|
||||||
|
["2026-09-03T05:09:50Z", 1028.5, 2.1],
|
||||||
|
["2026-09-03T05:10:00Z", 1029.0, 2.2],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gateway = EnlyzeApiGateway(client)
|
||||||
|
|
||||||
|
samples = gateway.get_material_samples(
|
||||||
|
machine_id="machine-1",
|
||||||
|
rate_variable_id="rate-variable",
|
||||||
|
gate_variable_id="gate-variable",
|
||||||
|
start=datetime(2026, 9, 3, 5, 9, 50, tzinfo=UTC),
|
||||||
|
end=datetime(2026, 9, 3, 5, 10, 10, tzinfo=UTC),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(samples) == 2
|
||||||
|
assert samples[0].timestamp == datetime(2026, 9, 3, 5, 9, 50, tzinfo=UTC)
|
||||||
|
assert samples[0].material_rate_kg_per_hour == 1028.5
|
||||||
|
assert samples[0].gate_value == 2.1
|
||||||
|
assert samples[1].material_rate_kg_per_hour == 1029.0
|
||||||
|
assert samples[1].gate_value == 2.2
|
||||||
|
|
||||||
|
client.post_json.assert_called_once_with(
|
||||||
|
"/v2/timeseries",
|
||||||
|
{
|
||||||
|
"machine": "machine-1",
|
||||||
|
"start": "2026-09-03T05:09:50+00:00",
|
||||||
|
"end": "2026-09-03T05:10:10+00:00",
|
||||||
|
"variables": [
|
||||||
|
{"uuid": "rate-variable"},
|
||||||
|
{"uuid": "gate-variable"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_material_samples_uses_column_names_not_fixed_positions() -> None:
|
||||||
|
client = Mock()
|
||||||
|
client.post_json.return_value.body = {
|
||||||
|
"data": {
|
||||||
|
"columns": ["time", "gate-variable", "rate-variable"],
|
||||||
|
"records": [
|
||||||
|
["2026-09-03T05:09:50Z", 2.1, 1028.5],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gateway = EnlyzeApiGateway(client)
|
||||||
|
|
||||||
|
samples = gateway.get_material_samples(
|
||||||
|
machine_id="machine-1",
|
||||||
|
rate_variable_id="rate-variable",
|
||||||
|
gate_variable_id="gate-variable",
|
||||||
|
start=datetime(2026, 9, 3, 5, 9, 50, tzinfo=UTC),
|
||||||
|
end=datetime(2026, 9, 3, 5, 10, 0, tzinfo=UTC),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert samples[0].material_rate_kg_per_hour == 1028.5
|
||||||
|
assert samples[0].gate_value == 2.1
|
||||||
Reference in New Issue
Block a user