Handle ENLYZE timeseries pagination
This commit is contained in:
@@ -68,52 +68,68 @@ class EnlyzeApiGateway:
|
||||
raise ValueError(f"{name} must be timezone-aware")
|
||||
if end < start:
|
||||
raise ValueError("end must not precede start")
|
||||
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},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
data = response.body["data"]
|
||||
columns = data["columns"]
|
||||
if not isinstance(columns, list):
|
||||
raise ValueError("columns must be a list")
|
||||
for required in ("time", rate_variable_id, gate_variable_id):
|
||||
if columns.count(required) != 1:
|
||||
raise ValueError(f"required column {required!r} must occur exactly once")
|
||||
time_index = columns.index("time")
|
||||
rate_index = columns.index(rate_variable_id)
|
||||
gate_index = columns.index(gate_variable_id)
|
||||
if not isinstance(data["records"], list):
|
||||
raise ValueError("records must be a list")
|
||||
samples = []
|
||||
for index, record in enumerate(data["records"]):
|
||||
try:
|
||||
if not isinstance(record, list) or len(record) != len(columns):
|
||||
raise ValueError("record must match columns")
|
||||
rate, gate = record[rate_index], record[gate_index]
|
||||
for value in (rate, gate):
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
raise ValueError("rate and gate must be numeric")
|
||||
if not isfinite(value):
|
||||
raise ValueError("rate and gate must be finite")
|
||||
samples.append(MaterialSample(
|
||||
timestamp=_parse_timestamp(record[time_index]),
|
||||
material_rate_kg_per_hour=float(rate), gate_value=float(gate),
|
||||
))
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(f"malformed record {index}: {exc}") from exc
|
||||
return samples
|
||||
except (KeyError, TypeError, ValueError) as exc:
|
||||
raise ValueError(f"Invalid timeseries response: {exc}") from exc
|
||||
request_body = {
|
||||
"machine": machine_id,
|
||||
"start": start.astimezone(UTC).isoformat(),
|
||||
"end": end.astimezone(UTC).isoformat(),
|
||||
"variables": [
|
||||
{"uuid": rate_variable_id},
|
||||
{"uuid": gate_variable_id},
|
||||
],
|
||||
}
|
||||
samples = []
|
||||
followed_cursors: set[str] = set()
|
||||
page = 1
|
||||
while True:
|
||||
response = self._client.post_json("/v2/timeseries", request_body)
|
||||
try:
|
||||
data = response.body["data"]
|
||||
columns = data["columns"]
|
||||
if not isinstance(columns, list):
|
||||
raise ValueError("columns must be a list")
|
||||
for required in ("time", rate_variable_id, gate_variable_id):
|
||||
if columns.count(required) != 1:
|
||||
raise ValueError(f"required column {required!r} must occur exactly once")
|
||||
time_index = columns.index("time")
|
||||
rate_index = columns.index(rate_variable_id)
|
||||
gate_index = columns.index(gate_variable_id)
|
||||
if not isinstance(data["records"], list):
|
||||
raise ValueError("records must be a list")
|
||||
for index, record in enumerate(data["records"]):
|
||||
try:
|
||||
if not isinstance(record, list) or len(record) != len(columns):
|
||||
raise ValueError("record must match columns")
|
||||
rate, gate = record[rate_index], record[gate_index]
|
||||
for value in (rate, gate):
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
raise ValueError("rate and gate must be numeric")
|
||||
if not isfinite(value):
|
||||
raise ValueError("rate and gate must be finite")
|
||||
samples.append(MaterialSample(
|
||||
timestamp=_parse_timestamp(record[time_index]),
|
||||
material_rate_kg_per_hour=float(rate), gate_value=float(gate),
|
||||
))
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError(f"malformed record {index}: {exc}") from exc
|
||||
next_cursor = None
|
||||
if "metadata" in response.body:
|
||||
metadata = response.body["metadata"]
|
||||
if not isinstance(metadata, dict) or "next_cursor" not in metadata:
|
||||
raise ValueError("metadata must be an object with a next_cursor field")
|
||||
next_cursor = metadata["next_cursor"]
|
||||
if next_cursor is not None and (
|
||||
not isinstance(next_cursor, str) or not next_cursor
|
||||
):
|
||||
raise ValueError("next_cursor must be null or a non-empty string")
|
||||
if next_cursor is None:
|
||||
return samples
|
||||
if next_cursor in followed_cursors:
|
||||
raise ValueError("next_cursor has already been followed")
|
||||
followed_cursors.add(next_cursor)
|
||||
request_body = {**request_body, "cursor": next_cursor}
|
||||
page += 1
|
||||
except (KeyError, TypeError, ValueError) as exc:
|
||||
raise ValueError(f"Invalid timeseries response on page {page}: {exc}") from exc
|
||||
|
||||
|
||||
def _parse_timestamp(value: str) -> datetime:
|
||||
|
||||
Reference in New Issue
Block a user