Skip to content

Commit 5f1beb0

Browse files
committed
update from main
2 parents fbfa774 + 476796f commit 5f1beb0

File tree

7 files changed

+137
-13
lines changed

7 files changed

+137
-13
lines changed

.github/workflows/packages.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
with:
2929
images: ghcr.io/stac-utils/stac-fastapi-pgstac
3030
- name: Build and push Docker image
31-
uses: docker/build-push-action@v6.15.0
31+
uses: docker/build-push-action@v6.16.0
3232
with:
3333
context: .
3434
file: Dockerfile

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
## [Unreleased]
44

5+
### Changed
6+
57
- Disable transaction and bulk_transactions extensions by default **breaking change**
8+
- update `stac-fastapi-*` version requirements to `>=5.2,<6.0`
9+
- add pgstac health-check in `/_mgmt/health`
610

711
## [5.0.2] - 2025-04-07
812

setup.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
"attrs",
1010
"orjson",
1111
"pydantic",
12-
"stac_pydantic==3.1.*",
13-
"stac-fastapi.api>=5.1,<6.0",
14-
"stac-fastapi.extensions>=5.1,<6.0",
15-
"stac-fastapi.types>=5.1,<6.0",
12+
"stac-fastapi.api>=5.2,<6.0",
13+
"stac-fastapi.extensions>=5.2,<6.0",
14+
"stac-fastapi.types>=5.2,<6.0",
1615
"asyncpg",
1716
"buildpg",
1817
"brotli_asgi",
@@ -27,7 +26,7 @@
2726
"pytest-postgresql",
2827
"pytest",
2928
"pytest-cov",
30-
"pytest-asyncio>=0.17,<0.26.0",
29+
"pytest-asyncio>=0.17,<0.27",
3130
"pre-commit",
3231
"requests",
3332
"shapely",
@@ -43,7 +42,7 @@
4342
"griffe-inherited-docstrings>=1.0.0",
4443
"mkdocstrings[python]>=0.25.1",
4544
],
46-
"server": ["uvicorn[standard]==0.34.0"],
45+
"server": ["uvicorn[standard]==0.34"],
4746
"awslambda": ["mangum"],
4847
}
4948

stac_fastapi/pgstac/app.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010

1111
from brotli_asgi import BrotliMiddleware
1212
from fastapi import FastAPI
13-
from fastapi.responses import ORJSONResponse
1413
from stac_fastapi.api.app import StacApi
1514
from stac_fastapi.api.middleware import CORSMiddleware, ProxyHeaderMiddleware
1615
from stac_fastapi.api.models import (
1716
EmptyRequest,
1817
ItemCollectionUri,
18+
JSONResponse,
1919
create_get_request_model,
2020
create_post_request_model,
2121
create_request_model,
@@ -40,7 +40,7 @@
4040
from starlette.middleware import Middleware
4141

4242
from stac_fastapi.pgstac.config import Settings
43-
from stac_fastapi.pgstac.core import CoreCrudClient
43+
from stac_fastapi.pgstac.core import CoreCrudClient, health_check
4444
from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
4545
from stac_fastapi.pgstac.extensions import QueryExtension
4646
from stac_fastapi.pgstac.extensions.filter import FiltersClient
@@ -54,7 +54,7 @@
5454
"transaction": TransactionExtension(
5555
client=TransactionsClient(),
5656
settings=settings,
57-
response_class=ORJSONResponse,
57+
response_class=JSONResponse,
5858
),
5959
"bulk_transactions": BulkTransactionExtension(client=BulkTransactionsClient()),
6060
}
@@ -110,7 +110,7 @@
110110
TransactionExtension(
111111
client=TransactionsClient(),
112112
settings=settings,
113-
response_class=ORJSONResponse,
113+
response_class=JSONResponse,
114114
),
115115
)
116116

@@ -180,7 +180,7 @@ async def lifespan(app: FastAPI):
180180
settings=settings,
181181
extensions=application_extensions,
182182
client=CoreCrudClient(pgstac_search_model=post_request_model),
183-
response_class=ORJSONResponse,
183+
response_class=JSONResponse,
184184
items_get_request_model=items_get_request_model,
185185
search_get_request_model=get_request_model,
186186
search_post_request_model=post_request_model,
@@ -194,6 +194,7 @@ async def lifespan(app: FastAPI):
194194
allow_methods=settings.cors_methods,
195195
),
196196
],
197+
health_check=health_check,
197198
)
198199
app = api.app
199200

stac_fastapi/pgstac/core.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,3 +605,49 @@ def _clean_search_args( # noqa: C901
605605
clean[k] = v
606606

607607
return clean
608+
609+
610+
async def health_check(request: Request) -> Union[Dict, JSONResponse]:
611+
"""PgSTAC HealthCheck."""
612+
resp = {
613+
"status": "UP",
614+
"lifespan": {
615+
"status": "UP",
616+
},
617+
}
618+
if not hasattr(request.app.state, "get_connection"):
619+
return JSONResponse(
620+
status_code=503,
621+
content={
622+
"status": "DOWN",
623+
"lifespan": {
624+
"status": "DOWN",
625+
"message": "application lifespan wasn't run",
626+
},
627+
"pgstac": {
628+
"status": "DOWN",
629+
"message": "Could not connect to database",
630+
},
631+
},
632+
)
633+
634+
try:
635+
async with request.app.state.get_connection(request, "r") as conn:
636+
q, p = render(
637+
"""SELECT pgstac.get_version();""",
638+
)
639+
version = await conn.fetchval(q, *p)
640+
except Exception as e:
641+
resp["status"] = "DOWN"
642+
resp["pgstac"] = {
643+
"status": "DOWN",
644+
"message": str(e),
645+
}
646+
return JSONResponse(status_code=503, content=resp)
647+
648+
resp["pgstac"] = {
649+
"status": "UP",
650+
"pgstac_version": version,
651+
}
652+
653+
return resp

tests/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343

4444
from stac_fastapi.pgstac.app import api as default_api
4545
from stac_fastapi.pgstac.config import PostgresSettings, Settings
46-
from stac_fastapi.pgstac.core import CoreCrudClient
46+
from stac_fastapi.pgstac.core import CoreCrudClient, health_check
4747
from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
4848
from stac_fastapi.pgstac.extensions import QueryExtension
4949
from stac_fastapi.pgstac.extensions.filter import FiltersClient
@@ -192,6 +192,7 @@ def api_client(request):
192192
collections_get_request_model=collection_search_extension.GET,
193193
response_class=ORJSONResponse,
194194
router=APIRouter(prefix=prefix),
195+
health_check=health_check,
195196
)
196197

197198
return api
@@ -303,6 +304,7 @@ def api_client_no_ext():
303304
TransactionExtension(client=TransactionsClient(), settings=api_settings)
304305
],
305306
client=CoreCrudClient(),
307+
health_check=health_check,
306308
)
307309

308310

tests/resources/test_mgmt.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
from httpx import ASGITransport, AsyncClient
2+
from stac_fastapi.api.app import StacApi
3+
4+
from stac_fastapi.pgstac.config import PostgresSettings, Settings
5+
from stac_fastapi.pgstac.core import CoreCrudClient, health_check
6+
from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
7+
8+
19
async def test_ping_no_param(app_client):
210
"""
311
Test ping endpoint with a mocked client.
@@ -7,3 +15,67 @@ async def test_ping_no_param(app_client):
715
res = await app_client.get("/_mgmt/ping")
816
assert res.status_code == 200
917
assert res.json() == {"message": "PONG"}
18+
19+
20+
async def test_health(app_client):
21+
"""
22+
Test health endpoint
23+
24+
Args:
25+
app_client (TestClient): mocked client fixture
26+
27+
"""
28+
res = await app_client.get("/_mgmt/health")
29+
assert res.status_code == 200
30+
body = res.json()
31+
assert body["status"] == "UP"
32+
assert body["pgstac"]["status"] == "UP"
33+
assert body["pgstac"]["pgstac_version"]
34+
35+
36+
async def test_health_503(database):
37+
"""Test health endpoint error."""
38+
39+
# No lifespan so no `get_connection` is application state
40+
api = StacApi(
41+
settings=Settings(testing=True),
42+
extensions=[],
43+
client=CoreCrudClient(),
44+
health_check=health_check,
45+
)
46+
47+
async with AsyncClient(
48+
transport=ASGITransport(app=api.app), base_url="http://test"
49+
) as client:
50+
res = await client.get("/_mgmt/health")
51+
assert res.status_code == 503
52+
body = res.json()
53+
assert body["status"] == "DOWN"
54+
assert body["lifespan"]["status"] == "DOWN"
55+
assert body["lifespan"]["message"] == "application lifespan wasn't run"
56+
assert body["pgstac"]["status"] == "DOWN"
57+
assert body["pgstac"]["message"] == "Could not connect to database"
58+
59+
# No lifespan so no `get_connection` is application state
60+
postgres_settings = PostgresSettings(
61+
postgres_user=database.user,
62+
postgres_pass=database.password,
63+
postgres_host_reader=database.host,
64+
postgres_host_writer=database.host,
65+
postgres_port=database.port,
66+
postgres_dbname=database.dbname,
67+
)
68+
# Create connection pool but close it just after
69+
await connect_to_db(api.app, postgres_settings=postgres_settings)
70+
await close_db_connection(api.app)
71+
72+
async with AsyncClient(
73+
transport=ASGITransport(app=api.app), base_url="http://test"
74+
) as client:
75+
res = await client.get("/_mgmt/health")
76+
assert res.status_code == 503
77+
body = res.json()
78+
assert body["status"] == "DOWN"
79+
assert body["lifespan"]["status"] == "UP"
80+
assert body["pgstac"]["status"] == "DOWN"
81+
assert body["pgstac"]["message"] == "pool is closed"

0 commit comments

Comments
 (0)