""" In-process HTTP server emulating PI Web API streamsets/recorded responses for E2E tests. """ from __future__ import annotations import json import time from typing import Any, Literal from pytest_httpserver import HTTPServer from werkzeug import Request from werkzeug.wrappers import Response PIWebAPIMode = Literal['success', 'empty', 'error', 'timeout'] STREAMSETS_RECORDED_PATH = '/streamsets/recorded' class PIWebAPITestServer: """ Thread-backed PI Web API stub using pytest-httpserver (real HTTP for pycurl clients). """ def __init__(self) -> None: self._httpserver = HTTPServer(host='127.0.0.1', port=0) self._mode: PIWebAPIMode = 'success' self._rows: list[dict[str, Any]] = [] self._timeout_sleep_seconds = 60 self.requests: list[Request] = [] @property def host(self) -> str: return self._httpserver.host @property def port(self) -> int: return self._httpserver.port @property def base_url(self) -> str: return f'http://{self.host}:{self.port}' def start(self) -> None: """Start the HTTP server and register the streamsets handler.""" self._httpserver.start() self._register_handler() def stop(self) -> None: """Stop the HTTP server.""" self._httpserver.stop() def clear(self) -> None: """Clear recorded requests and reset mode to success with no rows.""" self.requests.clear() self._mode = 'success' self._rows = [] self._httpserver.clear() self._register_handler() def set_mode( self, mode: PIWebAPIMode, rows: list[dict[str, Any]] | None = None, *, timeout_sleep_seconds: int = 60, ) -> None: """ Configure the next responses from the stub server. Args: - mode: Response mode (success, empty, error, timeout) - rows: Optional list of row dicts with keys name, webid, timestamp, value - timeout_sleep_seconds: Sleep duration for timeout mode (must exceed client timeout) """ self._mode = mode if rows is not None: self._rows = rows self._timeout_sleep_seconds = timeout_sleep_seconds self._register_handler() def _register_handler(self) -> None: self._httpserver.expect_request( STREAMSETS_RECORDED_PATH, method='GET', ).respond_with_handler(self._handle_streamsets_recorded) def _handle_streamsets_recorded(self, request: Request): self.requests.append(request) if self._mode == 'timeout': time.sleep(self._timeout_sleep_seconds) return self._json_response({'Items': []}, status=200) if self._mode == 'error': return self._json_response({'error': 'internal'}, status=500) if self._mode == 'empty' or not self._rows: return self._json_response({'Items': []}, status=200) items_by_name: dict[str, list[dict[str, Any]]] = {} for row in self._rows: name = row['name'] items_by_name.setdefault(name, []).append( { 'Timestamp': row['timestamp'], 'Value': row['value'], 'Good': True, 'Questionable': False, } ) items = [ {'Name': name, 'Items': points} for name, points in items_by_name.items() ] return self._json_response({'Items': items}, status=200) @staticmethod def _json_response(payload: dict[str, Any], *, status: int) -> Response: return Response( json.dumps(payload), status=status, mimetype='application/json', )