stac-fastapi.api 6.4.1__tar.gz → 6.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/PKG-INFO +5 -3
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/pyproject.toml +6 -1
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/stac_fastapi/api/app.py +13 -0
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/stac_fastapi/api/errors.py +0 -1
- stac_fastapi_api-6.6.0/stac_fastapi/api/metrics.py +131 -0
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/stac_fastapi/api/models.py +1 -6
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/stac_fastapi/api/version.py +1 -1
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/.gitignore +0 -0
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/README.md +0 -0
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/stac_fastapi/api/__init__.py +0 -0
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/stac_fastapi/api/config.py +0 -0
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/stac_fastapi/api/middleware.py +0 -0
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/stac_fastapi/api/openapi.py +0 -0
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/stac_fastapi/api/py.typed +0 -0
- {stac_fastapi_api-6.4.1 → stac_fastapi_api-6.6.0}/stac_fastapi/api/routes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: stac-fastapi-api
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.6.0
|
|
4
4
|
Summary: An implementation of STAC API based on the FastAPI framework.
|
|
5
5
|
Project-URL: Homepage, https://github.com/stac-utils/stac-fastapi
|
|
6
6
|
Author-email: ArturoAI <engineering@arturo.ai>
|
|
@@ -17,4 +17,6 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
18
|
Requires-Python: >=3.11
|
|
19
19
|
Requires-Dist: brotli-asgi
|
|
20
|
-
Requires-Dist: stac-fastapi-types~=6.
|
|
20
|
+
Requires-Dist: stac-fastapi-types~=6.6
|
|
21
|
+
Provides-Extra: metrics
|
|
22
|
+
Requires-Dist: prometheus-fastapi-instrumentator>=8.1.0; extra == 'metrics'
|
|
@@ -32,7 +32,12 @@ classifiers = [
|
|
|
32
32
|
dynamic = ["version"]
|
|
33
33
|
dependencies = [
|
|
34
34
|
"brotli_asgi",
|
|
35
|
-
"stac-fastapi.types~=6.
|
|
35
|
+
"stac-fastapi.types~=6.6",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
metrics = [
|
|
40
|
+
"prometheus-fastapi-instrumentator>=8.1.0",
|
|
36
41
|
]
|
|
37
42
|
|
|
38
43
|
[dependency-groups]
|
|
@@ -74,6 +74,10 @@ class StacApi:
|
|
|
74
74
|
health_check:
|
|
75
75
|
A Callable which return application's `health` information.
|
|
76
76
|
Defaults to `def health: return {"status": "UP"}`
|
|
77
|
+
add_metrics:
|
|
78
|
+
When ``True``, instrument the app and expose Prometheus metrics at
|
|
79
|
+
``{router_prefix}/_mgmt/metrics``. Requires
|
|
80
|
+
``stac-fastapi-api[metrics]`` to be installed.
|
|
77
81
|
|
|
78
82
|
"""
|
|
79
83
|
|
|
@@ -142,6 +146,7 @@ class StacApi:
|
|
|
142
146
|
health_check: Callable[[], dict] | Callable[[], Awaitable[dict]] = attr.ib(
|
|
143
147
|
default=lambda: {"status": "UP"}
|
|
144
148
|
)
|
|
149
|
+
add_metrics: bool = attr.ib(default=False)
|
|
145
150
|
|
|
146
151
|
def get_extension(self, extension: type[ApiExtension]) -> ApiExtension | None:
|
|
147
152
|
"""Get an extension.
|
|
@@ -477,6 +482,14 @@ class StacApi:
|
|
|
477
482
|
for middleware in self.middlewares:
|
|
478
483
|
self.app.user_middleware.insert(0, middleware)
|
|
479
484
|
|
|
485
|
+
if self.add_metrics:
|
|
486
|
+
from stac_fastapi.api.metrics import instrument_app
|
|
487
|
+
|
|
488
|
+
instrument_app(
|
|
489
|
+
self.app,
|
|
490
|
+
endpoint=f"{self.app.state.router_prefix}/_mgmt/metrics",
|
|
491
|
+
)
|
|
492
|
+
|
|
480
493
|
# customize route dependencies
|
|
481
494
|
for scopes, dependencies in self.route_dependencies:
|
|
482
495
|
self.add_route_dependencies(scopes=scopes, dependencies=dependencies)
|
|
@@ -27,7 +27,6 @@ DEFAULT_STATUS_CODES = {
|
|
|
27
27
|
ConflictError: status.HTTP_409_CONFLICT,
|
|
28
28
|
ForeignKeyError: status.HTTP_424_FAILED_DEPENDENCY,
|
|
29
29
|
DatabaseError: status.HTTP_424_FAILED_DEPENDENCY,
|
|
30
|
-
Exception: status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
31
30
|
InvalidQueryParameter: status.HTTP_400_BAD_REQUEST,
|
|
32
31
|
ResponseValidationError: status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
33
32
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Prometheus metrics with low-cardinality STAC operation labels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from prometheus_client import REGISTRY, Counter, Histogram
|
|
12
|
+
from prometheus_fastapi_instrumentator import Instrumentator
|
|
13
|
+
from prometheus_fastapi_instrumentator.metrics import Info
|
|
14
|
+
|
|
15
|
+
REQUESTS: Any = Counter(
|
|
16
|
+
"http_requests_total",
|
|
17
|
+
"Total HTTP requests by STAC operation.",
|
|
18
|
+
labelnames=("operation", "method", "status"),
|
|
19
|
+
registry=REGISTRY,
|
|
20
|
+
)
|
|
21
|
+
LATENCY: Any = Histogram(
|
|
22
|
+
"http_request_duration_seconds",
|
|
23
|
+
"HTTP request latency by STAC operation.",
|
|
24
|
+
labelnames=("operation", "method"),
|
|
25
|
+
buckets=(0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, float("inf")),
|
|
26
|
+
registry=REGISTRY,
|
|
27
|
+
)
|
|
28
|
+
except ImportError: # pragma: no cover
|
|
29
|
+
REGISTRY = None # type: ignore
|
|
30
|
+
Counter = None # type: ignore
|
|
31
|
+
Histogram = None # type: ignore
|
|
32
|
+
Instrumentator = None # type: ignore
|
|
33
|
+
Info = None # type: ignore
|
|
34
|
+
REQUESTS = None
|
|
35
|
+
LATENCY = None
|
|
36
|
+
|
|
37
|
+
OPERATIONS: dict[tuple[str, str], str] = {
|
|
38
|
+
("GET", "/"): "landing",
|
|
39
|
+
("GET", "/conformance"): "conformance",
|
|
40
|
+
("GET", "/collections"): "list_collections",
|
|
41
|
+
("GET", "/collections/{collection_id}"): "get_collection",
|
|
42
|
+
("GET", "/collections/{collection_id}/items"): "list_items",
|
|
43
|
+
("GET", "/collections/{collection_id}/items/{item_id}"): "get_item",
|
|
44
|
+
("GET", "/search"): "search",
|
|
45
|
+
("POST", "/search"): "search",
|
|
46
|
+
("POST", "/collections"): "create_collection",
|
|
47
|
+
("PUT", "/collections/{collection_id}"): "edit_collection",
|
|
48
|
+
("PATCH", "/collections/{collection_id}"): "edit_collection",
|
|
49
|
+
("DELETE", "/collections/{collection_id}"): "delete_collection",
|
|
50
|
+
("POST", "/collections/{collection_id}/items"): "create_item",
|
|
51
|
+
("PUT", "/collections/{collection_id}/items/{item_id}"): "edit_item",
|
|
52
|
+
("PATCH", "/collections/{collection_id}/items/{item_id}"): "edit_item",
|
|
53
|
+
("DELETE", "/collections/{collection_id}/items/{item_id}"): "delete_item",
|
|
54
|
+
("POST", "/collections/{collection_id}/bulk_items"): "bulk",
|
|
55
|
+
("GET", "/queryables"): "queryables",
|
|
56
|
+
("GET", "/collections/{collection_id}/queryables"): "queryables",
|
|
57
|
+
("GET", "/aggregations"): "aggregations",
|
|
58
|
+
("POST", "/aggregations"): "aggregations",
|
|
59
|
+
("GET", "/collections/{collection_id}/aggregations"): "aggregations",
|
|
60
|
+
("POST", "/collections/{collection_id}/aggregations"): "aggregations",
|
|
61
|
+
("GET", "/aggregate"): "aggregate",
|
|
62
|
+
("POST", "/aggregate"): "aggregate",
|
|
63
|
+
("GET", "/collections/{collection_id}/aggregate"): "aggregate",
|
|
64
|
+
("POST", "/collections/{collection_id}/aggregate"): "aggregate",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def register_operations(mapping: dict[tuple[str, str], str]) -> None:
|
|
69
|
+
"""Register or override operation labels for route templates."""
|
|
70
|
+
OPERATIONS.update(mapping)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def resolve_operation(method: str, route: str | None, router_prefix: str = "") -> str:
|
|
74
|
+
"""Map a request method and route template to a STAC operation label."""
|
|
75
|
+
if not route or route == "none":
|
|
76
|
+
return "unknown"
|
|
77
|
+
|
|
78
|
+
path = route
|
|
79
|
+
prefix = (router_prefix or "").rstrip("/")
|
|
80
|
+
if prefix and path.startswith(prefix):
|
|
81
|
+
path = path[len(prefix) :] or "/"
|
|
82
|
+
|
|
83
|
+
operation = OPERATIONS.get((method.upper(), path))
|
|
84
|
+
if operation:
|
|
85
|
+
return operation
|
|
86
|
+
|
|
87
|
+
if path.startswith("/catalogs"):
|
|
88
|
+
return "catalog"
|
|
89
|
+
if "bulk" in path:
|
|
90
|
+
return "bulk"
|
|
91
|
+
|
|
92
|
+
return "unknown"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def record_stac_metrics(info: Info) -> None:
|
|
96
|
+
"""Record request count and latency using STAC operation labels."""
|
|
97
|
+
route = info.request.scope.get("route")
|
|
98
|
+
route_path = getattr(route, "path", None)
|
|
99
|
+
router_prefix = getattr(info.request.app.state, "router_prefix", "") or ""
|
|
100
|
+
operation = resolve_operation(info.method, route_path, router_prefix)
|
|
101
|
+
|
|
102
|
+
REQUESTS.labels(operation, info.method, info.modified_status).inc()
|
|
103
|
+
LATENCY.labels(operation, info.method).observe(info.modified_duration)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def instrument_app(app: FastAPI, endpoint: str) -> None:
|
|
107
|
+
"""Instrument a FastAPI app and expose Prometheus metrics.
|
|
108
|
+
|
|
109
|
+
Must be called during app construction (e.g. from ``StacApi``), before any
|
|
110
|
+
requests. Adding middleware after the app has started is not supported.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
ImportError: If ``stac-fastapi-api[metrics]`` is not installed.
|
|
114
|
+
"""
|
|
115
|
+
if Instrumentator is None:
|
|
116
|
+
raise ImportError(
|
|
117
|
+
"Prometheus metrics require the optional dependency "
|
|
118
|
+
"`stac-fastapi-api[metrics]`. Install with: "
|
|
119
|
+
"pip install 'stac-fastapi-api[metrics]'"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
(
|
|
123
|
+
Instrumentator(
|
|
124
|
+
should_group_status_codes=True,
|
|
125
|
+
should_ignore_untemplated=True,
|
|
126
|
+
excluded_handlers=[".*/_mgmt/.*"],
|
|
127
|
+
)
|
|
128
|
+
.add(record_stac_metrics)
|
|
129
|
+
.instrument(app)
|
|
130
|
+
.expose(app, endpoint=endpoint, include_in_schema=False)
|
|
131
|
+
)
|
|
@@ -4,6 +4,7 @@ from typing import Annotated, Literal, cast
|
|
|
4
4
|
|
|
5
5
|
import attr
|
|
6
6
|
from fastapi import Path, Query
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
7
8
|
from pydantic import BaseModel, create_model
|
|
8
9
|
from stac_pydantic.shared import BBox
|
|
9
10
|
|
|
@@ -19,12 +20,6 @@ from stac_fastapi.types.search import (
|
|
|
19
20
|
_validate_datetime,
|
|
20
21
|
)
|
|
21
22
|
|
|
22
|
-
try:
|
|
23
|
-
import orjson # noqa
|
|
24
|
-
from fastapi.responses import ORJSONResponse as JSONResponse
|
|
25
|
-
except ImportError: # pragma: nocover
|
|
26
|
-
from starlette.responses import JSONResponse # type: ignore [assignment]
|
|
27
|
-
|
|
28
23
|
|
|
29
24
|
def create_request_model(
|
|
30
25
|
model_name="SearchGetRequest",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|