Initial production analytics foundation

This commit is contained in:
2026-09-04 05:34:52 +02:00
commit f017d187eb
30 changed files with 1219 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
import io
import json
import os
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
from unittest.mock import patch
from production_analytics.cli.__main__ import main
from production_analytics.enlyze.exploration import ExplorationResponse
class CliTests(unittest.TestCase):
@patch.dict(os.environ, {"ENLYZE_BASE_URL": "https://enlyze.example"}, clear=True)
@patch("production_analytics.cli.__main__.ExplorationClient.get")
def test_raw_prints_and_saves_sanitized_response(self, get_mock: object) -> None:
get_mock.return_value = ExplorationResponse( # type: ignore[attr-defined]
status_code=200,
path="/verified/path",
headers={"Content-Type": "application/json"},
body={"api_token": "secret", "authorization": "Bearer secret", "value": 4.2},
)
with tempfile.TemporaryDirectory() as temporary_directory:
output = io.StringIO()
with redirect_stdout(output):
result = main(
[
"enlyze",
"raw",
"/verified/path",
"--save-fixture",
"example.json",
"--fixture-dir",
temporary_directory,
"--save-raw",
"raw-example.json",
"--raw-dir",
temporary_directory,
]
)
fixture = json.loads((Path(temporary_directory) / "example.json").read_text())
raw_capture = json.loads((Path(temporary_directory) / "raw-example.json").read_text())
self.assertEqual(result, 0)
self.assertEqual(fixture["response"]["body"]["api_token"], "<redacted-secret>")
self.assertEqual(fixture["response"]["body"]["authorization"], "<redacted-secret>")
self.assertNotIn("Bearer secret", output.getvalue())
self.assertEqual(raw_capture["response"]["body"]["api_token"], "secret")
def test_raw_rejects_unsafe_fixture_name(self) -> None:
with patch.dict(os.environ, {"ENLYZE_BASE_URL": "https://enlyze.example"}, clear=True):
with patch("production_analytics.cli.__main__.ExplorationClient.get") as get_mock:
get_mock.return_value = ExplorationResponse(200, "/path", {}, {})
output = io.StringIO()
with redirect_stdout(output):
result = main(["enlyze", "raw", "/path", "--save-fixture", "../unsafe.json"])
self.assertEqual(result, 1)
self.assertIn("plain filename", output.getvalue())
@patch.dict(os.environ, {"ENLYZE_BASE_URL": "https://enlyze.example"}, clear=True)
@patch("production_analytics.cli.__main__.ExplorationClient.post_json")
def test_timeseries_builds_schema_supported_read_only_post(self, post_mock: object) -> None:
post_mock.return_value = ExplorationResponse( # type: ignore[attr-defined]
200, "/v2/timeseries", {}, {"metadata": {"next_cursor": None}, "data": {"columns": [], "records": []}}
)
with tempfile.TemporaryDirectory() as temporary_directory:
result = main(
[
"enlyze", "timeseries", "--machine", "machine-id", "--start", "2026-01-01T00:00:00+00:00",
"--end", "2026-01-01T00:10:00+00:00", "--variable", "variable-id", "--resampling-interval", "10",
"--resampling-method", "avg", "--save-fixture", "timeseries.json", "--fixture-dir", temporary_directory,
]
)
fixture = json.loads((Path(temporary_directory) / "timeseries.json").read_text())
self.assertEqual(result, 0)
self.assertEqual(post_mock.call_args.args[0], "/v2/timeseries") # type: ignore[attr-defined]
self.assertEqual(post_mock.call_args.args[1]["resampling_interval"], 10) # type: ignore[attr-defined]
self.assertEqual(fixture["request"]["method"], "POST")
+19
View File
@@ -0,0 +1,19 @@
from datetime import UTC, datetime, timedelta
import unittest
from production_analytics.domain import SourceRange
class SourceRangeTests(unittest.TestCase):
def test_accepts_ordered_timestamps(self) -> None:
start = datetime(2026, 1, 1, tzinfo=UTC)
source_range = SourceRange(start=start, end=start + timedelta(minutes=1))
self.assertEqual(source_range.start, start)
def test_rejects_reverse_interval(self) -> None:
end = datetime(2026, 1, 1, tzinfo=UTC)
with self.assertRaisesRegex(ValueError, "must not precede"):
SourceRange(start=end + timedelta(seconds=1), end=end)
+121
View File
@@ -0,0 +1,121 @@
import io
import json
import tempfile
import unittest
from unittest.mock import patch
from urllib.error import HTTPError
from production_analytics.enlyze.exploration import (
AuthenticationError,
ExplorationClient,
ExplorationSettings,
NonJsonResponseError,
load_secret_file,
)
from production_analytics.enlyze.sanitize import sanitize
class _Response:
status = 200
headers = {"Content-Type": "application/json", "X-Request-Id": "request-123"}
def __init__(self, body: bytes) -> None:
self._body = body
def read(self) -> bytes:
return self._body
def __enter__(self):
return self
def __exit__(self, *_: object) -> None:
return None
class ExplorationClientTests(unittest.TestCase):
def setUp(self) -> None:
self.settings = ExplorationSettings.from_environment(
{
"ENLYZE_BASE_URL": "https://enlyze.example/",
"ENLYZE_API_KEY": "secret",
}
)
@patch("production_analytics.enlyze.exploration.urlopen")
def test_get_uses_explicit_header_and_decodes_json(self, urlopen_mock: object) -> None:
urlopen_mock.return_value = _Response(b'{"items": [1]}') # type: ignore[attr-defined]
response = ExplorationClient(self.settings).get("/observed/path", {"limit": "1"})
request = urlopen_mock.call_args.args[0] # type: ignore[attr-defined]
self.assertEqual(request.get_method(), "GET")
self.assertEqual(request.get_header("Authorization"), "Bearer secret")
self.assertEqual(response.body, {"items": [1]})
@patch("production_analytics.enlyze.exploration.urlopen")
def test_non_json_response_is_clear_error(self, urlopen_mock: object) -> None:
urlopen_mock.return_value = _Response(b"not json") # type: ignore[attr-defined]
with self.assertRaises(NonJsonResponseError):
ExplorationClient(self.settings).get("/observed/path")
@patch("production_analytics.enlyze.exploration.urlopen")
def test_post_json_sends_json_body_and_bearer_header(self, urlopen_mock: object) -> None:
urlopen_mock.return_value = _Response(b'{"data": {}}') # type: ignore[attr-defined]
ExplorationClient(self.settings).post_json("/v2/timeseries", {"machine": "machine-id"})
request = urlopen_mock.call_args.args[0] # type: ignore[attr-defined]
self.assertEqual(request.get_method(), "POST")
self.assertEqual(request.get_header("Content-type"), "application/json")
self.assertEqual(request.data, b'{"machine": "machine-id"}')
self.assertEqual(request.get_header("Authorization"), "Bearer secret")
@patch("production_analytics.enlyze.exploration.urlopen")
def test_authentication_error_does_not_expose_response_body(self, urlopen_mock: object) -> None:
urlopen_mock.side_effect = HTTPError("https://example.invalid", 401, "Unauthorized", {}, io.BytesIO(b"secret")) # type: ignore[attr-defined]
with self.assertRaisesRegex(AuthenticationError, "HTTP 401"):
ExplorationClient(self.settings).get("/observed/path")
class SanitizationTests(unittest.TestCase):
def test_sanitization_preserves_shape_and_redacts_sensitive_values(self) -> None:
response = sanitize(
{
"machine_id": "machine-47",
"token": "very-secret",
"Authorization": "Bearer very-secret",
"site_name": "Sensitive Site",
"api_url": "https://internal.example/v1/items",
"timestamp": "2026-01-01T00:00:00Z",
"unit": "kW",
"value": 12.5,
"quality": "good",
"items": [{"machine_id": "machine-47"}],
}
)
self.assertEqual(response["token"], "<redacted-secret>")
self.assertEqual(response["Authorization"], "<redacted-secret>")
self.assertEqual(response["site_name"], "<redacted-site_name>")
self.assertEqual(response["api_url"], "https://redacted-host.invalid/v1/items")
self.assertEqual(response["timestamp"], "2026-01-01T00:00:00Z")
self.assertEqual(response["unit"], "kW")
self.assertEqual(response["value"], 12.5)
self.assertEqual(response["machine_id"], response["items"][0]["machine_id"])
class SecretFileTests(unittest.TestCase):
def test_secret_file_is_parsed_without_execution(self) -> None:
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8") as secret_file:
secret_file.write("ENLYZE_API_KEY='key value'\nUNRELATED=plain # comment\n")
secret_file.flush()
values = load_secret_file(secret_file.name)
self.assertEqual(values, {"ENLYZE_API_KEY": "key value", "UNRELATED": "plain"})
if __name__ == "__main__":
unittest.main()