Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,12 @@ reporting:
name: "Local Flight Blender Dev Instance"
version: "v0.12.0"
notes: "Running against local Docker Compose setup."
# Allure reporting (opt-in). When enabled, results are written under
# <output_dir>/<run_timestamp>/allure-results and HTML can be generated via
# POST /api/allure/generate. Set capture_http=true to also attach the raw
# HTTP request/response exchanges to each step (sensitive headers and
# request body fields are redacted; bodies are truncated).
allure:
enabled: true
capture_http: true
results_dir: "allure-results"
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "openutm-verification"
version = "0.2.0"
version = "0.2.1"
description = "Verification tools for Flight Blender and OpenUTM products"
readme = "README.md"
license = { file = "LICENSE" }
Expand All @@ -15,9 +15,9 @@ classifiers = [
dependencies = [
"httpx>=0.27.0",
"arrow==1.3.0",
"python-dotenv==1.0.1",
"python-dotenv==1.2.2",
"http-sfv==0.9.9",
"cryptography==44.0.3",
"cryptography==46.0.5",
"jwt==1.3.1",
"http-message-signatures==0.5.0",
"redis==6.0.0",
Expand Down Expand Up @@ -46,6 +46,7 @@ dependencies = [
"websockets>=12.0",
"markdown>=3.10",
"uas-standards==4.2.0",
"allure-python-commons>=2.15.0",
"fastapi>=0.121.3",
"uvicorn[standard]>=0.38.0", # includes watchfiles for efficient reload
"bluesky-simulator==1.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import time

import httpx
from loguru import logger
from websockets.asyncio.client import ClientConnection, connect

from openutm_verification.core.reporting.http_collector import HttpCollector
from openutm_verification.models import FlightBlenderError


Expand Down Expand Up @@ -33,17 +36,34 @@ async def _request(
silent_status: list[int] | None = None,
) -> httpx.Response:
url = f"{self.base_url}{endpoint}"
start = time.time()
response: httpx.Response | None = None
error: str | None = None
try:
response = await self.client.request(method, url, json=json)
if not (silent_status and response.status_code in silent_status):
response.raise_for_status()
return response
except httpx.HTTPStatusError as e:
response = e.response
error = f"{e.response.status_code}: {e.response.text[:500]}"
logger.error(f"HTTP error occurred: {e.response.status_code} - {e.response.text}")
raise FlightBlenderError(f"Request failed: {e.response.status_code}") from e
except httpx.RequestError as e:
error = str(e)
logger.error(f"Request error occurred: {e}")
raise FlightBlenderError("Request failed") from e
finally:
if HttpCollector.is_enabled():
HttpCollector.record_from_httpx(
method=method,
url=url,
request_headers=dict(self.client.headers),
request_body=json,
response=response,
start=start,
error=error,
)

async def get(self, endpoint: str, silent_status: list[int] | None = None) -> httpx.Response:
return await self._request("GET", endpoint, silent_status=silent_status)
Expand Down
57 changes: 47 additions & 10 deletions src/openutm_verification/core/clients/opensky/base_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import time
from typing import TYPE_CHECKING

import httpx
Expand All @@ -8,6 +9,7 @@

from openutm_verification.auth.oauth2 import OAuth2Client
from openutm_verification.core.execution.config_models import get_settings
from openutm_verification.core.reporting.http_collector import HttpCollector

if TYPE_CHECKING:
from openutm_verification.core.execution.config_models import OpenSkyConfig
Expand Down Expand Up @@ -66,16 +68,51 @@ async def _request(
headers["Authorization"] = f"Bearer {await self.oauth_client.get_access_token()}"

logger.debug(f"Making {method} request to {url}")
response = await self.client.request(method, url, params=params, headers=headers)

if response.status_code == 401 and config.opensky.auth.type == "oauth2":
logger.warning("Token expired, retrying with new token...")
headers["Authorization"] = f"Bearer {await self.oauth_client.get_access_token()}"
response = await self.client.request(method, url, params=params, headers=headers)

if not (silent_status and response.status_code in silent_status):
response.raise_for_status()
return response
start = time.time()
response: httpx.Response | None = None
error: str | None = None
try:
try:
response = await self.client.request(method, url, params=params, headers=headers)
except httpx.RequestError as exc:
error = str(exc)
raise

if response.status_code == 401 and config.opensky.auth.type == "oauth2":
# Record the failed attempt before retrying with a fresh token.
if HttpCollector.is_enabled():
HttpCollector.record_from_httpx(
method=method,
url=url,
request_headers={**dict(self.client.headers), **headers},
request_body=params,
response=response,
start=start,
)
logger.warning("Token expired, retrying with new token...")
headers["Authorization"] = f"Bearer {await self.oauth_client.get_access_token()}"
start = time.time()
response = None
try:
response = await self.client.request(method, url, params=params, headers=headers)
except httpx.RequestError as exc:
error = str(exc)
raise

if not (silent_status and response.status_code in silent_status):
response.raise_for_status()
return response
finally:
if HttpCollector.is_enabled():
HttpCollector.record_from_httpx(
method=method,
url=url,
request_headers={**dict(self.client.headers), **headers},
request_body=params,
response=response,
start=start,
error=error,
)

async def get(
self,
Expand Down
16 changes: 16 additions & 0 deletions src/openutm_verification/core/execution/config_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,29 @@ class DeploymentDetails(StrictBaseModel):
notes: str = ""


class AllureConfig(StrictBaseModel):
"""Configuration for Allure reporting.

``results_dir`` is interpreted as a relative path under the
configured reporting output directory
(``<output_dir>/<timestamp>/<results_dir>``, where ``<output_dir>`` is
``reporting.output_dir``) so each run gets its own isolated Allure results
folder. Absolute paths are honoured as-is for backwards compatibility.
"""

enabled: bool = False
capture_http: bool = False
results_dir: str = "allure-results"


class ReportingConfig(StrictBaseModel):
"""Configuration for generating reports."""

timestamp_subdir: str = ""
output_dir: str = "reports"
formats: list[str] = Field(default_factory=lambda: ["json", "html", "log"])
deployment_details: DeploymentDetails = Field(default_factory=DeploymentDetails)
allure: AllureConfig = Field(default_factory=AllureConfig)


class DataFiles(StrictBaseModel):
Expand Down
Loading
Loading