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.
Files changed (67) hide show
  1. package/.devcontainer/devcontainer.json +4 -0
  2. package/.github/workflows/build.yaml +73 -0
  3. package/.github/workflows/conventional-pr.yaml +26 -0
  4. package/.github/workflows/distribute.yaml +45 -0
  5. package/.github/workflows/docs.yaml +26 -0
  6. package/.github/workflows/test.yaml +13 -0
  7. package/.github/workflows/tox.yaml +24 -0
  8. package/.jsii +5058 -0
  9. package/.nvmrc +1 -0
  10. package/CHANGELOG.md +195 -0
  11. package/README.md +50 -0
  12. package/diagrams/bastion_diagram.excalidraw +1416 -0
  13. package/diagrams/bastion_diagram.png +0 -0
  14. package/diagrams/ingestor_diagram.excalidraw +2274 -0
  15. package/diagrams/ingestor_diagram.png +0 -0
  16. package/lib/bastion-host/index.d.ts +117 -0
  17. package/lib/bastion-host/index.js +162 -0
  18. package/lib/bootstrapper/index.d.ts +57 -0
  19. package/lib/bootstrapper/index.js +73 -0
  20. package/lib/bootstrapper/runtime/Dockerfile +18 -0
  21. package/lib/bootstrapper/runtime/handler.py +235 -0
  22. package/lib/database/index.d.ts +60 -0
  23. package/lib/database/index.js +84 -0
  24. package/lib/database/instance-memory.json +525 -0
  25. package/lib/index.d.ts +6 -0
  26. package/lib/index.js +19 -0
  27. package/lib/ingestor-api/index.d.ts +54 -0
  28. package/lib/ingestor-api/index.js +147 -0
  29. package/lib/ingestor-api/runtime/dev_requirements.txt +2 -0
  30. package/lib/ingestor-api/runtime/requirements.txt +12 -0
  31. package/lib/ingestor-api/runtime/src/__init__.py +0 -0
  32. package/lib/ingestor-api/runtime/src/collection.py +36 -0
  33. package/lib/ingestor-api/runtime/src/config.py +46 -0
  34. package/lib/ingestor-api/runtime/src/dependencies.py +94 -0
  35. package/lib/ingestor-api/runtime/src/handler.py +9 -0
  36. package/lib/ingestor-api/runtime/src/ingestor.py +82 -0
  37. package/lib/ingestor-api/runtime/src/loader.py +21 -0
  38. package/lib/ingestor-api/runtime/src/main.py +125 -0
  39. package/lib/ingestor-api/runtime/src/schemas.py +148 -0
  40. package/lib/ingestor-api/runtime/src/services.py +44 -0
  41. package/lib/ingestor-api/runtime/src/utils.py +52 -0
  42. package/lib/ingestor-api/runtime/src/validators.py +72 -0
  43. package/lib/ingestor-api/runtime/tests/__init__.py +0 -0
  44. package/lib/ingestor-api/runtime/tests/conftest.py +271 -0
  45. package/lib/ingestor-api/runtime/tests/test_collection.py +35 -0
  46. package/lib/ingestor-api/runtime/tests/test_collection_endpoint.py +41 -0
  47. package/lib/ingestor-api/runtime/tests/test_ingestor.py +60 -0
  48. package/lib/ingestor-api/runtime/tests/test_registration.py +198 -0
  49. package/lib/ingestor-api/runtime/tests/test_utils.py +35 -0
  50. package/lib/stac-api/index.d.ts +50 -0
  51. package/lib/stac-api/index.js +60 -0
  52. package/lib/stac-api/runtime/requirements.txt +8 -0
  53. package/lib/stac-api/runtime/src/__init__.py +0 -0
  54. package/lib/stac-api/runtime/src/app.py +58 -0
  55. package/lib/stac-api/runtime/src/config.py +96 -0
  56. package/lib/stac-api/runtime/src/handler.py +9 -0
  57. package/lib/titiler-pgstac-api/index.d.ts +33 -0
  58. package/lib/titiler-pgstac-api/index.js +67 -0
  59. package/lib/titiler-pgstac-api/runtime/Dockerfile +20 -0
  60. package/lib/titiler-pgstac-api/runtime/dev_requirements.txt +1 -0
  61. package/lib/titiler-pgstac-api/runtime/requirements.txt +3 -0
  62. package/lib/titiler-pgstac-api/runtime/src/__init__.py +3 -0
  63. package/lib/titiler-pgstac-api/runtime/src/handler.py +23 -0
  64. package/lib/titiler-pgstac-api/runtime/src/utils.py +26 -0
  65. package/package.json +81 -0
  66. package/tox.ini +52 -0
  67. 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,2 @@
1
+ httpx
2
+ moto[dynamodb, ssm]>=4.0.9
@@ -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,9 @@
1
+ """
2
+ Entrypoint for Lambda execution.
3
+ """
4
+
5
+ from mangum import Mangum
6
+
7
+ from .main import app
8
+
9
+ handler = Mangum(app, lifespan="off")
@@ -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}