From 77c025ff1cddccfc1bb56147ed87ac41a3c43b51 Mon Sep 17 00:00:00 2001 From: mateusz Date: Thu, 4 Dec 2025 11:58:50 +0100 Subject: [PATCH 01/22] test --- src/racetrack_job_wrapper/wrapper_api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index fef906e..7b62eb4 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -164,6 +164,11 @@ def _get_parameters(): def _setup_auxiliary_endpoints(options: EndpointOptions): """Configure custom auxiliary endpoints defined by user in an entypoint""" auxiliary_endpoints = list_auxiliary_endpoints(options.entrypoint) + + @options.api.get("simple/get/{param}") + def simple_get(param, query): + return 7 + for endpoint_path in sorted(auxiliary_endpoints.keys()): endpoint_method: Callable = auxiliary_endpoints[endpoint_path] From 51c881076c79866552bcc40f834f34a257429677 Mon Sep 17 00:00:00 2001 From: mateusz Date: Thu, 4 Dec 2025 12:02:22 +0100 Subject: [PATCH 02/22] t --- src/racetrack_job_wrapper/wrapper_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index 7b62eb4..a5091d1 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -165,9 +165,10 @@ def _setup_auxiliary_endpoints(options: EndpointOptions): """Configure custom auxiliary endpoints defined by user in an entypoint""" auxiliary_endpoints = list_auxiliary_endpoints(options.entrypoint) - @options.api.get("simple/get/{param}") def simple_get(param, query): - return 7 + return 8 + + options.api.get("/simple/get/{param}", simple_get) for endpoint_path in sorted(auxiliary_endpoints.keys()): From 5761446e0f69d40279401d62d1bf49140b521fef Mon Sep 17 00:00:00 2001 From: mateusz Date: Thu, 4 Dec 2025 12:13:56 +0100 Subject: [PATCH 03/22] test --- src/racetrack_job_wrapper/wrapper_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index a5091d1..d7ff09d 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -168,7 +168,7 @@ def _setup_auxiliary_endpoints(options: EndpointOptions): def simple_get(param, query): return 8 - options.api.get("/simple/get/{param}", simple_get) + options.api.get("/simple/get/{param}")(simple_get) for endpoint_path in sorted(auxiliary_endpoints.keys()): From f308ba0bed1486f2ced04fd1937ce474c69d8a4b Mon Sep 17 00:00:00 2001 From: mateusz Date: Thu, 4 Dec 2025 12:33:36 +0100 Subject: [PATCH 04/22] test --- src/racetrack_job_wrapper/wrapper_api.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index d7ff09d..f3632c5 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -1,3 +1,4 @@ +import functools import inspect import mimetypes import os @@ -167,8 +168,13 @@ def _setup_auxiliary_endpoints(options: EndpointOptions): def simple_get(param, query): return 8 + + def plus_one(func): + @functools.wraps(func) + def adder(*args, **kwargs): + return func(*args, **kwargs) + 1 - options.api.get("/simple/get/{param}")(simple_get) + options.api.get("/simple/get/{param}")(plus_one(simple_get)) for endpoint_path in sorted(auxiliary_endpoints.keys()): From af0bf280502a64b3ddea8f3df45f1d0f500d3013 Mon Sep 17 00:00:00 2001 From: mateusz Date: Thu, 4 Dec 2025 12:37:11 +0100 Subject: [PATCH 05/22] test --- src/racetrack_job_wrapper/wrapper_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index f3632c5..34f90de 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -173,6 +173,8 @@ def plus_one(func): @functools.wraps(func) def adder(*args, **kwargs): return func(*args, **kwargs) + 1 + + return adder options.api.get("/simple/get/{param}")(plus_one(simple_get)) From f76ae28cc5a194dd51173bc65167e423cf97e6ee Mon Sep 17 00:00:00 2001 From: mateusz Date: Thu, 4 Dec 2025 15:15:42 +0100 Subject: [PATCH 06/22] test --- src/racetrack_job_wrapper/entrypoint.py | 7 ++++ src/racetrack_job_wrapper/wrapper_api.py | 53 ++++++++++++++++++++---- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/racetrack_job_wrapper/entrypoint.py b/src/racetrack_job_wrapper/entrypoint.py index 89f6627..eecaec5 100644 --- a/src/racetrack_job_wrapper/entrypoint.py +++ b/src/racetrack_job_wrapper/entrypoint.py @@ -2,6 +2,8 @@ from inspect import getfullargspec from typing import Dict, List, Optional, Tuple, Iterable, Any, Callable, Mapping, Union +from racetrack_job_wrapper.endpoint_config import EndpointConfig + WsgiApplication = Callable[ [ Mapping[str, object], # environ @@ -140,6 +142,11 @@ def list_auxiliary_endpoints(entrypoint: JobEntrypoint) -> Dict[str, Callable]: return {} return getattr(entrypoint, 'auxiliary_endpoints')() +def list_auxiliary_endpoints_v2(entrypoint: JobEntrypoint) -> List[EndpointConfig]: + if not hasattr(entrypoint, 'auxiliary_endpoints_v2'): + return [] + return getattr(entrypoint, 'auxiliary_endpoints_v2')() + def list_static_endpoints(entrypoint: JobEntrypoint) -> Dict[str, Union[Tuple, str]]: if not hasattr(entrypoint, 'static_endpoints'): diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index 34f90de..0473ec4 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -7,12 +7,13 @@ import time from pathlib import Path -from typing import Any, Callable, Dict, Tuple, Union, Optional +from typing import Any, Callable, Dict, List, Tuple, Union, Optional from contextvars import ContextVar from fastapi import Body, FastAPI, APIRouter, Request, Response, HTTPException from fastapi.responses import RedirectResponse +from racetrack_job_wrapper.endpoint_config import EndpointConfig from racetrack_job_wrapper.profiler import MemoryProfiler from racetrack_job_wrapper.webview import setup_webview_endpoints from racetrack_job_wrapper.concurrency import AtomicInteger @@ -21,6 +22,7 @@ JobEntrypoint, list_entrypoint_parameters, list_auxiliary_endpoints, + list_auxiliary_endpoints_v2, list_static_endpoints, ) from racetrack_job_wrapper.health import setup_health_endpoints, HealthState @@ -128,7 +130,12 @@ def _setup_api_endpoints( options: EndpointOptions, ): _setup_perform_endpoint(options) - _setup_auxiliary_endpoints(options) + + if hasattr(entrypoint, 'auxiliary_endpoints'): + _setup_auxiliary_endpoints(options) + else: + _setup_auxiliary_endpoints_v2(options) + _setup_static_endpoints(api, entrypoint) if MemoryProfiler.is_enabled(): _setup_profiler_endpoints(api) @@ -166,6 +173,38 @@ def _setup_auxiliary_endpoints(options: EndpointOptions): """Configure custom auxiliary endpoints defined by user in an entypoint""" auxiliary_endpoints = list_auxiliary_endpoints(options.entrypoint) + for endpoint_path in sorted(auxiliary_endpoints.keys()): + + endpoint_method: Callable = auxiliary_endpoints[endpoint_path] + endpoint_name = endpoint_path.replace('/', '_') + example_input = get_input_example(options.entrypoint, endpoint=endpoint_path) + if not endpoint_path.startswith('/'): + endpoint_path = '/' + endpoint_path + + # keep these variables inside closure as next loop cycle will overwrite it + def _add_endpoint(_endpoint_path: str, _endpoint_method: Callable): + summary = f"Call auxiliary endpoint: {_endpoint_path}" + description = "Call auxiliary endpoint" + endpoint_docs = inspect.getdoc(_endpoint_method) + if endpoint_docs: + description = f"Call auxiliary endpoint: {endpoint_docs}" + + @options.api.post( + _endpoint_path, + operation_id=f'auxiliary_endpoint_{endpoint_name}', + summary=summary, + description=description, + ) + def _auxiliary_endpoint(payload: Dict[str, Any] = Body(default=example_input)) -> Any: + return _call_job_endpoint(_endpoint_method, _endpoint_path, payload, options) + + _add_endpoint(endpoint_path, endpoint_method) + logger.info(f'configured auxiliary endpoint: {endpoint_path}') + +def _setup_auxiliary_endpoints_v2(options: EndpointOptions): + """Configure custom auxiliary endpoints defined by user in an entypoint""" + auxiliary_endpoints: List[EndpointConfig] = list_auxiliary_endpoints_v2(options.entrypoint) + def simple_get(param, query): return 8 @@ -178,9 +217,8 @@ def adder(*args, **kwargs): options.api.get("/simple/get/{param}")(plus_one(simple_get)) - for endpoint_path in sorted(auxiliary_endpoints.keys()): - - endpoint_method: Callable = auxiliary_endpoints[endpoint_path] + for endpoint_config in sorted(auxiliary_endpoints): + endpoint_path = endpoint_config.path endpoint_name = endpoint_path.replace('/', '_') example_input = get_input_example(options.entrypoint, endpoint=endpoint_path) if not endpoint_path.startswith('/'): @@ -203,10 +241,9 @@ def _add_endpoint(_endpoint_path: str, _endpoint_method: Callable): def _auxiliary_endpoint(payload: Dict[str, Any] = Body(default=example_input)) -> Any: return _call_job_endpoint(_endpoint_method, _endpoint_path, payload, options) - _add_endpoint(endpoint_path, endpoint_method) + _add_endpoint(endpoint_path, endpoint_config.handler) logger.info(f'configured auxiliary endpoint: {endpoint_path}') - def _call_job_endpoint( endpoint_method: Callable, endpoint_path: str, @@ -217,8 +254,6 @@ def _call_job_endpoint( metric_endpoint_requests_started.labels(endpoint=endpoint_path).inc() start_time = time.time() try: - assert payload is not None, 'payload is empty' - def _endpoint_caller() -> Any: return endpoint_method(**payload) From 3e44eba9753b4d918bb2a0b2a3ef5b1e6778e002 Mon Sep 17 00:00:00 2001 From: mateusz Date: Thu, 4 Dec 2025 15:18:52 +0100 Subject: [PATCH 07/22] test --- src/racetrack_job_wrapper/endpoint_config.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/racetrack_job_wrapper/endpoint_config.py diff --git a/src/racetrack_job_wrapper/endpoint_config.py b/src/racetrack_job_wrapper/endpoint_config.py new file mode 100644 index 0000000..d229c01 --- /dev/null +++ b/src/racetrack_job_wrapper/endpoint_config.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass +from http import HTTPMethod +from typing import Callable + + +@dataclass +class EndpointConfig: + path: str + method: HTTPMethod + handler: Callable \ No newline at end of file From e9b4ad4fb10d85670d1ee16cb042c82530e822df Mon Sep 17 00:00:00 2001 From: mateusz Date: Thu, 4 Dec 2025 15:23:56 +0100 Subject: [PATCH 08/22] test --- src/racetrack_job_wrapper/wrapper_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index 0473ec4..44c78d9 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -217,7 +217,7 @@ def adder(*args, **kwargs): options.api.get("/simple/get/{param}")(plus_one(simple_get)) - for endpoint_config in sorted(auxiliary_endpoints): + for endpoint_config in auxiliary_endpoints: endpoint_path = endpoint_config.path endpoint_name = endpoint_path.replace('/', '_') example_input = get_input_example(options.entrypoint, endpoint=endpoint_path) From 95751122079979856f6808a7d9f76445a1aede1f Mon Sep 17 00:00:00 2001 From: mateusz Date: Mon, 8 Dec 2025 11:45:26 +0100 Subject: [PATCH 09/22] test --- src/racetrack_job_wrapper/wrapper_api.py | 41 ++++++++++++++++-------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index 44c78d9..76d4449 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -1,4 +1,5 @@ import functools +from http import HTTPMethod import inspect import mimetypes import os @@ -220,28 +221,42 @@ def adder(*args, **kwargs): for endpoint_config in auxiliary_endpoints: endpoint_path = endpoint_config.path endpoint_name = endpoint_path.replace('/', '_') - example_input = get_input_example(options.entrypoint, endpoint=endpoint_path) if not endpoint_path.startswith('/'): endpoint_path = '/' + endpoint_path # keep these variables inside closure as next loop cycle will overwrite it - def _add_endpoint(_endpoint_path: str, _endpoint_method: Callable): + def _add_endpoint(_endpoint_path: str, _endpoint_handler: Callable, _endpoint_method: HTTPMethod): summary = f"Call auxiliary endpoint: {_endpoint_path}" description = "Call auxiliary endpoint" - endpoint_docs = inspect.getdoc(_endpoint_method) + endpoint_docs = inspect.getdoc(_endpoint_handler) if endpoint_docs: description = f"Call auxiliary endpoint: {endpoint_docs}" - @options.api.post( - _endpoint_path, - operation_id=f'auxiliary_endpoint_{endpoint_name}', - summary=summary, - description=description, - ) - def _auxiliary_endpoint(payload: Dict[str, Any] = Body(default=example_input)) -> Any: - return _call_job_endpoint(_endpoint_method, _endpoint_path, payload, options) - - _add_endpoint(endpoint_path, endpoint_config.handler) + def forwarder(func): + @functools.wraps(func) + def adder(*args, **kwargs): + return func(*args, **kwargs) + return adder + + match _endpoint_method: + case HTTPMethod.POST: + options.api.post( + _endpoint_path, + operation_id=f'auxiliary_endpoint_{endpoint_name}', + summary=summary, + description=description, + )(forwarder(_endpoint_handler)) + case HTTPMethod.GET: + options.api.get( + _endpoint_path, + operation_id=f'auxiliary_endpoint_{endpoint_name}', + summary=summary, + description=description, + )(forwarder(_endpoint_handler)) + case _: + logger.error(f"method {_endpoint_method} chosen for path {_endpoint_path} is not supported") + + _add_endpoint(endpoint_path, endpoint_config.handler, endpoint_config.method) logger.info(f'configured auxiliary endpoint: {endpoint_path}') def _call_job_endpoint( From 837abd13dad79dc584cfa24e35b1766de6d4d868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Mon, 8 Dec 2025 12:17:07 +0100 Subject: [PATCH 10/22] test --- src/racetrack_job_wrapper/wrapper_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index 76d4449..f7c074a 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -8,10 +8,10 @@ import time from pathlib import Path -from typing import Any, Callable, Dict, List, Tuple, Union, Optional +from typing import Annotated, Any, Callable, Dict, List, Tuple, Union, Optional from contextvars import ContextVar -from fastapi import Body, FastAPI, APIRouter, Request, Response, HTTPException +from fastapi import Body, FastAPI, APIRouter, Query, Request, Response, HTTPException from fastapi.responses import RedirectResponse from racetrack_job_wrapper.endpoint_config import EndpointConfig @@ -206,7 +206,7 @@ def _setup_auxiliary_endpoints_v2(options: EndpointOptions): """Configure custom auxiliary endpoints defined by user in an entypoint""" auxiliary_endpoints: List[EndpointConfig] = list_auxiliary_endpoints_v2(options.entrypoint) - def simple_get(param, query): + def simple_get(param, query: Annotated[float, Query(examples=[2.4])]): return 8 def plus_one(func): @@ -216,7 +216,7 @@ def adder(*args, **kwargs): return adder - options.api.get("/simple/get/{param}")(plus_one(simple_get)) + options.api.get("/simple/get/{param}")(simple_get) for endpoint_config in auxiliary_endpoints: endpoint_path = endpoint_config.path From 5c106cec0dace10fa4d2c6a17806c98142a3cbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Mon, 8 Dec 2025 12:19:29 +0100 Subject: [PATCH 11/22] test --- src/racetrack_job_wrapper/wrapper_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index f7c074a..8ba47c9 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -206,7 +206,7 @@ def _setup_auxiliary_endpoints_v2(options: EndpointOptions): """Configure custom auxiliary endpoints defined by user in an entypoint""" auxiliary_endpoints: List[EndpointConfig] = list_auxiliary_endpoints_v2(options.entrypoint) - def simple_get(param, query: Annotated[float, Query(examples=[2.4])]): + def simple_get(param, query: Annotated[float, Query(examples=[2.4])], bod: Annotated[float, Body(examples=[2.4])]): return 8 def plus_one(func): From 0d6ec9d85ed382b03949f95276e9a62a24c612fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Mon, 8 Dec 2025 14:13:15 +0100 Subject: [PATCH 12/22] test --- src/racetrack_job_wrapper/wrapper_api.py | 40 +++++++++++++++--------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index 8ba47c9..fb7ac18 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -206,18 +206,6 @@ def _setup_auxiliary_endpoints_v2(options: EndpointOptions): """Configure custom auxiliary endpoints defined by user in an entypoint""" auxiliary_endpoints: List[EndpointConfig] = list_auxiliary_endpoints_v2(options.entrypoint) - def simple_get(param, query: Annotated[float, Query(examples=[2.4])], bod: Annotated[float, Body(examples=[2.4])]): - return 8 - - def plus_one(func): - @functools.wraps(func) - def adder(*args, **kwargs): - return func(*args, **kwargs) + 1 - - return adder - - options.api.get("/simple/get/{param}")(simple_get) - for endpoint_config in auxiliary_endpoints: endpoint_path = endpoint_config.path endpoint_name = endpoint_path.replace('/', '_') @@ -234,9 +222,29 @@ def _add_endpoint(_endpoint_path: str, _endpoint_handler: Callable, _endpoint_me def forwarder(func): @functools.wraps(func) - def adder(*args, **kwargs): - return func(*args, **kwargs) - return adder + def forward(*args, **kwargs): + metric_requests_started.inc() + metric_endpoint_requests_started.labels(endpoint=endpoint_path).inc() + start_time = time.time() + try: + def _endpoint_caller() -> Any: + return func(*args, **kwargs) + + result = options.concurrency_runner(_endpoint_caller) + return to_json_serializable(result) + + except TypeError as e: + metric_request_internal_errors.labels(endpoint=endpoint_path).inc() + raise ValueError(f'failed to call a function: {e}') + except BaseException as e: + metric_request_internal_errors.labels(endpoint=endpoint_path).inc() + raise e + finally: + metric_request_duration.labels(endpoint=endpoint_path).observe(time.time() - start_time) + metric_requests_done.inc() + metric_last_call_timestamp.set(time.time()) + + return forward match _endpoint_method: case HTTPMethod.POST: @@ -269,6 +277,8 @@ def _call_job_endpoint( metric_endpoint_requests_started.labels(endpoint=endpoint_path).inc() start_time = time.time() try: + assert payload is not None, 'payload is empty' + def _endpoint_caller() -> Any: return endpoint_method(**payload) From 2957e62eaec7e5356d9498a38256f9e296dcd4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Mon, 8 Dec 2025 14:39:23 +0100 Subject: [PATCH 13/22] test --- src/racetrack_job_wrapper/api/metrics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/racetrack_job_wrapper/api/metrics.py b/src/racetrack_job_wrapper/api/metrics.py index 26618fa..f7c54c9 100644 --- a/src/racetrack_job_wrapper/api/metrics.py +++ b/src/racetrack_job_wrapper/api/metrics.py @@ -32,7 +32,7 @@ def setup_metrics_endpoint(api: FastAPI): api.mount('/metrics', WSGIMiddleware(metrics_app)) TrailingSlashForwarder.mount_path('/metrics') - @api.get('/metrics', tags=['root']) - def _metrics_endpoint(): - """List current Prometheus metrics""" - pass # just register endpoint in swagger, it's handled by ASGI + # @api.get('/metrics', tags=['root']) + # def _metrics_endpoint(): + # """List current Prometheus metrics""" + # pass # just register endpoint in swagger, it's handled by ASGI From d4af01a7b56156aa2ddcab363eb725fee5e6ec00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Wed, 10 Dec 2025 11:19:06 +0100 Subject: [PATCH 14/22] test --- sample/dockerfiled/requirements.txt | 2 +- .../entrypoint.py | 46 +++++++++++++++++++ src/racetrack_job_wrapper/api/metrics.py | 8 ++-- 3 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 sample/python-auxiliary-endpoints-v2/entrypoint.py diff --git a/sample/dockerfiled/requirements.txt b/sample/dockerfiled/requirements.txt index 8875551..8cc3465 100644 --- a/sample/dockerfiled/requirements.txt +++ b/sample/dockerfiled/requirements.txt @@ -1 +1 @@ -git+https://github.com/TheRacetrack/job-runner-python-lib.git@master \ No newline at end of file +git+https://github.com/TheRacetrack/job-runner-python-lib.git@rest \ No newline at end of file diff --git a/sample/python-auxiliary-endpoints-v2/entrypoint.py b/sample/python-auxiliary-endpoints-v2/entrypoint.py new file mode 100644 index 0000000..1de7534 --- /dev/null +++ b/sample/python-auxiliary-endpoints-v2/entrypoint.py @@ -0,0 +1,46 @@ +from http import HTTPMethod +import random +from typing import Annotated, Dict, List + +from fastapi import Body, Path, Query + +from racetrack_job_wrapper.endpoint_config import EndpointConfig + + +class Job: + def perform(self, x: float, y: float) -> float: + """ + Add numbers. + :param x: First element to add. + :param y: Second element to add. + :return: Sum of the numbers. + """ + return x + y + + def auxiliary_endpoints_v2(self) -> List[EndpointConfig]: + """Dict of custom endpoint paths (besides "/perform") handled by Entrypoint methods""" + return [ + EndpointConfig('/multiply/{path}', HTTPMethod.POST, self.multiply), + EndpointConfig('/random', HTTPMethod.GET, self.random), + ] + + def multiply(self, body: Annotated[float, Body(examples=[1.2])], query: Annotated[float, Query(example=2.4)], path: Annotated[float, Path(example=234.21)]) -> float: + """ + Standard fastapi methods of documenting and configuring parameters between body, query and path work for auxiliary endpoints. + """ + return body*query*path + + def random(self, start: float, end: float) -> float: + """Return random number within a range""" + return random.uniform(start, end) + + def docs_input_examples(self) -> Dict[str, Dict]: + """Return mapping of Job's endpoints to corresponding exemplary inputs.""" + return { + '/perform': { + 'x': 40, + 'y': 2, + } + } + + diff --git a/src/racetrack_job_wrapper/api/metrics.py b/src/racetrack_job_wrapper/api/metrics.py index f7c54c9..26618fa 100644 --- a/src/racetrack_job_wrapper/api/metrics.py +++ b/src/racetrack_job_wrapper/api/metrics.py @@ -32,7 +32,7 @@ def setup_metrics_endpoint(api: FastAPI): api.mount('/metrics', WSGIMiddleware(metrics_app)) TrailingSlashForwarder.mount_path('/metrics') - # @api.get('/metrics', tags=['root']) - # def _metrics_endpoint(): - # """List current Prometheus metrics""" - # pass # just register endpoint in swagger, it's handled by ASGI + @api.get('/metrics', tags=['root']) + def _metrics_endpoint(): + """List current Prometheus metrics""" + pass # just register endpoint in swagger, it's handled by ASGI From 2e12e574c9a99571883fbe9a9f677539827a57dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Fri, 12 Dec 2025 11:16:16 +0100 Subject: [PATCH 15/22] test --- src/racetrack_job_wrapper/wrapper_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index fb7ac18..cc1c267 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -213,7 +213,7 @@ def _setup_auxiliary_endpoints_v2(options: EndpointOptions): endpoint_path = '/' + endpoint_path # keep these variables inside closure as next loop cycle will overwrite it - def _add_endpoint(_endpoint_path: str, _endpoint_handler: Callable, _endpoint_method: HTTPMethod): + def _add_endpoint(_endpoint_path: str, _endpoint_handler: Callable, _endpoint_method: HTTPMethod, _other_options: Dict[str, Any]): summary = f"Call auxiliary endpoint: {_endpoint_path}" description = "Call auxiliary endpoint" endpoint_docs = inspect.getdoc(_endpoint_handler) @@ -253,6 +253,7 @@ def _endpoint_caller() -> Any: operation_id=f'auxiliary_endpoint_{endpoint_name}', summary=summary, description=description, + **_other_options )(forwarder(_endpoint_handler)) case HTTPMethod.GET: options.api.get( @@ -264,7 +265,7 @@ def _endpoint_caller() -> Any: case _: logger.error(f"method {_endpoint_method} chosen for path {_endpoint_path} is not supported") - _add_endpoint(endpoint_path, endpoint_config.handler, endpoint_config.method) + _add_endpoint(endpoint_path, endpoint_config.handler, endpoint_config.method, endpoint_config.other_options) logger.info(f'configured auxiliary endpoint: {endpoint_path}') def _call_job_endpoint( From 6b90db113ed99ddbe89240648a47250b1693c8c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Fri, 12 Dec 2025 11:20:12 +0100 Subject: [PATCH 16/22] test --- pyproject.toml | 2 +- src/racetrack_job_wrapper/endpoint_config.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b1b6dc..1e9fcf5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", ] readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.10" dynamic = ["dependencies"] [project.urls] diff --git a/src/racetrack_job_wrapper/endpoint_config.py b/src/racetrack_job_wrapper/endpoint_config.py index d229c01..eae52ea 100644 --- a/src/racetrack_job_wrapper/endpoint_config.py +++ b/src/racetrack_job_wrapper/endpoint_config.py @@ -1,10 +1,11 @@ from dataclasses import dataclass from http import HTTPMethod -from typing import Callable +from typing import Any, Callable, Dict @dataclass class EndpointConfig: path: str - method: HTTPMethod - handler: Callable \ No newline at end of file + method: HTTPMethod + handler: Callable + other_options: Dict[str, Any] = {} \ No newline at end of file From 2390bcc151a42b882cd6d80ccdaec2836626eb45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Fri, 12 Dec 2025 11:25:53 +0100 Subject: [PATCH 17/22] test --- src/racetrack_job_wrapper/endpoint_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/racetrack_job_wrapper/endpoint_config.py b/src/racetrack_job_wrapper/endpoint_config.py index eae52ea..0b10c79 100644 --- a/src/racetrack_job_wrapper/endpoint_config.py +++ b/src/racetrack_job_wrapper/endpoint_config.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from http import HTTPMethod from typing import Any, Callable, Dict @@ -8,4 +8,4 @@ class EndpointConfig: path: str method: HTTPMethod handler: Callable - other_options: Dict[str, Any] = {} \ No newline at end of file + other_options: Dict[str, Any] = field(default_factory=dict) \ No newline at end of file From be5c819fd02f811f1d905c2e60ccf1257b8de83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Mon, 15 Dec 2025 11:35:53 +0100 Subject: [PATCH 18/22] test --- src/racetrack_job_wrapper/wrapper_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/racetrack_job_wrapper/wrapper_api.py b/src/racetrack_job_wrapper/wrapper_api.py index cc1c267..25b417c 100644 --- a/src/racetrack_job_wrapper/wrapper_api.py +++ b/src/racetrack_job_wrapper/wrapper_api.py @@ -253,7 +253,7 @@ def _endpoint_caller() -> Any: operation_id=f'auxiliary_endpoint_{endpoint_name}', summary=summary, description=description, - **_other_options + **_other_options, )(forwarder(_endpoint_handler)) case HTTPMethod.GET: options.api.get( @@ -261,6 +261,7 @@ def _endpoint_caller() -> Any: operation_id=f'auxiliary_endpoint_{endpoint_name}', summary=summary, description=description, + **_other_options, )(forwarder(_endpoint_handler)) case _: logger.error(f"method {_endpoint_method} chosen for path {_endpoint_path} is not supported") From 05487feade32799762ba897d7f94736364ad401b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Wed, 7 Jan 2026 11:09:41 +0100 Subject: [PATCH 19/22] Tests --- .../entrypoint.py | 12 ++- tests/wrap/test_auxiliary_endpoints_v2.py | 78 +++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 tests/wrap/test_auxiliary_endpoints_v2.py diff --git a/sample/python-auxiliary-endpoints-v2/entrypoint.py b/sample/python-auxiliary-endpoints-v2/entrypoint.py index 1de7534..458f3d8 100644 --- a/sample/python-auxiliary-endpoints-v2/entrypoint.py +++ b/sample/python-auxiliary-endpoints-v2/entrypoint.py @@ -18,15 +18,19 @@ def perform(self, x: float, y: float) -> float: return x + y def auxiliary_endpoints_v2(self) -> List[EndpointConfig]: - """Dict of custom endpoint paths (besides "/perform") handled by Entrypoint methods""" + """ + Dict of custom endpoint paths (besides "/perform") handled by Entrypoint methods. + EndpointConfig consists of a path to the endpoint, http method(POST or GET only), + handler function for the endpoint and optional parameters dict that is passed to FastAPI. + """ return [ - EndpointConfig('/multiply/{path}', HTTPMethod.POST, self.multiply), - EndpointConfig('/random', HTTPMethod.GET, self.random), + EndpointConfig('/multiply/{path}', HTTPMethod.POST, self.multiply, other_options=dict(tags=["items"])), + EndpointConfig('/random', HTTPMethod.GET, self.random, other_options=dict(tags=["items"])), ] def multiply(self, body: Annotated[float, Body(examples=[1.2])], query: Annotated[float, Query(example=2.4)], path: Annotated[float, Path(example=234.21)]) -> float: """ - Standard fastapi methods of documenting and configuring parameters between body, query and path work for auxiliary endpoints. + Standard FastAPI methods of documenting and configuring parameters between body, query and path work for auxiliary endpoints. """ return body*query*path diff --git a/tests/wrap/test_auxiliary_endpoints_v2.py b/tests/wrap/test_auxiliary_endpoints_v2.py new file mode 100644 index 0000000..1237370 --- /dev/null +++ b/tests/wrap/test_auxiliary_endpoints_v2.py @@ -0,0 +1,78 @@ +from http import HTTPMethod +from typing import Annotated, Callable, Dict, List + +from fastapi import Body +from racetrack_job_wrapper.endpoint_config import EndpointConfig +from racetrack_job_wrapper.wrapper_api import create_api_app +from racetrack_job_wrapper.health import HealthState +from fastapi.testclient import TestClient + + +def test_auxiliary_endpoints_v2(): + class TestEntrypoint: + def perform(self, x: float, y: float) -> float: + """ + Add numbers. + :param x: First element to add. + :param y: Second element to add. + :return: Sum of the numbers. + """ + return x + y + + def auxiliary_endpoints_v2(self) -> List[EndpointConfig]: + return [ + EndpointConfig('/explain', HTTPMethod.POST, self.explain), + EndpointConfig('/random', HTTPMethod.GET, self.random), + ] + + def explain(self, x: Annotated[float, Body()], y: Annotated[float, Body()]) -> Dict[str, float]: + """ + Explain feature importance of a model result. + :param x: First element to add. + :param y: Second element to add. + :return: Dict of feature importance. + """ + result = self.perform(x, y) + return {'x_importance': x / result, 'y_importance': y / result} + + def random(self) -> float: + """Return random number""" + return 4 # chosen by fair dice roll + + def docs_input_examples(self) -> Dict[str, Dict]: + return { + '/perform': { + 'x': 40, + 'y': 2, + }, + '/explain': { + 'x': 1, + 'y': 2, + }, + '/random': {}, + } + + entrypoint = TestEntrypoint() + fastapi_app = create_api_app(entrypoint, HealthState(live=True, ready=True)) + + client = TestClient(fastapi_app) + + response = client.post( + "/api/v1/perform", + json={"x": 40, "y": 2}, + ) + assert response.status_code == 200 + assert response.json() == 42 + + response = client.post( + "/api/v1/explain", + json={"x": 2, "y": 8}, + ) + assert response.status_code == 200 + assert response.json() == {'x_importance': 0.2, 'y_importance': 0.8} + + response = client.get( + "/api/v1/random", + ) + assert response.status_code == 200 + assert response.json() == 4 From be33d03a5215f52a0c03fb3e4a1964b7b2fe26c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Wed, 7 Jan 2026 11:13:49 +0100 Subject: [PATCH 20/22] Fix --- sample/dockerfiled/requirements.txt | 2 +- src/racetrack_job_wrapper/endpoint_config.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/sample/dockerfiled/requirements.txt b/sample/dockerfiled/requirements.txt index 8cc3465..8875551 100644 --- a/sample/dockerfiled/requirements.txt +++ b/sample/dockerfiled/requirements.txt @@ -1 +1 @@ -git+https://github.com/TheRacetrack/job-runner-python-lib.git@rest \ No newline at end of file +git+https://github.com/TheRacetrack/job-runner-python-lib.git@master \ No newline at end of file diff --git a/src/racetrack_job_wrapper/endpoint_config.py b/src/racetrack_job_wrapper/endpoint_config.py index 0b10c79..024d2ae 100644 --- a/src/racetrack_job_wrapper/endpoint_config.py +++ b/src/racetrack_job_wrapper/endpoint_config.py @@ -5,7 +5,16 @@ @dataclass class EndpointConfig: + """Definition of a single HTTP endpoint. + + Attributes: + path: HTTP path at which the endpoint is registered. + method: HTTP verb used to bind the handler - only GET and POST are supported at moment. + handler: Callable invoked when the endpoint is hit. + other_options: Additional FastAPI route options. + """ + path: str method: HTTPMethod handler: Callable - other_options: Dict[str, Any] = field(default_factory=dict) \ No newline at end of file + other_options: Dict[str, Any] = field(default_factory=dict) From 78df19abc0c8ea3b34de37f91315e0cd0a9f8e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Wed, 7 Jan 2026 11:19:55 +0100 Subject: [PATCH 21/22] Docs and fixes --- docs/user_guide.md | 22 +++++++++++++++++++ .../entrypoint.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/user_guide.md b/docs/user_guide.md index 5fa4a2d..c8e917e 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -145,6 +145,28 @@ def explain(self, x: float, y: float) -> dict[str, float]: return {'x_importance': x / result, 'y_importance': y / result} ``` +If you need more control over those endpoints, you can define `auxiliary_endpoints_v2` method instead. +`auxiliary_endpoints_v2` returns list of `EndpointConfig`s that allows choosing between defining endpoints +as POST and GET as well as full control over where arguments are coming from(body, query, path) and can +pass additional arguments to FastAPI to handle additional use cases such as tagging endpoints. +```python +def auxiliary_endpoints_v2(self) -> List[EndpointConfig]: + """ + Dict of custom endpoint paths (besides "/perform") handled by Entrypoint methods. + EndpointConfig consists of a path to the endpoint, http method(POST or GET only), + handler function for the endpoint and optional parameters dict that is passed to FastAPI. + """ + return [ + EndpointConfig('/multiply/{path}', HTTPMethod.POST, self.multiply, other_options=dict(tags=["items"])), + ] + + def multiply(self, body: Annotated[float, Body(examples=[1.2])], query: Annotated[float, Query(example=2.4)], path: Annotated[float, Path(example=234.21)]) -> float: + """ + Standard FastAPI methods of documenting and configuring parameters between body, query and path work for auxiliary endpoints. + """ + return body * query * path +``` + If you want to define example data for your auxiliary endpoints, you can implement `docs_input_examples` method returning mapping of Job's endpoints to corresponding exemplary inputs. diff --git a/sample/python-auxiliary-endpoints-v2/entrypoint.py b/sample/python-auxiliary-endpoints-v2/entrypoint.py index 458f3d8..ec15ef5 100644 --- a/sample/python-auxiliary-endpoints-v2/entrypoint.py +++ b/sample/python-auxiliary-endpoints-v2/entrypoint.py @@ -32,7 +32,7 @@ def multiply(self, body: Annotated[float, Body(examples=[1.2])], query: Annotate """ Standard FastAPI methods of documenting and configuring parameters between body, query and path work for auxiliary endpoints. """ - return body*query*path + return body * query * path def random(self, start: float, end: float) -> float: """Return random number within a range""" From 60456bba1d76816403d65032aeaff6ba33cb8502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Twar=C3=B3g?= Date: Mon, 19 Jan 2026 11:16:08 +0100 Subject: [PATCH 22/22] Update documentation and version --- docs/CHANGELOG.md | 6 ++++++ docs/user_guide.md | 2 ++ pyproject.toml | 2 +- src/racetrack_job_wrapper/__init__.py | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3df9535..9795346 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,12 @@ All **user-facing**, notable changes will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.18.0] - 2026-01-19 +### Added +- `auxiliary_endpoints_v2` - new syntax for defining auxiliary endpoints with more options +### Changed +- now requires Python version 3.10 or higher + ## [1.17.0] - 2024-11-05 ### Added - Memory profiler (memray) can be enabled and managed at runtime by endpoints. diff --git a/docs/user_guide.md b/docs/user_guide.md index c8e917e..f18ea28 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -146,6 +146,8 @@ def explain(self, x: float, y: float) -> dict[str, float]: ``` If you need more control over those endpoints, you can define `auxiliary_endpoints_v2` method instead. +At this moment this new syntax is incompatible with original `auxiliary_endpoints` - you can use only one +or the other in the same job. `auxiliary_endpoints_v2` returns list of `EndpointConfig`s that allows choosing between defining endpoints as POST and GET as well as full control over where arguments are coming from(body, query, path) and can pass additional arguments to FastAPI to handle additional use cases such as tagging endpoints. diff --git a/pyproject.toml b/pyproject.toml index 1e9fcf5..1dbd2f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "racetrack_job_runner" -version = "1.17.0" # should be in sync with src/racetrack_job_wrapper/__init__.py +version = "1.18.0" # should be in sync with src/racetrack_job_wrapper/__init__.py description = "Racetrack Job Runner" license = {text = "Apache License 2.0"} authors = [ diff --git a/src/racetrack_job_wrapper/__init__.py b/src/racetrack_job_wrapper/__init__.py index 707b74f..9f45204 100644 --- a/src/racetrack_job_wrapper/__init__.py +++ b/src/racetrack_job_wrapper/__init__.py @@ -1,2 +1,2 @@ name = 'racetrack_job_wrapper' -__version__ = "1.17.0" # should be in sync with pyproject.toml +__version__ = "1.18.0" # should be in sync with pyproject.toml