Skip to content

Commit d3e9911

Browse files
feat: collection endpoint (#18)
* Migrate collection endpoint changes * push unsaved changes * adding vpc configuration * missing parameters * update loader * uncomment auth * test: collection endpoint test/style changes * Fix linting errors. * Add AWSLambdaVPCAccessExecutionRole to ingestor role for deployment. --------- Co-authored-by: sharkinsspatial <[email protected]>
1 parent a3f2e32 commit d3e9911

File tree

9 files changed

+416
-90
lines changed

9 files changed

+416
-90
lines changed

lib/ingestor-api/index.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export class StacIngestor extends Construct {
3636
env,
3737
dataAccessRole: props.dataAccessRole,
3838
stage: props.stage,
39+
dbSecret: props.stacDbSecret,
40+
dbVpc: props.vpc,
41+
dbSecurityGroup: props.stacDbSecurityGroup,
42+
subnetSelection: props.subnetSelection,
3943
});
4044

4145
this.buildApiEndpoint({
@@ -84,6 +88,10 @@ export class StacIngestor extends Construct {
8488
env: Record<string, string>;
8589
dataAccessRole: iam.IRole;
8690
stage: string;
91+
dbSecret: secretsmanager.ISecret;
92+
dbVpc: ec2.IVpc;
93+
dbSecurityGroup: ec2.ISecurityGroup;
94+
subnetSelection: ec2.SubnetSelection;
8795
}): PythonFunction {
8896
const handler_role = new iam.Role(this, "execution-role", {
8997
description:
@@ -92,7 +100,10 @@ export class StacIngestor extends Construct {
92100
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
93101
managedPolicies: [
94102
iam.ManagedPolicy.fromAwsManagedPolicyName(
95-
"service-role/AWSLambdaBasicExecutionRole"
103+
"service-role/AWSLambdaBasicExecutionRole",
104+
),
105+
iam.ManagedPolicy.fromAwsManagedPolicyName(
106+
"service-role/AWSLambdaVPCAccessExecutionRole",
96107
),
97108
],
98109
});
@@ -101,12 +112,25 @@ export class StacIngestor extends Construct {
101112
entry: `${__dirname}/runtime`,
102113
index: "src/handler.py",
103114
runtime: lambda.Runtime.PYTHON_3_9,
104-
environment: props.env,
105115
timeout: Duration.seconds(30),
116+
environment: { DB_SECRET_ARN: props.dbSecret.secretArn, ...props.env },
117+
vpc: props.dbVpc,
118+
vpcSubnets: props.subnetSelection,
119+
allowPublicSubnet: true,
106120
role: handler_role,
107121
memorySize: 2048,
108122
});
109123

124+
// Allow handler to read DB secret
125+
props.dbSecret.grantRead(handler);
126+
127+
// Allow handler to connect to DB
128+
props.dbSecurityGroup.addIngressRule(
129+
handler.connections.securityGroups[0],
130+
ec2.Port.tcp(5432),
131+
"Allow connections from STAC Ingestor"
132+
);
133+
110134
props.table.grantReadWriteData(handler);
111135
props.dataAccessRole.grantAssumeRole(handler_role);
112136

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import os
2+
3+
from pypgstac.db import PgstacDB
4+
5+
from .schemas import StacCollection
6+
from .utils import (
7+
IngestionType,
8+
convert_decimals_to_float,
9+
get_db_credentials,
10+
load_into_pgstac,
11+
)
12+
from .vedaloader import VEDALoader
13+
14+
15+
def ingest(collection: StacCollection):
16+
"""
17+
Takes a collection model,
18+
does necessary preprocessing,
19+
and loads into the PgSTAC collection table
20+
"""
21+
creds = get_db_credentials(os.environ["DB_SECRET_ARN"])
22+
collection = [
23+
convert_decimals_to_float(collection.dict(by_alias=True, exclude_unset=True))
24+
]
25+
with PgstacDB(dsn=creds.dsn_string, debug=True) as db:
26+
load_into_pgstac(db=db, ingestions=collection, table=IngestionType.collections)
27+
28+
29+
def delete(collection_id: str):
30+
"""
31+
Deletes the collection from the database
32+
"""
33+
creds = get_db_credentials(os.environ["DB_SECRET_ARN"])
34+
with PgstacDB(dsn=creds.dsn_string, debug=True) as db:
35+
loader = VEDALoader(db=db)
36+
loader.delete_collection(collection_id)

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

Lines changed: 21 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1-
import decimal
21
import os
32
from datetime import datetime
4-
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Sequence
3+
from typing import TYPE_CHECKING, Iterator, List, Optional, Sequence
54

6-
import boto3
7-
import orjson
8-
import pydantic
95
from boto3.dynamodb.types import TypeDeserializer
106
from pypgstac.db import PgstacDB
11-
from pypgstac.load import Methods
127

138
from .dependencies import get_settings, get_table
149
from .schemas import Ingestion, Status
15-
from .vedaloader import VEDALoader
10+
from .utils import (
11+
IngestionType,
12+
convert_decimals_to_float,
13+
get_db_credentials,
14+
load_into_pgstac,
15+
)
1616

1717
if TYPE_CHECKING:
1818
from aws_lambda_typing import context as context_
@@ -33,78 +33,6 @@ def get_queued_ingestions(records: List["DynamodbRecord"]) -> Iterator[Ingestion
3333
yield ingestion
3434

3535

36-
class DbCreds(pydantic.BaseModel):
37-
username: str
38-
password: str
39-
host: str
40-
port: int
41-
dbname: str
42-
engine: str
43-
44-
@property
45-
def dsn_string(self) -> str:
46-
return f"{self.engine}://{self.username}:{self.password}@{self.host}:{self.port}/{self.dbname}" # noqa
47-
48-
49-
def get_db_credentials(secret_arn: str) -> DbCreds:
50-
"""
51-
Load pgSTAC database credentials from AWS Secrets Manager.
52-
"""
53-
print("Fetching DB credentials...")
54-
session = boto3.session.Session(region_name=secret_arn.split(":")[3])
55-
client = session.client(service_name="secretsmanager")
56-
response = client.get_secret_value(SecretId=secret_arn)
57-
return DbCreds.parse_raw(response["SecretString"])
58-
59-
60-
def convert_decimals_to_float(item: Dict[str, Any]) -> Dict[str, Any]:
61-
"""
62-
DynamoDB stores floats as Decimals. We want to convert them back to floats
63-
before inserting them into pgSTAC to avoid any issues when the records are
64-
converted to JSON by pgSTAC.
65-
"""
66-
67-
def decimal_to_float(obj):
68-
if isinstance(obj, decimal.Decimal):
69-
return float(obj)
70-
raise TypeError
71-
72-
return orjson.loads(
73-
orjson.dumps(
74-
item,
75-
default=decimal_to_float,
76-
)
77-
)
78-
79-
80-
def load_into_pgstac(creds: DbCreds, ingestions: Sequence[Ingestion]):
81-
"""
82-
Bulk insert STAC records into pgSTAC.
83-
"""
84-
with PgstacDB(dsn=creds.dsn_string, debug=True) as db:
85-
loader = VEDALoader(db=db)
86-
87-
items = [
88-
# NOTE: Important to deserialize values to convert decimals to floats
89-
convert_decimals_to_float(i.item)
90-
for i in ingestions
91-
]
92-
93-
print(f"Ingesting {len(items)} items")
94-
loading_result = loader.load_items(
95-
file=items,
96-
# use insert_ignore to avoid overwritting existing items or upsert to replace
97-
insert_mode=Methods.upsert,
98-
)
99-
100-
# Trigger update on summaries and extents
101-
collections = set([item.collection for item in items])
102-
for collection in collections:
103-
loader.update_collection_summaries(collection)
104-
105-
return loading_result
106-
107-
10836
def update_dynamodb(
10937
ingestions: Sequence[Ingestion],
11038
status: Status,
@@ -136,14 +64,24 @@ def handler(event: "events.DynamoDBStreamEvent", context: "context_.Context"):
13664
print("No queued ingestions to process")
13765
return
13866

67+
items = [
68+
# NOTE: Important to deserialize values to convert decimals to floats
69+
convert_decimals_to_float(ingestion.item)
70+
for ingestion in ingestions
71+
]
72+
73+
creds = get_db_credentials(os.environ["DB_SECRET_ARN"])
74+
13975
# Insert into PgSTAC DB
14076
outcome = Status.succeeded
14177
message = None
14278
try:
143-
load_into_pgstac(
144-
creds=get_db_credentials(os.environ["DB_SECRET_ARN"]),
145-
ingestions=ingestions,
146-
)
79+
with PgstacDB(dsn=creds.dsn_string, debug=True) as db:
80+
load_into_pgstac(
81+
db=db,
82+
ingestions=items,
83+
table=IngestionType.items,
84+
)
14785
except Exception as e:
14886
print(f"Encountered failure loading items into pgSTAC: {e}")
14987
outcome = Status.failed

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from fastapi import Depends, FastAPI, HTTPException
22

3+
from . import collection as collection_loader
34
from . import config, dependencies, schemas, services
45

56
app = FastAPI(
@@ -84,6 +85,38 @@ def cancel_ingestion(
8485
return ingestion.cancel(db)
8586

8687

88+
@app.post(
89+
"/collections",
90+
tags=["Collection"],
91+
status_code=201,
92+
dependencies=[Depends(dependencies.get_username)],
93+
)
94+
def publish_collection(collection: schemas.StacCollection):
95+
# pgstac create collection
96+
try:
97+
collection_loader.ingest(collection)
98+
return {f"Successfully published: {collection.id}"}
99+
except Exception as e:
100+
raise HTTPException(
101+
status_code=400,
102+
detail=(f"Unable to publish collection: {e}"),
103+
)
104+
105+
106+
@app.delete(
107+
"/collections/{collection_id}",
108+
tags=["Collection"],
109+
dependencies=[Depends(dependencies.get_username)],
110+
)
111+
def delete_collection(collection_id: str):
112+
try:
113+
collection_loader.delete(collection_id=collection_id)
114+
return {f"Successfully deleted: {collection_id}"}
115+
except Exception as e:
116+
print(e)
117+
raise HTTPException(status_code=400, detail=(f"{e}"))
118+
119+
87120
@app.get("/auth/me")
88121
def who_am_i(username=Depends(dependencies.get_username)):
89122
"""

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from fastapi.exceptions import RequestValidationError
1111
from pydantic import BaseModel, PositiveInt, dataclasses, error_wrappers, validator
12-
from stac_pydantic import Item, shared
12+
from stac_pydantic import Collection, Item, shared
1313

1414
from . import validators
1515

@@ -43,7 +43,13 @@ def exists(cls, collection):
4343
return collection
4444

4545

46+
class StacCollection(Collection):
47+
id: str
48+
item_assets: Dict
49+
50+
4651
class Status(str, enum.Enum):
52+
started = "started"
4753
queued = "queued"
4854
failed = "failed"
4955
succeeded = "succeeded"

0 commit comments

Comments
 (0)