eoapi-cdk 5.0.0
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.
- package/.devcontainer/devcontainer.json +4 -0
- package/.github/workflows/build.yaml +73 -0
- package/.github/workflows/conventional-pr.yaml +26 -0
- package/.github/workflows/distribute.yaml +45 -0
- package/.github/workflows/docs.yaml +26 -0
- package/.github/workflows/test.yaml +13 -0
- package/.github/workflows/tox.yaml +24 -0
- package/.jsii +5058 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +195 -0
- package/README.md +50 -0
- package/diagrams/bastion_diagram.excalidraw +1416 -0
- package/diagrams/bastion_diagram.png +0 -0
- package/diagrams/ingestor_diagram.excalidraw +2274 -0
- package/diagrams/ingestor_diagram.png +0 -0
- package/lib/bastion-host/index.d.ts +117 -0
- package/lib/bastion-host/index.js +162 -0
- package/lib/bootstrapper/index.d.ts +57 -0
- package/lib/bootstrapper/index.js +73 -0
- package/lib/bootstrapper/runtime/Dockerfile +18 -0
- package/lib/bootstrapper/runtime/handler.py +235 -0
- package/lib/database/index.d.ts +60 -0
- package/lib/database/index.js +84 -0
- package/lib/database/instance-memory.json +525 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +19 -0
- package/lib/ingestor-api/index.d.ts +54 -0
- package/lib/ingestor-api/index.js +147 -0
- package/lib/ingestor-api/runtime/dev_requirements.txt +2 -0
- package/lib/ingestor-api/runtime/requirements.txt +12 -0
- package/lib/ingestor-api/runtime/src/__init__.py +0 -0
- package/lib/ingestor-api/runtime/src/collection.py +36 -0
- package/lib/ingestor-api/runtime/src/config.py +46 -0
- package/lib/ingestor-api/runtime/src/dependencies.py +94 -0
- package/lib/ingestor-api/runtime/src/handler.py +9 -0
- package/lib/ingestor-api/runtime/src/ingestor.py +82 -0
- package/lib/ingestor-api/runtime/src/loader.py +21 -0
- package/lib/ingestor-api/runtime/src/main.py +125 -0
- package/lib/ingestor-api/runtime/src/schemas.py +148 -0
- package/lib/ingestor-api/runtime/src/services.py +44 -0
- package/lib/ingestor-api/runtime/src/utils.py +52 -0
- package/lib/ingestor-api/runtime/src/validators.py +72 -0
- package/lib/ingestor-api/runtime/tests/__init__.py +0 -0
- package/lib/ingestor-api/runtime/tests/conftest.py +271 -0
- package/lib/ingestor-api/runtime/tests/test_collection.py +35 -0
- package/lib/ingestor-api/runtime/tests/test_collection_endpoint.py +41 -0
- package/lib/ingestor-api/runtime/tests/test_ingestor.py +60 -0
- package/lib/ingestor-api/runtime/tests/test_registration.py +198 -0
- package/lib/ingestor-api/runtime/tests/test_utils.py +35 -0
- package/lib/stac-api/index.d.ts +50 -0
- package/lib/stac-api/index.js +60 -0
- package/lib/stac-api/runtime/requirements.txt +8 -0
- package/lib/stac-api/runtime/src/__init__.py +0 -0
- package/lib/stac-api/runtime/src/app.py +58 -0
- package/lib/stac-api/runtime/src/config.py +96 -0
- package/lib/stac-api/runtime/src/handler.py +9 -0
- package/lib/titiler-pgstac-api/index.d.ts +33 -0
- package/lib/titiler-pgstac-api/index.js +67 -0
- package/lib/titiler-pgstac-api/runtime/Dockerfile +20 -0
- package/lib/titiler-pgstac-api/runtime/dev_requirements.txt +1 -0
- package/lib/titiler-pgstac-api/runtime/requirements.txt +3 -0
- package/lib/titiler-pgstac-api/runtime/src/__init__.py +3 -0
- package/lib/titiler-pgstac-api/runtime/src/handler.py +23 -0
- package/lib/titiler-pgstac-api/runtime/src/utils.py +26 -0
- package/package.json +81 -0
- package/tox.ini +52 -0
- package/tsconfig.tsbuildinfo +18116 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var _a;
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.StacIngestor = void 0;
|
|
5
|
+
const JSII_RTTI_SYMBOL_1 = Symbol.for("jsii.rtti");
|
|
6
|
+
const aws_cdk_lib_1 = require("aws-cdk-lib");
|
|
7
|
+
const aws_lambda_python_alpha_1 = require("@aws-cdk/aws-lambda-python-alpha");
|
|
8
|
+
const constructs_1 = require("constructs");
|
|
9
|
+
class StacIngestor extends constructs_1.Construct {
|
|
10
|
+
constructor(scope, id, props) {
|
|
11
|
+
super(scope, id);
|
|
12
|
+
this.table = this.buildTable();
|
|
13
|
+
const env = {
|
|
14
|
+
DYNAMODB_TABLE: this.table.tableName,
|
|
15
|
+
ROOT_PATH: `/${props.stage}`,
|
|
16
|
+
NO_PYDANTIC_SSM_SETTINGS: "1",
|
|
17
|
+
STAC_URL: props.stacUrl,
|
|
18
|
+
DATA_ACCESS_ROLE: props.dataAccessRole.roleArn,
|
|
19
|
+
...props.apiEnv,
|
|
20
|
+
};
|
|
21
|
+
this.handlerRole = new aws_cdk_lib_1.aws_iam.Role(this, "execution-role", {
|
|
22
|
+
description: "Role used by STAC Ingestor. Manually defined so that we can choose a name that is supported by the data access roles trust policy",
|
|
23
|
+
assumedBy: new aws_cdk_lib_1.aws_iam.ServicePrincipal("lambda.amazonaws.com"),
|
|
24
|
+
managedPolicies: [
|
|
25
|
+
aws_cdk_lib_1.aws_iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"),
|
|
26
|
+
aws_cdk_lib_1.aws_iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole"),
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
const handler = this.buildApiLambda({
|
|
30
|
+
table: this.table,
|
|
31
|
+
env,
|
|
32
|
+
dataAccessRole: props.dataAccessRole,
|
|
33
|
+
stage: props.stage,
|
|
34
|
+
dbSecret: props.stacDbSecret,
|
|
35
|
+
dbVpc: props.vpc,
|
|
36
|
+
dbSecurityGroup: props.stacDbSecurityGroup,
|
|
37
|
+
subnetSelection: props.subnetSelection,
|
|
38
|
+
});
|
|
39
|
+
this.buildApiEndpoint({
|
|
40
|
+
handler,
|
|
41
|
+
stage: props.stage,
|
|
42
|
+
endpointConfiguration: props.apiEndpointConfiguration,
|
|
43
|
+
policy: props.apiPolicy,
|
|
44
|
+
});
|
|
45
|
+
this.buildIngestor({
|
|
46
|
+
table: this.table,
|
|
47
|
+
env: env,
|
|
48
|
+
dbSecret: props.stacDbSecret,
|
|
49
|
+
dbVpc: props.vpc,
|
|
50
|
+
dbSecurityGroup: props.stacDbSecurityGroup,
|
|
51
|
+
subnetSelection: props.subnetSelection,
|
|
52
|
+
});
|
|
53
|
+
this.registerSsmParameter({
|
|
54
|
+
name: "dynamodb_table",
|
|
55
|
+
value: this.table.tableName,
|
|
56
|
+
description: "Name of table used to store ingestions",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
buildTable() {
|
|
60
|
+
const table = new aws_cdk_lib_1.aws_dynamodb.Table(this, "ingestions-table", {
|
|
61
|
+
partitionKey: { name: "created_by", type: aws_cdk_lib_1.aws_dynamodb.AttributeType.STRING },
|
|
62
|
+
sortKey: { name: "id", type: aws_cdk_lib_1.aws_dynamodb.AttributeType.STRING },
|
|
63
|
+
billingMode: aws_cdk_lib_1.aws_dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
64
|
+
removalPolicy: aws_cdk_lib_1.RemovalPolicy.DESTROY,
|
|
65
|
+
stream: aws_cdk_lib_1.aws_dynamodb.StreamViewType.NEW_IMAGE,
|
|
66
|
+
});
|
|
67
|
+
table.addGlobalSecondaryIndex({
|
|
68
|
+
indexName: "status",
|
|
69
|
+
partitionKey: { name: "status", type: aws_cdk_lib_1.aws_dynamodb.AttributeType.STRING },
|
|
70
|
+
sortKey: { name: "created_at", type: aws_cdk_lib_1.aws_dynamodb.AttributeType.STRING },
|
|
71
|
+
});
|
|
72
|
+
return table;
|
|
73
|
+
}
|
|
74
|
+
buildApiLambda(props) {
|
|
75
|
+
const handler = new aws_lambda_python_alpha_1.PythonFunction(this, "api-handler", {
|
|
76
|
+
entry: `${__dirname}/runtime`,
|
|
77
|
+
index: "src/handler.py",
|
|
78
|
+
runtime: aws_cdk_lib_1.aws_lambda.Runtime.PYTHON_3_9,
|
|
79
|
+
timeout: aws_cdk_lib_1.Duration.seconds(30),
|
|
80
|
+
environment: { DB_SECRET_ARN: props.dbSecret.secretArn, ...props.env },
|
|
81
|
+
vpc: props.dbVpc,
|
|
82
|
+
vpcSubnets: props.subnetSelection,
|
|
83
|
+
allowPublicSubnet: true,
|
|
84
|
+
role: this.handlerRole,
|
|
85
|
+
memorySize: 2048,
|
|
86
|
+
});
|
|
87
|
+
// Allow handler to read DB secret
|
|
88
|
+
props.dbSecret.grantRead(handler);
|
|
89
|
+
// Allow handler to connect to DB
|
|
90
|
+
props.dbSecurityGroup.addIngressRule(handler.connections.securityGroups[0], aws_cdk_lib_1.aws_ec2.Port.tcp(5432), "Allow connections from STAC Ingestor");
|
|
91
|
+
props.table.grantReadWriteData(handler);
|
|
92
|
+
return handler;
|
|
93
|
+
}
|
|
94
|
+
buildIngestor(props) {
|
|
95
|
+
const handler = new aws_lambda_python_alpha_1.PythonFunction(this, "stac-ingestor", {
|
|
96
|
+
entry: `${__dirname}/runtime`,
|
|
97
|
+
index: "src/ingestor.py",
|
|
98
|
+
runtime: aws_cdk_lib_1.aws_lambda.Runtime.PYTHON_3_9,
|
|
99
|
+
timeout: aws_cdk_lib_1.Duration.seconds(180),
|
|
100
|
+
environment: { DB_SECRET_ARN: props.dbSecret.secretArn, ...props.env },
|
|
101
|
+
vpc: props.dbVpc,
|
|
102
|
+
vpcSubnets: props.subnetSelection,
|
|
103
|
+
allowPublicSubnet: true,
|
|
104
|
+
memorySize: 2048,
|
|
105
|
+
});
|
|
106
|
+
// Allow handler to read DB secret
|
|
107
|
+
props.dbSecret.grantRead(handler);
|
|
108
|
+
// Allow handler to connect to DB
|
|
109
|
+
props.dbSecurityGroup.addIngressRule(handler.connections.securityGroups[0], aws_cdk_lib_1.aws_ec2.Port.tcp(5432), "Allow connections from STAC Ingestor");
|
|
110
|
+
// Allow handler to write results back to DBƒ
|
|
111
|
+
props.table.grantWriteData(handler);
|
|
112
|
+
// Trigger handler from writes to DynamoDB table
|
|
113
|
+
handler.addEventSource(new aws_cdk_lib_1.aws_lambda_event_sources.DynamoEventSource(props.table, {
|
|
114
|
+
// Read when batches reach size...
|
|
115
|
+
batchSize: 1000,
|
|
116
|
+
// ... or when window is reached.
|
|
117
|
+
maxBatchingWindow: aws_cdk_lib_1.Duration.seconds(10),
|
|
118
|
+
// Read oldest data first.
|
|
119
|
+
startingPosition: aws_cdk_lib_1.aws_lambda.StartingPosition.TRIM_HORIZON,
|
|
120
|
+
retryAttempts: 1,
|
|
121
|
+
}));
|
|
122
|
+
return handler;
|
|
123
|
+
}
|
|
124
|
+
buildApiEndpoint(props) {
|
|
125
|
+
return new aws_cdk_lib_1.aws_apigateway.LambdaRestApi(this, `${aws_cdk_lib_1.Stack.of(this).stackName}-ingestor-api`, {
|
|
126
|
+
handler: props.handler,
|
|
127
|
+
proxy: true,
|
|
128
|
+
cloudWatchRole: true,
|
|
129
|
+
deployOptions: { stageName: props.stage },
|
|
130
|
+
endpointExportName: `${aws_cdk_lib_1.Stack.of(this)}-ingestor-api`,
|
|
131
|
+
endpointConfiguration: props.endpointConfiguration,
|
|
132
|
+
policy: props.policy,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
registerSsmParameter(props) {
|
|
136
|
+
const parameterNamespace = aws_cdk_lib_1.Stack.of(this).stackName;
|
|
137
|
+
return new aws_cdk_lib_1.aws_ssm.StringParameter(this, `${props.name.replace("_", "-")}-parameter`, {
|
|
138
|
+
description: props.description,
|
|
139
|
+
parameterName: `/${parameterNamespace}/${props.name}`,
|
|
140
|
+
stringValue: props.value,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.StacIngestor = StacIngestor;
|
|
145
|
+
_a = JSII_RTTI_SYMBOL_1;
|
|
146
|
+
StacIngestor[_a] = { fqn: "eoapi-cdk.StacIngestor", version: "5.0.0" };
|
|
147
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUFBLDZDQVlxQjtBQUNyQiw4RUFBa0U7QUFDbEUsMkNBQXVDO0FBRXZDLE1BQWEsWUFBYSxTQUFRLHNCQUFTO0lBSXpDLFlBQVksS0FBZ0IsRUFBRSxFQUFVLEVBQUUsS0FBd0I7UUFDaEUsS0FBSyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztRQUVqQixJQUFJLENBQUMsS0FBSyxHQUFHLElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQztRQUUvQixNQUFNLEdBQUcsR0FBMkI7WUFDbEMsY0FBYyxFQUFFLElBQUksQ0FBQyxLQUFLLENBQUMsU0FBUztZQUNwQyxTQUFTLEVBQUUsSUFBSSxLQUFLLENBQUMsS0FBSyxFQUFFO1lBQzVCLHdCQUF3QixFQUFFLEdBQUc7WUFDN0IsUUFBUSxFQUFFLEtBQUssQ0FBQyxPQUFPO1lBQ3ZCLGdCQUFnQixFQUFFLEtBQUssQ0FBQyxjQUFjLENBQUMsT0FBTztZQUM5QyxHQUFHLEtBQUssQ0FBQyxNQUFNO1NBQ2hCLENBQUM7UUFFRixJQUFJLENBQUMsV0FBVyxHQUFHLElBQUkscUJBQUcsQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFFLGdCQUFnQixFQUFFO1lBQ3RELFdBQVcsRUFDVCxtSUFBbUk7WUFDckksU0FBUyxFQUFFLElBQUkscUJBQUcsQ0FBQyxnQkFBZ0IsQ0FBQyxzQkFBc0IsQ0FBQztZQUMzRCxlQUFlLEVBQUU7Z0JBQ2YscUJBQUcsQ0FBQyxhQUFhLENBQUMsd0JBQXdCLENBQ3hDLDBDQUEwQyxDQUMzQztnQkFDRCxxQkFBRyxDQUFDLGFBQWEsQ0FBQyx3QkFBd0IsQ0FDeEMsOENBQThDLENBQy9DO2FBQ0Y7U0FDRixDQUFDLENBQUM7UUFFSCxNQUFNLE9BQU8sR0FBRyxJQUFJLENBQUMsY0FBYyxDQUFDO1lBQ2xDLEtBQUssRUFBRSxJQUFJLENBQUMsS0FBSztZQUNq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QUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxTQUFTLENBQUM7UUFDcEQsT0FBTyxJQUFJLHFCQUFHLENBQUMsZUFBZSxDQUM1QixJQUFJLEVBQ0osR0FBRyxLQUFLLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxHQUFHLEVBQUUsR0FBRyxDQUFDLFlBQVksRUFDM0M7WUFDRSxXQUFXLEVBQUUsS0FBSyxDQUFDLFdBQVc7WUFDOUIsYUFBYSxFQUFFLElBQUksa0JBQWtCLElBQUksS0FBSyxDQUFDLElBQUksRUFBRTtZQUNyRCxXQUFXLEVBQUUsS0FBSyxDQUFDLEtBQUs7U0FDekIsQ0FDRixDQUFDO0lBQ0osQ0FBQzs7QUFsTkgsb0NBbU5DIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHtcbiAgYXdzX2FwaWdhdGV3YXkgYXMgYXBpZ2F0ZXdheSxcbiAgYXdzX2R5bmFtb2RiIGFzIGR5bmFtb2RiLFxuICBhd3NfZWMyIGFzIGVjMixcbiAgYXdzX2lhbSBhcyBpYW0sXG4gIGF3c19sYW1iZGEgYXMgbGFtYmRhLFxuICBhd3NfbGFtYmRhX2V2ZW50X3NvdXJjZXMgYXMgZXZlbnRzLFxuICBhd3Nfc2VjcmV0c21hbmFnZXIgYXMgc2VjcmV0c21hbmFnZXIsXG4gIGF3c19zc20gYXMgc3NtLFxuICBEdXJhdGlvbixcbiAgUmVtb3ZhbFBvbGljeSxcbiAgU3RhY2ssXG59IGZyb20gXCJhd3MtY2RrLWxpYlwiO1xuaW1wb3J0IHsgUHl0aG9uRnVuY3Rpb24gfSBmcm9tIFwiQGF3cy1jZGsvYXdzLWxhbWJkYS1weXRob24tYWxwaGFcIjtcbmltcG9ydCB7IENvbnN0cnVjdCB9IGZyb20gXCJjb25zdHJ1Y3RzXCI7XG5cbmV4cG9ydCBjbGFzcyBTdGFjSW5nZXN0b3IgZXh0ZW5kcyBDb25zdHJ1Y3Qge1xuICB0YWJsZTogZHluYW1vZGIuVGFibGU7XG4gIHB1YmxpYyBoYW5kbGVyUm9sZTogaWFtLlJvbGU7XG5cbiAgY29uc3RydWN0b3Ioc2NvcGU6IENvbnN0cnVjdCwgaWQ6IHN0cmluZywgcHJvcHM6IFN0YWNJbmdlc3RvclByb3BzKSB7XG4gICAgc3VwZXIoc2NvcGUsIGlkKTtcblxuICAgIHRoaXMudGFibGUgPSB0aGlzLmJ1aWxkVGFibGUoKTtcblxuICAgIGNvbnN0IGVudjogUmVjb3JkPHN0cmluZywgc3RyaW5nPiA9IHtcbiAgICAgIERZTkFNT0RCX1RBQkxFOiB0aGlzLnRhYmxlLnRhYmxlTmFtZSxcbiAgICAgIFJPT1RfUEFUSDogYC8ke3Byb3BzLnN0YWdlfWAsXG4gICAgICBOT19QWURBTlRJQ19TU01fU0VUVElOR1M6IFwiMVwiLFxuICAgICAgU1RBQ19VUkw6IHByb3BzLnN0YWNVcmwsXG4gICAgICBEQVRBX0FDQ0VTU19ST0xFOiBwcm9wcy5kYXRhQWNjZXNzUm9sZS5yb2xlQXJuLFxuICAgICAgLi4ucHJvcHMuYXBpRW52LFxuICAgIH07XG5cbiAgICB0aGlzLmhhbmRsZXJSb2xlID0gbmV3IGlhbS5Sb2xlKHRoaXMsIFwiZXhlY3V0aW9uLXJvbGVcIiwge1xuICAgICAgZGVzY3JpcHRpb246XG4gICAgICAgIFwiUm9sZSB1c2VkIGJ5IFNUQUMgSW5nZXN0b3IuIE1hbnVhbGx5IGRlZmluZWQgc28gdGhhdCB3ZSBjYW4gY2hvb3NlIGEgbmFtZSB0aGF0IGlzIHN1cHBvcnRlZCBieSB0aGUgZGF0YSBhY2Nlc3Mgcm9sZXMgdHJ1c3QgcG9saWN5XCIsXG4gICAgICBhc3N1bWVkQnk6IG5ldyBpYW0uU2VydmljZVByaW5jaXBhbChcImxhbWJkYS5hbWF6b25hd3MuY29tXCIpLFxuICAgICAgbWFuYWdlZFBvbGljaWVzOiBbXG4gICAgICAgIGlhbS5NYW5hZ2VkUG9saWN5LmZyb21Bd3NNYW5hZ2VkUG9saWN5TmFtZShcbiAgICAgICAgICBcInNlcnZpY2Utcm9sZS9BV1NMYW1iZGFCYXNpY0V4ZWN1dGlvblJvbGVcIixcbiAgICAgICAgKSxcbiAgICAgICAgaWFtLk1hbmFnZWRQb2xpY3kuZnJvbUF3c01hbmFnZWRQb2xpY3lOYW1lKFxuICAgICAgICAgIFwic2VydmljZS1yb2xlL0FXU0xhbWJkYVZQQ0FjY2Vzc0V4ZWN1dGlvblJvbGVcIixcbiAgICAgICAgKSxcbiAgICAgIF0sXG4gICAgfSk7XG4gICAgXG4gICAgY29uc3QgaGFuZGxlciA9IHRoaXMuYnVpbGRBcGlMYW1iZGEoe1xuICAgICAgdGFibGU6IHRoaXMudGFibGUsXG4gICAgICBlbnYsXG4gICAgICBkYXRhQWNjZXNzUm9sZTogcHJvcHMuZGF0YUFjY2Vzc1JvbGUsXG4gICAgICBzdGFnZTogcHJvcHMuc3RhZ2UsXG4gICAgICBkYlNlY3JldDogcHJvcHMuc3RhY0RiU2VjcmV0LFxuICAgICAgZGJWcGM6IHByb3BzLnZwYyxcbiAgICAgIGRiU2VjdXJpdHlHcm91cDogcHJvcHMuc3RhY0RiU2VjdXJpdHlHcm91cCxcbiAgICAgIHN1Ym5ldFNlbGVjdGlvbjogcHJvcHMuc3VibmV0U2VsZWN0aW9uLFxuICAgIH0pO1xuXG4gICAgdGhpcy5idWlsZEFwaUVuZHBvaW50KHtcbiAgICAgIGhhbmRsZXIsXG4gICAgICBzdGFnZTogcHJvcHMuc3RhZ2UsXG4gICAgICBlbmRwb2ludENvbmZpZ3VyYXRpb246IHByb3BzLmFwaUVuZHBvaW50Q29uZmlndXJhdGlvbixcbiAgICAgIHBvbGljeTogcHJvcHMuYXBpUG9saWN5LFxuICAgIH0pO1xuXG4gICAgdGhpcy5idWlsZEluZ2VzdG9yKHtcbiAgICAgIHRhYmxlOiB0aGlzLnRhYmxlLFxuICAgICAgZW52OiBlbnYsXG4gICAgICBkYlNlY3JldDogcHJvcHMuc3RhY0RiU2VjcmV0LFxuICAgICAgZGJWcGM6IHByb3BzLnZwYyxcbiAgICAgIGRiU2VjdXJpdHlHcm91cDogcHJvcHMuc3RhY0RiU2VjdXJpdHlHcm91cCxcbiAgICAgIHN1Ym5ldFNlbGVjdGlvbjogcHJvcHMuc3VibmV0U2VsZWN0aW9uLFxuICAgIH0pO1xuXG4gICAgdGhpcy5yZWdpc3RlclNzbVBhcmFtZXRlcih7XG4gICAgICBuYW1lOiBcImR5bmFtb2RiX3RhYmxlXCIsXG4gICAgICB2YWx1ZTogdGhpcy50YWJsZS50YWJsZU5hbWUsXG4gICAgICBkZXNjcmlwdGlvbjogXCJOYW1lIG9mIHRhYmxlIHVzZWQgdG8gc3RvcmUgaW5nZXN0aW9uc1wiLFxuICAgIH0pO1xuICB9XG5cbiAgcHJpdmF0ZSBidWlsZFRhYmxlKCk6IGR5bmFtb2RiLlRhYmxlIHtcbiAgICBjb25zdCB0YWJsZSA9IG5ldyBkeW5hbW9kYi5UYWJsZSh0aGlzLCBcImluZ2VzdGlvbnMtdGFibGVcIiwge1xuICAgICAgcGFydGl0aW9uS2V5OiB7IG5hbWU6IFwiY3JlYXRlZF9ieVwiLCB0eXBlOiBkeW5hbW9kYi5BdHRyaWJ1dGVUeXBlLlNUUklORyB9LFxuICAgICAgc29ydEtleTogeyBuYW1lOiBcImlkXCIsIHR5cGU6IGR5bmFtb2RiLkF0dHJpYnV0ZVR5cGUuU1RSSU5HIH0sXG4gICAgICBiaWxsaW5nTW9kZTogZHluYW1vZGIuQmlsbGluZ01vZGUuUEFZX1BFUl9SRVFVRVNULFxuICAgICAgcmVtb3ZhbFBvbGljeTogUmVtb3ZhbFBvbGljeS5ERVNUUk9ZLFxuICAgICAgc3RyZWFtOiBkeW5hbW9kYi5TdHJlYW1WaWV3VHlwZS5ORVdfSU1BR0UsXG4gICAgfSk7XG5cbiAgICB0YWJsZS5hZGRHbG9iYWxTZWNvbmRhcnlJbmRleCh7XG4gICAgICBpbmRleE5hbWU6IFwic3RhdHVzXCIsXG4gICAgICBwYXJ0aXRpb25LZXk6IHsgbmFtZTogXCJzdGF0dXNcIiwgdHlwZTogZHluYW1vZGIuQXR0cmlidXRlVHlwZS5TVFJJTkcgfSxcbiAgICAgIHNvcnRLZXk6IHsgbmFtZTogXCJjcmVhdGVkX2F0XCIsIHR5cGU6IGR5bmFtb2RiLkF0dHJpYnV0ZVR5cGUuU1RSSU5HIH0sXG4gICAgfSk7XG5cbiAgICByZXR1cm4gdGFibGU7XG4gIH1cblxuICBwcml2YXRlIGJ1aWxkQXBpTGFtYmRhKHByb3BzOiB7XG4gICAgdGFibGU6IGR5bmFtb2RiLklUYWJsZTtcbiAgICBlbnY6IFJlY29yZDxzdHJpbmcsIHN0cmluZz47XG4gICAgZGF0YUFjY2Vzc1JvbGU6IGlhbS5JUm9sZTtcbiAgICBzdGFnZTogc3RyaW5nO1xuICAgIGRiU2VjcmV0OiBzZWNyZXRzbWFuYWdlci5JU2VjcmV0O1xuICAgIGRiVnBjOiBlYzIuSVZwYztcbiAgICBkYlNlY3VyaXR5R3JvdXA6IGVjMi5JU2VjdXJpdHlHcm91cDtcbiAgICBzdWJuZXRTZWxlY3Rpb246IGVjMi5TdWJuZXRTZWxlY3Rpb25cbiAgfSk6IFB5dGhvbkZ1bmN0aW9uIHtcbiAgICBcbiAgICBjb25zdCBoYW5kbGVyID0gbmV3IFB5dGhvbkZ1bmN0aW9uKHRoaXMsIFwiYXBpLWhhbmRsZXJcIiwge1xuICAgICAgZW50cnk6IGAke19fZGlybmFtZX0vcnVudGltZWAsXG4gICAgICBpbmRleDogXCJzcmMvaGFuZGxlci5weVwiLFxuICAgICAgcnVudGltZTogbGFtYmRhLlJ1bnRpbWUuUFlUSE9OXzNfOSxcbiAgICAgIHRpbWVvdXQ6IER1cmF0aW9uLnNlY29uZHMoMzApLFxuICAgICAgZW52aXJvbm1lbnQ6IHsgREJfU0VDUkVUX0FSTjogcHJvcHMuZGJTZWNyZXQuc2VjcmV0QXJuLCAuLi5wcm9wcy5lbnYgfSxcbiAgICAgIHZwYzogcHJvcHMuZGJWcGMsXG4gICAgICB2cGNTdWJuZXRzOiBwcm9wcy5zdWJuZXRTZWxlY3Rpb24sXG4gICAgICBhbGxvd1B1YmxpY1N1Ym5ldDogdHJ1ZSxcbiAgICAgIHJvbGU6IHRoaXMuaGFuZGxlclJvbGUsXG4gICAgICBtZW1vcnlTaXplOiAyMDQ4LFxuICAgIH0pO1xuXG4gICAgLy8gQWxsb3cgaGFuZGxlciB0byByZWFkIERCIHNlY3JldFxuICAgIHByb3BzLmRiU2VjcmV0LmdyYW50UmVhZChoYW5kbGVyKTtcblxuICAgIC8vIEFsbG93IGhhbmRsZXIgdG8gY29ubmVjdCB0byBEQlxuICAgIHByb3BzLmRiU2VjdXJpdHlHcm91cC5hZGRJbmdyZXNzUnVsZShcbiAgICAgIGhhbmRsZXIuY29ubmVjdGlvbnMuc2VjdXJpdHlHcm91cHNbMF0sXG4gICAgICBlYzIuUG9ydC50Y3AoNTQzMiksXG4gICAgICBcIkFsbG93IGNvbm5lY3Rpb25zIGZyb20gU1RBQyBJbmdlc3RvclwiXG4gICAgKTtcblxuICAgIHByb3BzLnRhYmxlLmdyYW50UmVhZFdyaXRlRGF0YShoYW5kbGVyKTtcblxuICAgIHJldHVybiBoYW5kbGVyO1xuICB9XG5cbiAgcHJpdmF0ZSBidWlsZEluZ2VzdG9yKHByb3BzOiB7XG4gICAgdGFibGU6IGR5bmFtb2RiLklUYWJsZTtcbiAgICBlbnY6IFJlY29yZDxzdHJpbmcsIHN0cmluZz47XG4gICAgZGJTZWNyZXQ6IHNlY3JldHNtYW5hZ2VyLklTZWNyZXQ7XG4gICAgZGJWcGM6IGVjMi5JVnBjO1xuICAgIGRiU2VjdXJpdHlHcm91cDogZWMyLklTZWN1cml0eUdyb3VwO1xuICAgIHN1Ym5ldFNlbGVjdGlvbjogZWMyLlN1Ym5ldFNlbGVjdGlvbjtcbiAgfSk6IFB5dGhvbkZ1bmN0aW9uIHtcbiAgICBjb25zdCBoYW5kbGVyID0gbmV3IFB5dGhvbkZ1bmN0aW9uKHRoaXMsIFwic3RhYy1pbmdlc3RvclwiLCB7XG4gICAgICBlbnRyeTogYCR7X19kaXJuYW1lfS9ydW50aW1lYCxcbiAgICAgIGluZGV4OiBcInNyYy9pbmdlc3Rvci5weVwiLFxuICAgICAgcnVudGltZTogbGFtYmRhLlJ1bnRpbWUuUFlUSE9OXzNfOSxcbiAgICAgIHRpbWVvdXQ6IER1cmF0aW9uLnNlY29uZHMoMTgwKSxcbiAgICAgIGVudmlyb25tZW50OiB7IERCX1NFQ1JFVF9BUk46IHByb3BzLmRiU2VjcmV0LnNlY3JldEFybiwgLi4ucHJvcHMuZW52IH0sXG4gICAgICB2cGM6IHByb3BzLmRiVnBjLFxuICAgICAgdnBjU3VibmV0czogcHJvcHMuc3VibmV0U2VsZWN0aW9uLFxuICAgICAgYWxsb3dQdWJsaWNTdWJuZXQ6IHRydWUsXG4gICAgICBtZW1vcnlTaXplOiAyMDQ4LFxuICAgIH0pO1xuXG4gICAgLy8gQWxsb3cgaGFuZGxlciB0byByZWFkIERCIHNlY3JldFxuICAgIHByb3BzLmRiU2VjcmV0LmdyYW50UmVhZChoYW5kbGVyKTtcblxuICAgIC8vIEFsbG93IGhhbmRsZXIgdG8gY29ubmVjdCB0byBEQlxuICAgIHByb3BzLmRiU2VjdXJpdHlHcm91cC5hZGRJbmdyZXNzUnVsZShcbiAgICAgIGhhbmRsZXIuY29ubmVjdGlvbnMuc2VjdXJpdHlHcm91cHNbMF0sXG4gICAgICBlYzIuUG9ydC50Y3AoNTQzMiksXG4gICAgICBcIkFsbG93IGNvbm5lY3Rpb25zIGZyb20gU1RBQyBJbmdlc3RvclwiXG4gICAgKTtcblxuICAgIC8vIEFsbG93IGhhbmRsZXIgdG8gd3JpdGUgcmVzdWx0cyBiYWNrIHRvIERCxpJcbiAgICBwcm9wcy50YWJsZS5ncmFudFdyaXRlRGF0YShoYW5kbGVyKTtcblxuICAgIC8vIFRyaWdnZXIgaGFuZGxlciBmcm9tIHdyaXRlcyB0byBEeW5hbW9EQiB0YWJsZVxuICAgIGhhbmRsZXIuYWRkRXZlbnRTb3VyY2UoXG4gICAgICBuZXcgZXZlbnRzLkR5bmFtb0V2ZW50U291cmNlKHByb3BzLnRhYmxlLCB7XG4gICAgICAgIC8vIFJlYWQgd2hlbiBiYXRjaGVzIHJlYWNoIHNpemUuLi5cbiAgICAgICAgYmF0Y2hTaXplOiAxMDAwLFxuICAgICAgICAvLyAuLi4gb3Igd2hlbiB3aW5kb3cgaXMgcmVhY2hlZC5cbiAgICAgICAgbWF4QmF0Y2hpbmdXaW5kb3c6IER1cmF0aW9uLnNlY29uZHMoMTApLFxuICAgICAgICAvLyBSZWFkIG9sZGVzdCBkYXRhIGZpcnN0LlxuICAgICAgICBzdGFydGluZ1Bvc2l0aW9uOiBsYW1iZGEuU3RhcnRpbmdQb3NpdGlvbi5UUklNX0hPUklaT04sXG4gICAgICAgIHJldHJ5QXR0ZW1wdHM6IDEsXG4gICAgICB9KVxuICAgICk7XG5cbiAgICByZXR1cm4gaGFuZGxlcjtcbiAgfVxuXG4gIHByaXZhdGUgYnVpbGRBcGlFbmRwb2ludChwcm9wczoge1xuICAgIGhhbmRsZXI6IGxhbWJkYS5JRnVuY3Rpb247XG4gICAgc3RhZ2U6IHN0cmluZztcbiAgICBwb2xpY3k/OiBpYW0uUG9saWN5RG9jdW1lbnQ7XG4gICAgZW5kcG9pbnRDb25maWd1cmF0aW9uPzogYXBpZ2F0ZXdheS5FbmRwb2ludENvbmZpZ3VyYXRpb247XG4gIH0pOiBhcGlnYXRld2F5LkxhbWJkYVJlc3RBcGkge1xuICAgIHJldHVybiBuZXcgYXBpZ2F0ZXdheS5MYW1iZGFSZXN0QXBpKFxuICAgICAgdGhpcyxcbiAgICAgIGAke1N0YWNrLm9mKHRoaXMpLnN0YWNrTmFtZX0taW5nZXN0b3ItYXBpYCxcbiAgICAgIHtcbiAgICAgICAgaGFuZGxlcjogcHJvcHMuaGFuZGxlcixcbiAgICAgICAgcHJveHk6IHRydWUsXG5cbiAgICAgICAgY2xvdWRXYXRjaFJvbGU6IHRydWUsXG4gICAgICAgIGRlcGxveU9wdGlvbnM6IHsgc3RhZ2VOYW1lOiBwcm9wcy5zdGFnZSB9LFxuICAgICAgICBlbmRwb2ludEV4cG9ydE5hbWU6IGAke1N0YWNrLm9mKHRoaXMpfS1pbmdlc3Rvci1hcGlgLFxuXG4gICAgICAgIGVuZHBvaW50Q29uZmlndXJhdGlvbjogcHJvcHMuZW5kcG9pbnRDb25maWd1cmF0aW9uLFxuICAgICAgICBwb2xpY3k6IHByb3BzLnBvbGljeSxcbiAgICAgIH1cbiAgICApO1xuICB9XG5cbiAgcHJpdmF0ZSByZWdpc3RlclNzbVBhcmFtZXRlcihwcm9wczoge1xuICAgIG5hbWU6IHN0cmluZztcbiAgICB2YWx1ZTogc3RyaW5nO1xuICAgIGRlc2NyaXB0aW9uOiBzdHJpbmc7XG4gIH0pOiBzc20uSVN0cmluZ1BhcmFtZXRlciB7XG4gICAgY29uc3QgcGFyYW1ldGVyTmFtZXNwYWNlID0gU3RhY2sub2YodGhpcykuc3RhY2tOYW1lO1xuICAgIHJldHVybiBuZXcgc3NtLlN0cmluZ1BhcmFtZXRlcihcbiAgICAgIHRoaXMsXG4gICAgICBgJHtwcm9wcy5uYW1lLnJlcGxhY2UoXCJfXCIsIFwiLVwiKX0tcGFyYW1ldGVyYCxcbiAgICAgIHtcbiAgICAgICAgZGVzY3JpcHRpb246IHByb3BzLmRlc2NyaXB0aW9uLFxuICAgICAgICBwYXJhbWV0ZXJOYW1lOiBgLyR7cGFyYW1ldGVyTmFtZXNwYWNlfS8ke3Byb3BzLm5hbWV9YCxcbiAgICAgICAgc3RyaW5nVmFsdWU6IHByb3BzLnZhbHVlLFxuICAgICAgfVxuICAgICk7XG4gIH1cbn1cblxuZXhwb3J0IGludGVyZmFjZSBTdGFjSW5nZXN0b3JQcm9wcyB7XG4gIC8qKlxuICAgKiBBUk4gb2YgQVdTIFJvbGUgdXNlZCB0byB2YWxpZGF0ZSBhY2Nlc3MgdG8gUzMgZGF0YVxuICAgKi9cbiAgcmVhZG9ubHkgZGF0YUFjY2Vzc1JvbGU6IGlhbS5JUm9sZTtcblxuICAvKipcbiAgICogVVJMIG9mIFNUQUMgQVBJXG4gICAqL1xuICByZWFkb25seSBzdGFjVXJsOiBzdHJpbmc7XG5cbiAgLyoqXG4gICAqIFN0YWdlIG9mIGRlcGxveW1lbnQgKGUuZy4gYGRldmAsIGBwcm9kYClcbiAgICovXG4gIHJlYWRvbmx5IHN0YWdlOiBzdHJpbmc7XG5cbiAgLyoqXG4gICAqIFNlY3JldCBjb250YWluaW5nIHBnU1RBQyBEQiBjb25uZWN0aW9uIGluZm9ybWF0aW9uXG4gICAqL1xuICByZWFkb25seSBzdGFjRGJTZWNyZXQ6IHNlY3JldHNtYW5hZ2VyLklTZWNyZXQ7XG5cbiAgLyoqXG4gICAqIFZQQyBydW5uaW5nIHBnU1RBQyBEQlxuICAgKi9cbiAgcmVhZG9ubHkgdnBjOiBlYzIuSVZwYztcblxuICAvKipcbiAgICogU2VjdXJpdHkgR3JvdXAgdXNlZCBieSBwZ1NUQUMgREJcbiAgICovXG4gIHJlYWRvbmx5IHN0YWNEYlNlY3VyaXR5R3JvdXA6IGVjMi5JU2VjdXJpdHlHcm91cDtcblxuICAvKipcbiAgICogQm9vbGVhbiBpbmRpY2F0aW5nIHdoZXRoZXIgb3Igbm90IHBnU1RBQyBEQiBpcyBpbiBhIHB1YmxpYyBzdWJuZXRcbiAgICovXG4gIHJlYWRvbmx5IHN1Ym5ldFNlbGVjdGlvbjogZWMyLlN1Ym5ldFNlbGVjdGlvbjtcblxuICAvKipcbiAgICogRW52aXJvbm1lbnQgdmFyaWFibGVzIHRvIGJlIHNlbnQgdG8gTGFtYmRhLlxuICAgKi9cbiAgcmVhZG9ubHkgYXBpRW52PzogUmVjb3JkPHN0cmluZywgc3RyaW5nPjtcblxuICAvKipcbiAgICogQVBJIEVuZHBvaW50IENvbmZpZ3VyYXRpb24sIHVzZWZ1bCBmb3IgY3JlYXRpbmcgcHJpdmF0ZSBBUElzLlxuICAgKi9cbiAgcmVhZG9ubHkgYXBpRW5kcG9pbnRDb25maWd1cmF0aW9uPzogYXBpZ2F0ZXdheS5FbmRwb2ludENvbmZpZ3VyYXRpb247XG5cbiAgLyoqXG4gICAqIEFQSSBQb2xpY3kgRG9jdW1lbnQsIHVzZWZ1bCBmb3IgY3JlYXRpbmcgcHJpdmF0ZSBBUElzLlxuICAgKi9cbiAgcmVhZG9ubHkgYXBpUG9saWN5PzogaWFtLlBvbGljeURvY3VtZW50O1xufVxuIl19
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Authlib==1.0.1
|
|
2
|
+
cachetools==5.1.0
|
|
3
|
+
fastapi>=0.75.1
|
|
4
|
+
mangum>=0.15.0
|
|
5
|
+
orjson>=3.6.8
|
|
6
|
+
psycopg[binary,pool]>=3.0.15
|
|
7
|
+
pydantic_ssm_settings>=0.2.0
|
|
8
|
+
pydantic>=1.9.0
|
|
9
|
+
pypgstac==0.6.13
|
|
10
|
+
requests>=2.27.1
|
|
11
|
+
# Waiting for https://github.com/stac-utils/stac-pydantic/pull/116
|
|
12
|
+
stac-pydantic @ git+https://github.com/alukach/stac-pydantic.git@patch-1
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from pypgstac.db import PgstacDB
|
|
4
|
+
from pypgstac.load import Methods
|
|
5
|
+
|
|
6
|
+
from .loader import Loader
|
|
7
|
+
from .schemas import StacCollection
|
|
8
|
+
from .utils import get_db_credentials
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def ingest(collection: StacCollection):
|
|
12
|
+
"""
|
|
13
|
+
Takes a collection model,
|
|
14
|
+
does necessary preprocessing,
|
|
15
|
+
and loads into the PgSTAC collection table
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
creds = get_db_credentials(os.environ["DB_SECRET_ARN"])
|
|
19
|
+
with PgstacDB(dsn=creds.dsn_string, debug=True) as db:
|
|
20
|
+
loader = Loader(db=db)
|
|
21
|
+
collection = [
|
|
22
|
+
collection.to_dict()
|
|
23
|
+
] # pypgstac wants either a string or an Iterable of dicts.
|
|
24
|
+
loader.load_collections(file=collection, insert_mode=Methods.upsert)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(f"Encountered failure loading collection into pgSTAC: {e}")
|
|
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 = Loader(db=db)
|
|
36
|
+
loader.delete_collection(collection_id)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from getpass import getuser
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pydantic import AnyHttpUrl, BaseSettings, Field, constr
|
|
6
|
+
from pydantic_ssm_settings import AwsSsmSourceConfig
|
|
7
|
+
|
|
8
|
+
AwsArn = constr(regex=r"^arn:aws:iam::\d{12}:role/.+")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Settings(BaseSettings):
|
|
12
|
+
dynamodb_table: str
|
|
13
|
+
|
|
14
|
+
root_path: Optional[str] = Field(description="Path from where to serve this URL.")
|
|
15
|
+
|
|
16
|
+
jwks_url: Optional[AnyHttpUrl] = Field(
|
|
17
|
+
description="URL of JWKS, e.g. https://cognito-idp.{region}.amazonaws.com/{userpool_id}/.well-known/jwks.json" # noqa
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
stac_url: AnyHttpUrl = Field(description="URL of STAC API")
|
|
21
|
+
|
|
22
|
+
data_access_role: AwsArn = Field(
|
|
23
|
+
description="ARN of AWS Role used to validate access to S3 data"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
requester_pays: Optional[bool] = Field(
|
|
27
|
+
description="Path from where to serve this URL.", default=False
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
class Config(AwsSsmSourceConfig):
|
|
31
|
+
env_file = ".env"
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_ssm(cls, stack: str):
|
|
35
|
+
return cls(_secrets_dir=f"/{stack}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
settings = (
|
|
39
|
+
Settings()
|
|
40
|
+
if os.environ.get("NO_PYDANTIC_SSM_SETTINGS")
|
|
41
|
+
else Settings.from_ssm(
|
|
42
|
+
stack=os.environ.get(
|
|
43
|
+
"STACK", f"veda-stac-ingestion-system-{os.environ.get('STAGE', getuser())}"
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import boto3
|
|
5
|
+
import requests
|
|
6
|
+
from authlib.jose import JsonWebKey, JsonWebToken, JWTClaims, KeySet, errors
|
|
7
|
+
from cachetools import TTLCache, cached
|
|
8
|
+
from fastapi import Depends, HTTPException, Request, security
|
|
9
|
+
|
|
10
|
+
from . import config, services
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
token_scheme = security.HTTPBearer()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_settings(request: Request) -> config.Settings:
|
|
18
|
+
return request.app.extra["settings"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_jwks_url(settings: config.Settings = Depends(get_settings)) -> str:
|
|
22
|
+
return settings.jwks_url
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@cached(TTLCache(maxsize=1, ttl=3600))
|
|
26
|
+
def get_jwks(jwks_url: str = Depends(get_jwks_url)) -> KeySet:
|
|
27
|
+
with requests.get(jwks_url) as response:
|
|
28
|
+
response.raise_for_status()
|
|
29
|
+
return JsonWebKey.import_key_set(response.json())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def decode_token(
|
|
33
|
+
token: security.HTTPAuthorizationCredentials = Depends(token_scheme),
|
|
34
|
+
jwks: KeySet = Depends(get_jwks),
|
|
35
|
+
) -> JWTClaims:
|
|
36
|
+
"""
|
|
37
|
+
Validate & decode JWT
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
claims = JsonWebToken(["RS256"]).decode(
|
|
41
|
+
s=token.credentials,
|
|
42
|
+
key=jwks,
|
|
43
|
+
claims_options={
|
|
44
|
+
# # Example of validating audience to match expected value
|
|
45
|
+
# "aud": {"essential": True, "values": [APP_CLIENT_ID]}
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if "client_id" in claims:
|
|
50
|
+
# Insert Cognito's `client_id` into `aud` claim if `aud` claim is unset
|
|
51
|
+
claims.setdefault("aud", claims["client_id"])
|
|
52
|
+
|
|
53
|
+
claims.validate()
|
|
54
|
+
return claims
|
|
55
|
+
except errors.JoseError: #
|
|
56
|
+
logger.exception("Unable to decode token")
|
|
57
|
+
raise HTTPException(status_code=403, detail="Bad auth token")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_username_from_token(
|
|
61
|
+
claims: Optional[security.HTTPBasicCredentials] = Depends(decode_token),
|
|
62
|
+
):
|
|
63
|
+
return claims["sub"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_username_from_request(provided_by: str):
|
|
67
|
+
return provided_by
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
get_username = (
|
|
71
|
+
get_username_from_token if config.settings.jwks_url else get_username_from_request
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_table(settings: config.Settings = Depends(get_settings)):
|
|
76
|
+
client = boto3.resource("dynamodb")
|
|
77
|
+
return client.Table(settings.dynamodb_table)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_db(table=Depends(get_table)) -> services.Database:
|
|
81
|
+
return services.Database(table=table)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def fetch_ingestion(
|
|
85
|
+
ingestion_id: str,
|
|
86
|
+
db: services.Database = Depends(get_db),
|
|
87
|
+
username: str = Depends(get_username),
|
|
88
|
+
):
|
|
89
|
+
try:
|
|
90
|
+
return db.fetch_one(username=username, ingestion_id=ingestion_id)
|
|
91
|
+
except services.NotInDb:
|
|
92
|
+
raise HTTPException(
|
|
93
|
+
status_code=404, detail="No ingestion found with provided ID"
|
|
94
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import TYPE_CHECKING, Iterator, List, Optional, Sequence
|
|
4
|
+
|
|
5
|
+
from boto3.dynamodb.types import TypeDeserializer
|
|
6
|
+
|
|
7
|
+
from .config import settings
|
|
8
|
+
from .dependencies import get_table
|
|
9
|
+
from .schemas import Ingestion, Status
|
|
10
|
+
from .utils import get_db_credentials, load_items
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from aws_lambda_typing import context as context_
|
|
14
|
+
from aws_lambda_typing import events
|
|
15
|
+
from aws_lambda_typing.events.dynamodb_stream import DynamodbRecord
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_queued_ingestions(records: List["DynamodbRecord"]) -> Iterator[Ingestion]:
|
|
19
|
+
deserializer = TypeDeserializer()
|
|
20
|
+
for record in records:
|
|
21
|
+
# Parse Record
|
|
22
|
+
parsed = {
|
|
23
|
+
k: deserializer.deserialize(v)
|
|
24
|
+
for k, v in record["dynamodb"]["NewImage"].items()
|
|
25
|
+
}
|
|
26
|
+
ingestion = Ingestion.parse_obj(parsed)
|
|
27
|
+
if ingestion.status == Status.queued:
|
|
28
|
+
yield ingestion
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def update_dynamodb(
|
|
32
|
+
ingestions: Sequence[Ingestion],
|
|
33
|
+
status: Status,
|
|
34
|
+
message: Optional[str] = None,
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Bulk update DynamoDB with ingestion results.
|
|
38
|
+
"""
|
|
39
|
+
# Update records in DynamoDB
|
|
40
|
+
print(f"Updating ingested items status in DynamoDB, marking as {status}...")
|
|
41
|
+
table = get_table(settings)
|
|
42
|
+
with table.batch_writer(overwrite_by_pkeys=["created_by", "id"]) as batch:
|
|
43
|
+
for ingestion in ingestions:
|
|
44
|
+
batch.put_item(
|
|
45
|
+
Item=ingestion.copy(
|
|
46
|
+
update={
|
|
47
|
+
"status": status,
|
|
48
|
+
"message": message,
|
|
49
|
+
"updated_at": datetime.now(),
|
|
50
|
+
}
|
|
51
|
+
).dynamodb_dict()
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def handler(event: "events.DynamoDBStreamEvent", context: "context_.Context"):
|
|
56
|
+
# Parse input
|
|
57
|
+
ingestions = list(get_queued_ingestions(event["Records"]))
|
|
58
|
+
if not ingestions:
|
|
59
|
+
print("No queued ingestions to process")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
# Insert into PgSTAC DB
|
|
63
|
+
outcome = Status.succeeded
|
|
64
|
+
message = None
|
|
65
|
+
try:
|
|
66
|
+
load_items(
|
|
67
|
+
creds=get_db_credentials(os.environ["DB_SECRET_ARN"]),
|
|
68
|
+
ingestions=ingestions,
|
|
69
|
+
)
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f"Encountered failure loading items into pgSTAC: {e}")
|
|
72
|
+
outcome = Status.failed
|
|
73
|
+
message = str(e)
|
|
74
|
+
|
|
75
|
+
# Update DynamoDB with outcome
|
|
76
|
+
update_dynamodb(
|
|
77
|
+
ingestions=ingestions,
|
|
78
|
+
status=outcome,
|
|
79
|
+
message=message,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
print("Completed batch...")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Utilities to bulk load data into pgstac from json/ndjson."""
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from pypgstac.load import Loader as BaseLoader
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Loader(BaseLoader):
|
|
10
|
+
"""Utilities for loading data and updating collection summaries/extents."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, db) -> None:
|
|
13
|
+
super().__init__(db)
|
|
14
|
+
self.check_version()
|
|
15
|
+
self.conn = self.db.connect()
|
|
16
|
+
|
|
17
|
+
def delete_collection(self, collection_id: str) -> None:
|
|
18
|
+
with self.conn.cursor() as cur:
|
|
19
|
+
with self.conn.transaction():
|
|
20
|
+
logger.info(f"Deleting collection: {collection_id}.")
|
|
21
|
+
cur.execute("SELECT pgstac.delete_collection(%s);", [collection_id])
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from fastapi import Depends, FastAPI, HTTPException
|
|
2
|
+
|
|
3
|
+
from . import collection as collection_loader
|
|
4
|
+
from . import config, dependencies, schemas, services
|
|
5
|
+
|
|
6
|
+
app = FastAPI(
|
|
7
|
+
root_path=config.settings.root_path,
|
|
8
|
+
settings=config.settings,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.get(
|
|
13
|
+
"/ingestions", response_model=schemas.ListIngestionResponse, tags=["Ingestion"]
|
|
14
|
+
)
|
|
15
|
+
async def list_ingestions(
|
|
16
|
+
list_request: schemas.ListIngestionRequest = Depends(),
|
|
17
|
+
db: services.Database = Depends(dependencies.get_db),
|
|
18
|
+
):
|
|
19
|
+
return db.fetch_many(
|
|
20
|
+
status=list_request.status, next=list_request.next, limit=list_request.limit
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.post(
|
|
25
|
+
"/ingestions",
|
|
26
|
+
response_model=schemas.Ingestion,
|
|
27
|
+
tags=["Ingestion"],
|
|
28
|
+
status_code=201,
|
|
29
|
+
)
|
|
30
|
+
async def create_ingestion(
|
|
31
|
+
item: schemas.AccessibleItem,
|
|
32
|
+
username: str = Depends(dependencies.get_username),
|
|
33
|
+
db: services.Database = Depends(dependencies.get_db),
|
|
34
|
+
) -> schemas.Ingestion:
|
|
35
|
+
return schemas.Ingestion(
|
|
36
|
+
id=item.id,
|
|
37
|
+
created_by=username,
|
|
38
|
+
item=item,
|
|
39
|
+
status=schemas.Status.queued,
|
|
40
|
+
).enqueue(db)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.get(
|
|
44
|
+
"/ingestions/{ingestion_id}",
|
|
45
|
+
response_model=schemas.Ingestion,
|
|
46
|
+
tags=["Ingestion"],
|
|
47
|
+
)
|
|
48
|
+
def get_ingestion(
|
|
49
|
+
ingestion: schemas.Ingestion = Depends(dependencies.fetch_ingestion),
|
|
50
|
+
) -> schemas.Ingestion:
|
|
51
|
+
return ingestion
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.patch(
|
|
55
|
+
"/ingestions/{ingestion_id}",
|
|
56
|
+
response_model=schemas.Ingestion,
|
|
57
|
+
tags=["Ingestion"],
|
|
58
|
+
)
|
|
59
|
+
def update_ingestion(
|
|
60
|
+
update: schemas.UpdateIngestionRequest,
|
|
61
|
+
ingestion: schemas.Ingestion = Depends(dependencies.fetch_ingestion),
|
|
62
|
+
db: services.Database = Depends(dependencies.get_db),
|
|
63
|
+
):
|
|
64
|
+
updated_item = ingestion.copy(update=update.dict(exclude_unset=True))
|
|
65
|
+
return updated_item.save(db)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.delete(
|
|
69
|
+
"/ingestions/{ingestion_id}",
|
|
70
|
+
response_model=schemas.Ingestion,
|
|
71
|
+
tags=["Ingestion"],
|
|
72
|
+
)
|
|
73
|
+
def cancel_ingestion(
|
|
74
|
+
ingestion: schemas.Ingestion = Depends(dependencies.fetch_ingestion),
|
|
75
|
+
db: services.Database = Depends(dependencies.get_db),
|
|
76
|
+
) -> schemas.Ingestion:
|
|
77
|
+
if ingestion.status != schemas.Status.queued:
|
|
78
|
+
raise HTTPException(
|
|
79
|
+
status_code=400,
|
|
80
|
+
detail=(
|
|
81
|
+
"Unable to delete ingestion if status is not "
|
|
82
|
+
f"{schemas.Status.queued}"
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
return ingestion.cancel(db)
|
|
86
|
+
|
|
87
|
+
|
|
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
|
+
|
|
120
|
+
@app.get("/auth/me")
|
|
121
|
+
def who_am_i(username=Depends(dependencies.get_username)):
|
|
122
|
+
"""
|
|
123
|
+
Return username for the provided request
|
|
124
|
+
"""
|
|
125
|
+
return {"username": username}
|