Skip to content

Commit fef6ce2

Browse files
committed
feat: update ingestor-api runtime to use uv
chore: deal with pydantic deprecation warnings
1 parent 9f8aec8 commit fef6ce2

19 files changed

Lines changed: 1957 additions & 88 deletions

.github/workflows/deploy.yaml

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,23 @@ jobs:
130130
select(.key | test("tipgapioutput"; "i")) |
131131
.value' | head -1)
132132
133+
INGESTOR_API_URL=$(cat stack_outputs.json | jq -r '
134+
to_entries[] |
135+
select(.key | test("ingestorapioutput"; "i")) |
136+
.value' | head -1)
137+
133138
echo "Extracted URLs:"
134139
echo "STAC_API_URL: $STAC_API_URL"
135140
echo "TITILER_PGSTAC_API_URL: $TITILER_PGSTAC_API_URL"
136141
echo "TIPG_API_URL: $TIPG_API_URL"
142+
echo "INGESTOR_API_URL: $INGESTOR_API_URL"
137143
138144
# Array of API URLs to check
139145
declare -a API_HEALTH_ENDPOINTS=(
140146
"STAC_API_URL:${STAC_API_URL}_mgmt/health"
141147
"TITILER_PGSTAC_API_URL:${TITILER_PGSTAC_API_URL}healthz"
142148
"TIPG_API_URL:${TIPG_API_URL}healthz"
149+
"INGESTOR_API_URL:${INGESTOR_API_URL}ingestions"
143150
)
144151
145152
# Check each API
@@ -166,6 +173,127 @@ jobs:
166173
fi
167174
done
168175
176+
echo "=== Ingestor Integration Test ==="
177+
178+
if [ -n "$INGESTOR_API_URL" ] && [ "$INGESTOR_API_URL" != "null" ]; then
179+
COLLECTION_ID="integration-test-collection"
180+
ITEM_ID="integration-test-item-001"
181+
182+
# Use a public URL that responds to HEAD requests for asset validation
183+
PUBLIC_ASSET_URL="https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/simple-item.json"
184+
185+
# No JWKS_URL configured, so identity is provided via ?provided_by query param
186+
INGESTOR_USER="ci-test"
187+
188+
echo "--- Posting collection $COLLECTION_ID ---"
189+
COLLECTION_STATUS=$(curl -s -o /tmp/collection_response.json -w "%{http_code}" \
190+
--max-time 30 \
191+
-X POST "${INGESTOR_API_URL}collections?provided_by=${INGESTOR_USER}" \
192+
-H "Content-Type: application/json" \
193+
-d "{
194+
\"id\": \"$COLLECTION_ID\",
195+
\"type\": \"Collection\",
196+
\"stac_version\": \"1.0.0\",
197+
\"description\": \"Integration test collection\",
198+
\"title\": \"Integration Test Collection\",
199+
\"license\": \"proprietary\",
200+
\"item_assets\": {
201+
\"data\": {\"type\": \"application/json\", \"roles\": [\"data\"]}
202+
},
203+
\"extent\": {
204+
\"spatial\": {\"bbox\": [[-180, -90, 180, 90]]},
205+
\"temporal\": {\"interval\": [[\"2020-01-01T00:00:00Z\", null]]}
206+
},
207+
\"links\": [],
208+
\"stac_extensions\": []
209+
}")
210+
211+
echo "Collection POST status: $COLLECTION_STATUS"
212+
cat /tmp/collection_response.json
213+
214+
if [ "$COLLECTION_STATUS" != "201" ]; then
215+
echo "❌ Failed to post collection (status $COLLECTION_STATUS)"
216+
exit 1
217+
fi
218+
echo "✅ Collection posted successfully"
219+
220+
echo "--- Posting item $ITEM_ID ---"
221+
ITEM_STATUS=$(curl -s -o /tmp/item_response.json -w "%{http_code}" \
222+
--max-time 30 \
223+
-X POST "${INGESTOR_API_URL}ingestions?provided_by=${INGESTOR_USER}" \
224+
-H "Content-Type: application/json" \
225+
-d "{
226+
\"stac_version\": \"1.0.0\",
227+
\"stac_extensions\": [],
228+
\"type\": \"Feature\",
229+
\"id\": \"$ITEM_ID\",
230+
\"collection\": \"$COLLECTION_ID\",
231+
\"bbox\": [-180, -90, 180, 90],
232+
\"geometry\": {
233+
\"type\": \"Polygon\",
234+
\"coordinates\": [[[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]]]
235+
},
236+
\"properties\": {\"datetime\": \"2020-01-01T00:00:00Z\"},
237+
\"assets\": {
238+
\"data\": {
239+
\"href\": \"$PUBLIC_ASSET_URL\",
240+
\"type\": \"application/json\",
241+
\"roles\": [\"data\"]
242+
}
243+
},
244+
\"links\": []
245+
}")
246+
247+
echo "Item POST status: $ITEM_STATUS"
248+
cat /tmp/item_response.json
249+
250+
if [ "$ITEM_STATUS" != "201" ]; then
251+
echo "❌ Failed to post item (status $ITEM_STATUS)"
252+
exit 1
253+
fi
254+
echo "✅ Item queued for ingestion"
255+
256+
echo "--- Polling ingestion status for $ITEM_ID ---"
257+
INGESTION_STATUS="queued"
258+
POLL_ATTEMPTS=0
259+
MAX_POLL_ATTEMPTS=20
260+
261+
while [ "$INGESTION_STATUS" = "queued" ] || [ "$INGESTION_STATUS" = "started" ]; do
262+
if [ "$POLL_ATTEMPTS" -ge "$MAX_POLL_ATTEMPTS" ]; then
263+
echo "❌ Timed out waiting for ingestion of $ITEM_ID (status: $INGESTION_STATUS)"
264+
exit 1
265+
fi
266+
267+
sleep 15
268+
POLL_ATTEMPTS=$((POLL_ATTEMPTS + 1))
269+
270+
INGESTION_RESPONSE=$(curl -s --max-time 30 "${INGESTOR_API_URL}ingestions/${ITEM_ID}?provided_by=${INGESTOR_USER}")
271+
INGESTION_STATUS=$(echo "$INGESTION_RESPONSE" | jq -r '.status // "unknown"')
272+
echo "Attempt $POLL_ATTEMPTS: ingestion status = $INGESTION_STATUS"
273+
done
274+
275+
if [ "$INGESTION_STATUS" != "succeeded" ]; then
276+
echo "❌ Ingestion failed with status: $INGESTION_STATUS"
277+
echo "$INGESTION_RESPONSE" | jq .
278+
exit 1
279+
fi
280+
echo "✅ Item ingested successfully"
281+
282+
echo "--- Verifying item in STAC API ---"
283+
STAC_ITEM_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
284+
--max-time 30 \
285+
"${STAC_API_URL}collections/$COLLECTION_ID/items/$ITEM_ID")
286+
287+
if [ "$STAC_ITEM_STATUS" = "200" ]; then
288+
echo "✅ Item $ITEM_ID is accessible in STAC API"
289+
else
290+
echo "❌ Item $ITEM_ID not found in STAC API (status $STAC_ITEM_STATUS)"
291+
exit 1
292+
fi
293+
else
294+
echo "⚠️ INGESTOR_API_URL not found in stack outputs, skipping ingestor test"
295+
fi
296+
169297
echo "=== Operational Checks Complete ==="
170298
cd -
171299

.github/workflows/tox.yaml

Lines changed: 0 additions & 26 deletions
This file was deleted.

lib/ingestor-api/index.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
aws_apigateway as apigateway,
33
aws_logs,
4+
CfnOutput,
45
Duration,
56
aws_dynamodb as dynamodb,
67
aws_ec2 as ec2,
@@ -23,6 +24,11 @@ export class StacIngestor extends Construct {
2324
table: dynamodb.Table;
2425
public handlerRole: iam.Role;
2526

27+
/**
28+
* URL of the Ingestor API.
29+
*/
30+
public readonly url: string;
31+
2632
constructor(scope: Construct, id: string, props: StacIngestorProps) {
2733
super(scope, id);
2834

@@ -64,14 +70,21 @@ export class StacIngestor extends Construct {
6470
pgstacVersion: props.pgstacVersion,
6571
});
6672

67-
this.buildApiEndpoint({
73+
const api = this.buildApiEndpoint({
6874
handler,
6975
stage: props.stage,
7076
endpointConfiguration: props.apiEndpointConfiguration,
7177
policy: props.apiPolicy,
7278
ingestorDomainNameOptions: props.ingestorDomainNameOptions,
7379
});
7480

81+
this.url = api.url;
82+
83+
new CfnOutput(this, "ingestor-api-output", {
84+
exportName: `${Stack.of(this).stackName}-ingestor-url`,
85+
value: this.url,
86+
});
87+
7588
this.buildIngestor({
7689
table: this.table,
7790
env: env,

lib/ingestor-api/runtime/Dockerfile

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
ARG PYTHON_VERSION
2-
1+
ARG PYTHON_VERSION=3.12
32
FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION}
3+
COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /bin/
44

55
WORKDIR /tmp
66

7-
RUN dnf install -y git
8-
9-
RUN python -m pip install pip -U
10-
11-
COPY runtime/requirements.txt requirements.txt
7+
COPY runtime/pyproject.toml runtime/uv.lock ./
128

139
ARG PGSTAC_VERSION
14-
RUN echo "pypgstac==${PGSTAC_VERSION}" > constraints.txt
15-
RUN python -m pip install -r requirements.txt -c constraints.txt "mangum>=0.14,<0.15" -t /asset --no-binary pydantic
10+
RUN <<EOF
11+
uv add --no-sync pypgstac==${PGSTAC_VERSION}
12+
uv export --locked --no-editable --no-dev --format requirements.txt -o requirements.txt
13+
uv pip install \
14+
--compile-bytecode \
15+
--target /asset \
16+
--no-cache-dir \
17+
--disable-pip-version-check \
18+
-r requirements.txt
19+
EOF
1620

1721
RUN mkdir -p /asset/src
1822
COPY runtime/src/*.py /asset/src/

lib/ingestor-api/runtime/dev_requirements.txt

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[project]
2+
name = "ingestor-api"
3+
version = "0.0.0"
4+
description = "ingestor-api runtime"
5+
requires-python = ">=3.12"
6+
dependencies = [
7+
"authlib>=1.3",
8+
"boto3",
9+
"cachetools>=5.0",
10+
"fastapi>=0.110",
11+
"mangum==0.19",
12+
"orjson>=3.9",
13+
"psycopg[binary,pool]>=3.0",
14+
"pydantic>=2.0",
15+
"pydantic-settings>=2.0",
16+
"pydantic-ssm-settings>=1.0,<2.0",
17+
"pypgstac",
18+
"requests>=2.27",
19+
"stac-pydantic>=3.0,<4.0",
20+
]
21+
22+
[dependency-groups]
23+
dev = [
24+
"boto3",
25+
"httpx>=0.24",
26+
"moto[dynamodb,ssm]>=4.0,<5.0",
27+
"pytest>=7.0",
28+
]
29+
30+
[tool.hatch.build.targets.wheel]
31+
packages = ["src"]
32+
33+
[build-system]
34+
requires = ["hatchling"]
35+
build-backend = "hatchling.build"
36+
37+
[tool.pytest.ini_options]
38+
testpaths = ["tests"]

lib/ingestor-api/runtime/requirements.txt

Lines changed: 0 additions & 10 deletions
This file was deleted.

lib/ingestor-api/runtime/src/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
AnyHttpUrl,
88
Field,
99
constr,
10+
field_validator,
1011
)
1112
from pydantic_settings import BaseSettings
1213
from pydantic_ssm_settings.settings import SsmBaseSettings
@@ -26,6 +27,14 @@ class Settings(BaseSettings):
2627
description="URL of JWKS, e.g. https://cognito-idp.{region}.amazonaws.com/{userpool_id}/.well-known/jwks.json", # noqa
2728
)
2829

30+
@field_validator("jwks_url", mode="before")
31+
@classmethod
32+
def empty_str_to_none(cls, v):
33+
"""Treat empty string as None so callers can disable auth by setting JWKS_URL=''."""
34+
if v == "":
35+
return None
36+
return v
37+
2938
stac_url: HttpUrlString = Field(description="URL of STAC API")
3039

3140
data_access_role: AwsArn = Field(

lib/ingestor-api/runtime/src/ingestor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def update_dynamodb(
4242
with table.batch_writer(overwrite_by_pkeys=["created_by", "id"]) as batch:
4343
for ingestion in ingestions:
4444
batch.put_item(
45-
Item=ingestion.copy(
45+
Item=ingestion.model_copy(
4646
update={
4747
"status": status,
4848
"message": message,

lib/ingestor-api/runtime/src/schemas.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@
33
import enum
44
import json
55
from datetime import datetime
6-
from typing import TYPE_CHECKING, Dict, List, Optional, Union
6+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
77
from urllib.parse import urlparse
88

99
from fastapi.encoders import jsonable_encoder
10-
from fastapi.exceptions import RequestValidationError
1110
from pydantic import (
1211
BaseModel,
1312
Json,
1413
PositiveInt,
1514
dataclasses,
16-
error_wrappers,
1715
field_validator,
1816
)
1917
from stac_pydantic import Collection, Item, shared
@@ -107,25 +105,19 @@ def dynamodb_dict(self):
107105
class ListIngestionRequest:
108106
status: Status = Status.queued
109107
limit: Optional[PositiveInt] = None
110-
next: Optional[str] = None
111-
112-
def __post_init_post_parse__(self) -> None:
113-
# https://github.com/tiangolo/fastapi/issues/1474#issuecomment-1049987786
114-
if self.next is None:
115-
return
108+
next: Optional[Any] = None
116109

110+
@field_validator("next", mode="before")
111+
@classmethod
112+
def decode_next_token(cls, v: Optional[str]) -> Optional[Any]:
113+
"""Decode the base64-encoded JSON pagination token supplied as a query param."""
114+
if v is None:
115+
return None
117116
try:
118-
self.next = json.loads(base64.b64decode(self.next))
117+
return json.loads(base64.b64decode(v))
119118
except (UnicodeDecodeError, binascii.Error) as e:
120-
raise RequestValidationError(
121-
[
122-
error_wrappers.ErrorWrapper(
123-
ValueError(
124-
"Unable to decode next token. Should be base64 encoded JSON"
125-
),
126-
"query.next",
127-
)
128-
]
119+
raise ValueError(
120+
"Unable to decode next token. Should be base64 encoded JSON"
129121
) from e
130122

131123

0 commit comments

Comments
 (0)