Skip to content

Commit a092a7a

Browse files
committed
feat: add PgStacApiLambda
1 parent f57928e commit a092a7a

File tree

11 files changed

+366
-10
lines changed

11 files changed

+366
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ lib/**/*.d.ts
55
.jsii
66
dist
77
docs
8+
__pycache__

lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./bootstrapper";
22
export * from "./database";
3+
export * from "./stac-api";

lib/stac-api/index.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {
2+
Stack,
3+
aws_ec2 as ec2,
4+
aws_rds as rds,
5+
aws_lambda as lambda,
6+
aws_secretsmanager as secretsmanager,
7+
CfnOutput,
8+
} from "aws-cdk-lib";
9+
import {
10+
PythonFunction,
11+
PythonFunctionProps,
12+
} from "@aws-cdk/aws-lambda-python-alpha";
13+
import { HttpApi } from "@aws-cdk/aws-apigatewayv2-alpha";
14+
import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha";
15+
import { Construct } from "constructs";
16+
17+
export class PgStacApiLambda extends Construct {
18+
constructor(scope: Construct, id: string, props: PgStacApiLambdaProps) {
19+
super(scope, id);
20+
21+
const apiCode = props.apiCode || {
22+
entry: `${__dirname}/runtime`,
23+
index: "src/handler.py",
24+
handler: "handler",
25+
};
26+
27+
const handler = new PythonFunction(this, "stac-api", {
28+
...apiCode,
29+
/**
30+
* NOTE: Unable to use Py3.9, due to issues with hashes:
31+
*
32+
* ERROR: Hashes are required in --require-hashes mode, but they are missing
33+
* from some requirements. Here is a list of those requirements along with the
34+
* hashes their downloaded archives actually had. Add lines like these to your
35+
* requirements files to prevent tampering. (If you did not enable
36+
* --require-hashes manually, note that it turns on automatically when any
37+
* package has a hash.)
38+
* anyio==3.6.1 --hash=sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be
39+
* */
40+
runtime: lambda.Runtime.PYTHON_3_8,
41+
architecture: lambda.Architecture.X86_64,
42+
environment: {
43+
PGSTAC_SECRET_ARN: props.dbSecret.secretArn,
44+
DB_MIN_CONN_SIZE: "0",
45+
DB_MAX_CONN_SIZE: "1",
46+
...props.apiEnv,
47+
},
48+
vpc: props.vpc,
49+
vpcSubnets: props.subnetSelection,
50+
allowPublicSubnet: true,
51+
memorySize: 8192,
52+
});
53+
54+
props.dbSecret.grantRead(handler);
55+
handler.connections.allowTo(props.db, ec2.Port.tcp(5432));
56+
57+
const stacApi = new HttpApi(this, "api", {
58+
defaultIntegration: new HttpLambdaIntegration("integration", handler),
59+
});
60+
61+
new CfnOutput(this, "stac-api-output", {
62+
exportName: `${Stack.of(this).stackName}-url`,
63+
value: stacApi.url!,
64+
});
65+
}
66+
}
67+
68+
export interface PgStacApiLambdaProps {
69+
/**
70+
* VPC into which the lambda should be deployed.
71+
*/
72+
readonly vpc: ec2.IVpc;
73+
74+
/**
75+
* RDS Instance with installed pgSTAC.
76+
*/
77+
readonly db: rds.IDatabaseInstance;
78+
79+
/**
80+
* Subnet into which the lambda should be deployed.
81+
*/
82+
readonly subnetSelection: ec2.SubnetSelection;
83+
84+
/**
85+
* Secret containing connection information for pgSTAC database.
86+
*/
87+
readonly dbSecret: secretsmanager.ISecret;
88+
89+
/**
90+
* Custom code to run for fastapi-pgstac.
91+
*
92+
* @default - simplified version of fastapi-pgstac
93+
*/
94+
readonly apiCode?: ApiEntrypoint;
95+
96+
/**
97+
* Customized environment variables to send to fastapi-pgstac runtime.
98+
*/
99+
readonly apiEnv?: Record<string, string>;
100+
}
101+
102+
export interface ApiEntrypoint {
103+
/**
104+
* Path to the source of the function or the location for dependencies.
105+
*/
106+
readonly entry: PythonFunctionProps["entry"];
107+
/**
108+
* The path (relative to entry) to the index file containing the exported handler.
109+
*/
110+
readonly index: PythonFunctionProps["index"];
111+
/**
112+
* The name of the exported handler in the index file.
113+
*/
114+
readonly handler: PythonFunctionProps["handler"];
115+
}

lib/stac-api/runtime/.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*/.venv/*
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
mangum==0.15.1
2+
stac-fastapi.api==2.4.1
3+
stac-fastapi.extensions==2.4.1
4+
stac-fastapi.pgstac==2.4.1
5+
stac-fastapi.types==2.4.1
6+
# https://github.com/stac-utils/stac-fastapi/pull/466
7+
pygeoif==0.7
8+
starlette_cramjam

lib/stac-api/runtime/src/__init__.py

Whitespace-only changes.

lib/stac-api/runtime/src/app.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
FastAPI application using PGStac.
3+
"""
4+
5+
from fastapi.middleware.cors import CORSMiddleware
6+
from fastapi.responses import ORJSONResponse
7+
from stac_fastapi.api.app import StacApi
8+
from stac_fastapi.pgstac.core import CoreCrudClient
9+
from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
10+
from starlette_cramjam.middleware import CompressionMiddleware
11+
12+
from .config import (
13+
ApiSettings,
14+
extensions as PgStacExtensions,
15+
get_request_model as GETModel,
16+
post_request_model as POSTModel,
17+
)
18+
19+
api_settings = ApiSettings()
20+
21+
api = StacApi(
22+
title=api_settings.name,
23+
api_version=api_settings.version,
24+
description=api_settings.description or api_settings.name,
25+
settings=api_settings.load_postgres_settings(),
26+
extensions=PgStacExtensions,
27+
client=CoreCrudClient(post_request_model=POSTModel),
28+
search_get_request_model=GETModel,
29+
search_post_request_model=POSTModel,
30+
response_class=ORJSONResponse,
31+
middlewares=[CompressionMiddleware],
32+
)
33+
34+
app = api.app
35+
36+
# Set all CORS enabled origins
37+
if api_settings.cors_origins:
38+
app.add_middleware(
39+
CORSMiddleware,
40+
allow_origins=api_settings.cors_origins,
41+
allow_credentials=True,
42+
allow_methods=["GET", "POST", "OPTIONS"],
43+
allow_headers=["*"],
44+
)
45+
46+
47+
@app.on_event("startup")
48+
async def startup_event():
49+
"""Connect to database on startup."""
50+
print("Setting up DB connection...")
51+
await connect_to_db(app)
52+
print("DB connection setup.")
53+
54+
55+
@app.on_event("shutdown")
56+
async def shutdown_event():
57+
"""Close database connection."""
58+
print("Closing up DB connection...")
59+
await close_db_connection(app)
60+
print("DB connection closed.")

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""API settings.
2+
Based on https://github.com/developmentseed/eoAPI/tree/master/src/eoapi/stac"""
3+
import base64
4+
import json
5+
from typing import Optional
6+
7+
import boto3
8+
import pydantic
9+
10+
from stac_fastapi.api.models import create_get_request_model, create_post_request_model
11+
12+
# from stac_fastapi.pgstac.extensions import QueryExtension
13+
from stac_fastapi.extensions.core import (
14+
ContextExtension,
15+
FieldsExtension,
16+
FilterExtension,
17+
QueryExtension,
18+
SortExtension,
19+
TokenPaginationExtension,
20+
)
21+
from stac_fastapi.pgstac.config import Settings
22+
from stac_fastapi.pgstac.types.search import PgstacSearch
23+
24+
25+
def get_secret_dict(secret_name: str):
26+
"""Retrieve secrets from AWS Secrets Manager
27+
28+
Args:
29+
secret_name (str): name of aws secrets manager secret containing database connection secrets
30+
profile_name (str, optional): optional name of aws profile for use in debugger only
31+
32+
Returns:
33+
secrets (dict): decrypted secrets in dict
34+
"""
35+
36+
# Create a Secrets Manager client
37+
session = boto3.session.Session()
38+
client = session.client(service_name="secretsmanager")
39+
40+
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
41+
42+
if "SecretString" in get_secret_value_response:
43+
return json.loads(get_secret_value_response["SecretString"])
44+
else:
45+
return json.loads(base64.b64decode(get_secret_value_response["SecretBinary"]))
46+
47+
48+
class ApiSettings(pydantic.BaseSettings):
49+
"""API settings"""
50+
51+
name: str = "asdi-stac-api"
52+
version: str = "0.1"
53+
description: Optional[str] = None
54+
cors_origins: str = "*"
55+
cachecontrol: str = "public, max-age=3600"
56+
debug: bool = False
57+
58+
pgstac_secret_arn: Optional[str]
59+
60+
@pydantic.validator("cors_origins")
61+
def parse_cors_origin(cls, v):
62+
"""Parse CORS origins."""
63+
return [origin.strip() for origin in v.split(",")]
64+
65+
def load_postgres_settings(self) -> "Settings":
66+
"""Load postgres connection params from AWS secret"""
67+
68+
if self.pgstac_secret_arn:
69+
secret = get_secret_dict(self.pgstac_secret_arn)
70+
71+
return Settings(
72+
postgres_host_reader=secret["host"],
73+
postgres_host_writer=secret["host"],
74+
postgres_dbname=secret["dbname"],
75+
postgres_user=secret["username"],
76+
postgres_pass=secret["password"],
77+
postgres_port=secret["port"],
78+
)
79+
else:
80+
return Settings()
81+
82+
class Config:
83+
"""model config"""
84+
85+
env_file = ".env"
86+
87+
88+
extensions = [
89+
FilterExtension(),
90+
QueryExtension(),
91+
SortExtension(),
92+
FieldsExtension(),
93+
TokenPaginationExtension(),
94+
ContextExtension(),
95+
]
96+
post_request_model = create_post_request_model(extensions, base_model=PgstacSearch)
97+
get_request_model = create_get_request_model(extensions)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
Handler for AWS Lambda.
3+
"""
4+
5+
from mangum import Mangum
6+
7+
from .app import app
8+
9+
handler = Mangum(app)

0 commit comments

Comments
 (0)