From 20298909449a4402dad36272880dfa2050254532 Mon Sep 17 00:00:00 2001 From: Gabor Torok Date: Mon, 20 Apr 2026 15:00:16 -0700 Subject: [PATCH 1/3] start of v2 --- app/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config.py b/app/config.py index 35c17c93..b9ffb407 100644 --- a/app/config.py +++ b/app/config.py @@ -7,7 +7,7 @@ logger = get_stream_logger(__name__, LOG_LEVEL) -API_VERSION = "1.0.0" +API_VERSION = "2.0.0" # lines in the description can't have indentation (markup format) description = """ From 96964e055ee926d5d9b6d209512d0aa3432b7dca Mon Sep 17 00:00:00 2001 From: Gabor Torok Date: Mon, 20 Apr 2026 15:22:35 -0700 Subject: [PATCH 2/3] more v2 changes --- README.md | 6 +++--- VALIDATION.MD | 2 +- app/config.py | 5 +++-- app/routers/error_handlers.py | 26 ++++++++++++++------------ test/test_filesystem.py | 6 +++--- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b47226df..54d0e6ed 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,10 @@ See it live: - NERSC instance: - API docs: https://api.iri.nersc.gov - - API requests: https://api.iri.nersc.gov/api/v1/ + - API requests: https://api.iri.nersc.gov/api/v2/ - ALCF instance: - API docs: https://api.alcf.anl.gov - - API requests: https://api.alcf.anl.gov/api/v1/ + - API requests: https://api.alcf.anl.gov/api/v2/ - ESnet instance: https://iri-dev.ppg.es.net ## Prerequisites @@ -50,7 +50,7 @@ If using docker (see next section), your dockerfile could extend this reference - `API_URL_ROOT`: the base url when constructing links returned by the api (eg.: https://iri.myfacility.com) - `API_PREFIX`: the path prefix where the api is hosted. Defaults to `/`. (eg.: `/api`) -- `API_URL`: the path to the api itself. Defaults to `api/v1`. +- `API_URL`: the path to the api itself. Defaults to `api/v2`. - `OPENTELEMETRY_ENABLED`: Enables OpenTelemetry. If enabled, the application will use OpenTelemetry SDKs and emit traces, metrics, and logs. Default to false - `OTLP_ENDPOINT`: OpenTelemetry Protocol collector endpoint to export telemetry data. If empty or not set, telemetry data is logged locally to log file. Default: "" diff --git a/VALIDATION.MD b/VALIDATION.MD index 26f77ef6..b9988311 100644 --- a/VALIDATION.MD +++ b/VALIDATION.MD @@ -7,7 +7,7 @@ On every pull request or push to `main` branch, Github Actions run the following 3. Waits for `/openapi.json` to become available on localhost:8000. 4. Runs Schemathesis validation twice: - Against Facilities API’s OpenAPI spec. (http://localhost:8000/openapi.json) - - Against the official IRI Facility API OpenAPI spec. (https://github.com/doe-iri/iri-facility-api-docs/blob/main/specification/openapi/openapi_iri_facility_api_v1.json) + - Against the official IRI Facility API OpenAPI spec. (https://github.com/doe-iri/iri-facility-api-docs/blob/main/specification/openapi/openapi_iri_facility_api_v2.json) 5. Fails the workflow if either validation fails. 6. Saves Schemathesis HTML/XML reports as artifacts (or saves it locally when run with `act`). 7. Dumps API container logs and do clean up to stop container. diff --git a/app/config.py b/app/config.py index b9ffb407..0d0b7c63 100644 --- a/app/config.py +++ b/app/config.py @@ -8,6 +8,7 @@ logger = get_stream_logger(__name__, LOG_LEVEL) API_VERSION = "2.0.0" +API_VERSION_SHORT = "v2" # lines in the description can't have indentation (markup format) description = """ @@ -19,7 +20,7 @@ """ # version is the openapi.json spec version -# /api/v1 mount point means it's the latest backward-compatible url +# /api/v2 mount point means it's the latest backward-compatible url API_CONFIG = { "title": "IRI Facility API reference implementation", "description": description, @@ -38,7 +39,7 @@ API_URL_ROOT = os.environ.get("API_URL_ROOT", "https://api.iri.nersc.gov") API_PREFIX = os.environ.get("API_PREFIX", "/") -API_URL = os.environ.get("API_URL", "api/v1") +API_URL = os.environ.get("API_URL", f"api/{API_VERSION_SHORT}") OPENTELEMETRY_ENABLED = os.environ.get("OPENTELEMETRY_ENABLED", "false").lower() == "true" OPENTELEMETRY_DEBUG = os.environ.get("OPENTELEMETRY_DEBUG", "false").lower() == "true" diff --git a/app/routers/error_handlers.py b/app/routers/error_handlers.py index a94334da..42a10ab2 100644 --- a/app/routers/error_handlers.py +++ b/app/routers/error_handlers.py @@ -13,6 +13,8 @@ from fastapi.exceptions import RequestValidationError from starlette.exceptions import HTTPException as StarletteHTTPException +from .. import config + class Problem(BaseModel): model_config = ConfigDict(extra="allow", json_schema_extra={"description": 'Error structure for REST interface based on RFC 9457, "Problem Details for HTTP APIs."'}) @@ -20,7 +22,7 @@ class Problem(BaseModel): status: int = Field(..., ge=100, le=599, description="The HTTP status code for this occurrence.", example=404) title: str|None = Field(default=None, description="Short human-readable summary.", example="Not Found") detail: str|None = Field(default=None, description="Human-readable explanation.", example="Descriptive text.") - instance: str = Field(..., description="A URI reference identifying this occurrence.", example="http://localhost/api/v1/resource/123") + instance: str = Field(..., description="A URI reference identifying this occurrence.", example=f"http://localhost/{config.API_URL}/resource/123") def get_url_base(request: Request) -> str: @@ -222,18 +224,18 @@ async def global_handler(request: Request, exc: Exception): "title": "Invalid parameter", "status": 400, "detail": "modified_since must be in ISO 8601 format.", - "instance": "/api/v1/status/resources?modified_since=BADVALUE", + "instance": f"/{config.API_URL}/status/resources?modified_since=BADVALUE", "invalid_params": [{"name": "modified_since", "reason": "Invalid datetime format"}], } -EXAMPLE_401 = {"type": "https://iri.example.com/problems/unauthorized", "title": "Unauthorized", "status": 401, "detail": "Bearer token is missing or invalid.", "instance": "/api/v1/status/resources"} +EXAMPLE_401 = {"type": "https://iri.example.com/problems/unauthorized", "title": "Unauthorized", "status": 401, "detail": "Bearer token is missing or invalid.", "instance": f"/{config.API_URL}/status/resources"} EXAMPLE_403 = { "type": "https://iri.example.com/problems/forbidden", "title": "Forbidden", "status": 403, "detail": "Caller is authenticated but lacks required role.", - "instance": "/api/v1/status/resources", + "instance": f"/{config.API_URL}/status/resources", } EXAMPLE_404 = { @@ -241,7 +243,7 @@ async def global_handler(request: Request, exc: Exception): "title": "Not Found", "status": 404, "detail": "The resource ID 'abc123' does not exist.", - "instance": "/api/v1/status/resources/abc123", + "instance": f"/{config.API_URL}/status/resources/abc123", } EXAMPLE_405 = { @@ -249,7 +251,7 @@ async def global_handler(request: Request, exc: Exception): "title": "Method Not Allowed", "status": 405, "detail": "HTTP method TRACE is not allowed for this endpoint.", - "instance": "/api/v1/status/resources", + "instance": f"/{config.API_URL}/status/resources", } EXAMPLE_409 = { @@ -257,7 +259,7 @@ async def global_handler(request: Request, exc: Exception): "title": "Conflict", "status": 409, "detail": "A job with this ID already exists.", - "instance": "/api/v1/compute/job/perlmutter/123", + "instance": f"/{config.API_URL}/compute/job/perlmutter/123", } EXAMPLE_422 = { @@ -265,7 +267,7 @@ async def global_handler(request: Request, exc: Exception): "title": "Unprocessable Entity", "status": 422, "detail": "The PSIJ JobSpec is syntactically correct but invalid.", - "instance": "/api/v1/compute/job/perlmutter", + "instance": f"/{config.API_URL}/compute/job/perlmutter", "invalid_params": [{"name": "job_spec.executable", "reason": "Executable must be provided"}], } @@ -274,7 +276,7 @@ async def global_handler(request: Request, exc: Exception): "title": "Internal Server Error", "status": 500, "detail": "An unexpected error occurred.", - "instance": "/api/v1/status/resources", + "instance": f"/{config.API_URL}/status/resources", } EXAMPLE_501 = { @@ -282,7 +284,7 @@ async def global_handler(request: Request, exc: Exception): "title": "Not Implemented", "status": 501, "detail": "This functionality is not implemented.", - "instance": "/api/v1/status/resources", + "instance": f"/{config.API_URL}/status/resources", } EXAMPLE_503 = { @@ -290,7 +292,7 @@ async def global_handler(request: Request, exc: Exception): "title": "Service Unavailable", "status": 503, "detail": "The service is temporarily unavailable.", - "instance": "/api/v1/status/resources", + "instance": f"/{config.API_URL}/status/resources", } EXAMPLE_504 = { @@ -298,7 +300,7 @@ async def global_handler(request: Request, exc: Exception): "title": "Gateway Timeout", "status": 504, "detail": "The server did not receive a timely response.", - "instance": "/api/v1/status/resources", + "instance": f"/{config.API_URL}/status/resources", } DEFAULT_RESPONSES = { diff --git a/test/test_filesystem.py b/test/test_filesystem.py index e83f6137..75e2674a 100644 --- a/test/test_filesystem.py +++ b/test/test_filesystem.py @@ -13,9 +13,9 @@ # CONFIG — EDIT THESE AS NEEDED # ========================= -BASE_URL = "http://localhost:8000/api/v1" -#BASE_URL = "https://api.iri.nersc.gov/api/v1" -#BASE_URL = "https://iri-dev.ppg.es.net/api/v1" +BASE_URL = "http://localhost:8000/api/v2" +#BASE_URL = "https://api.iri.nersc.gov/api/v2" +#BASE_URL = "https://iri-dev.ppg.es.net/api/v2" TOKEN = os.environ.get("IRI_API_TOKEN", "12345") # ========================= HEADERS = {"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"} From 47a9b2e3ffcad6684a1fdbdc2b56c81a354b9835 Mon Sep 17 00:00:00 2001 From: Gabor Torok Date: Mon, 20 Apr 2026 15:50:25 -0700 Subject: [PATCH 3/3] more v2 changes --- app/config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/config.py b/app/config.py index 0d0b7c63..b7fdc285 100644 --- a/app/config.py +++ b/app/config.py @@ -10,6 +10,10 @@ API_VERSION = "2.0.0" API_VERSION_SHORT = "v2" +API_URL_ROOT = os.environ.get("API_URL_ROOT", "https://api.iri.nersc.gov") +API_PREFIX = os.environ.get("API_PREFIX", "/") +API_URL = os.environ.get("API_URL", f"api/{API_VERSION_SHORT}") + # lines in the description can't have indentation (markup format) description = """ A simple implementation of the IRI facility API using python and the fastApi library. @@ -25,7 +29,8 @@ "title": "IRI Facility API reference implementation", "description": description, "version": API_VERSION, - "docs_url": "/", + "docs_url": f"/{API_URL}", + "openapi_url": f"/{API_URL}/openapi.json", "contact": {"name": "Facility API contact", "url": "https://www.somefacility.gov/about/contact-us/"}, "terms_of_service": "https://www.somefacility.gov/terms-of-service", } @@ -36,11 +41,6 @@ except Exception as exc: logger.error(f"Error parsing IRI_API_PARAMS: {exc}") - -API_URL_ROOT = os.environ.get("API_URL_ROOT", "https://api.iri.nersc.gov") -API_PREFIX = os.environ.get("API_PREFIX", "/") -API_URL = os.environ.get("API_URL", f"api/{API_VERSION_SHORT}") - OPENTELEMETRY_ENABLED = os.environ.get("OPENTELEMETRY_ENABLED", "false").lower() == "true" OPENTELEMETRY_DEBUG = os.environ.get("OPENTELEMETRY_DEBUG", "false").lower() == "true" OTLP_ENDPOINT = os.environ.get("OTLP_ENDPOINT", "")