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,60 @@
1
+ from unittest.mock import patch
2
+
3
+ import pytest
4
+
5
+
6
+ @pytest.fixture()
7
+ def dynamodb_stream_event():
8
+ return {"Records": None}
9
+
10
+
11
+ @pytest.fixture()
12
+ def get_queued_ingestions(example_ingestion):
13
+ with patch(
14
+ "src.ingestor.get_queued_ingestions",
15
+ return_value=iter([example_ingestion]),
16
+ autospec=True,
17
+ ) as m:
18
+ yield m
19
+
20
+
21
+ @pytest.fixture()
22
+ def get_db_credentials():
23
+ with patch("src.ingestor.get_db_credentials", return_value="", autospec=True) as m:
24
+ yield m
25
+
26
+
27
+ @pytest.fixture()
28
+ def load_items():
29
+ with patch("src.ingestor.load_items", return_value=0, autospec=True) as m:
30
+ yield m
31
+
32
+
33
+ @pytest.fixture()
34
+ def get_table(mock_table):
35
+ with patch("src.ingestor.get_table", return_value=mock_table, autospec=True) as m:
36
+ yield m
37
+
38
+
39
+ def test_handler(
40
+ monkeypatch,
41
+ test_environ,
42
+ dynamodb_stream_event,
43
+ example_ingestion,
44
+ get_queued_ingestions,
45
+ get_db_credentials,
46
+ load_items,
47
+ get_table,
48
+ mock_table,
49
+ ):
50
+ import src.ingestor as ingestor
51
+
52
+ ingestor.handler(dynamodb_stream_event, {})
53
+ load_items.assert_called_once_with(
54
+ creds="",
55
+ ingestions=list([example_ingestion]),
56
+ )
57
+ response = mock_table.get_item(
58
+ Key={"created_by": example_ingestion.created_by, "id": example_ingestion.id}
59
+ )
60
+ assert response["Item"]["status"] == "succeeded"
@@ -0,0 +1,198 @@
1
+ import base64
2
+ import json
3
+ from datetime import timedelta
4
+ from typing import TYPE_CHECKING, List
5
+ from unittest.mock import call, patch
6
+
7
+ from fastapi.encoders import jsonable_encoder
8
+ import pytest
9
+
10
+
11
+ if TYPE_CHECKING:
12
+ from fastapi.testclient import TestClient
13
+ from src import schemas, services
14
+
15
+ ingestion_endpoint = "/ingestions"
16
+
17
+
18
+ @pytest.fixture()
19
+ def collection_exists():
20
+ with patch("src.validators.collection_exists", return_value=True) as m:
21
+ yield m
22
+
23
+
24
+ @pytest.fixture()
25
+ def collection_missing():
26
+ def bad_collection(collection_id: str):
27
+ raise ValueError("MOCKED MISSING COLLECTION ERROR")
28
+
29
+ with patch("src.validators.collection_exists", side_effect=bad_collection) as m:
30
+ yield m
31
+
32
+
33
+ @pytest.fixture()
34
+ def asset_exists():
35
+ with patch("src.validators.url_is_accessible", return_value=True) as m:
36
+ yield m
37
+
38
+
39
+ @pytest.fixture()
40
+ def asset_missing():
41
+ def bad_asset_url(href: str):
42
+ raise ValueError("MOCKED INACCESSIBLE URL ERROR")
43
+
44
+ with patch("src.validators.url_is_accessible", side_effect=bad_asset_url) as m:
45
+ yield m
46
+
47
+
48
+ class TestCreate:
49
+ @pytest.fixture(autouse=True)
50
+ def setup(
51
+ self,
52
+ api_client: "TestClient",
53
+ mock_table: "services.Table",
54
+ example_ingestion: "schemas.Ingestion",
55
+ ):
56
+ from src import services
57
+
58
+ self.api_client = api_client
59
+ self.mock_table = mock_table
60
+ self.db = services.Database(self.mock_table)
61
+ self.example_ingestion = example_ingestion
62
+
63
+ def test_unauthenticated_create(self):
64
+ response = self.api_client.post(
65
+ ingestion_endpoint,
66
+ json=jsonable_encoder(self.example_ingestion.item),
67
+ )
68
+
69
+ assert response.status_code == 403
70
+
71
+ def test_create(self, client_authenticated, collection_exists, asset_exists):
72
+ response = self.api_client.post(
73
+ ingestion_endpoint,
74
+ json=jsonable_encoder(self.example_ingestion.item),
75
+ )
76
+
77
+ assert response.status_code == 201
78
+ assert collection_exists.called_once_with(
79
+ self.example_ingestion.item.collection
80
+ )
81
+
82
+ stored_data = self.db.fetch_many(status="queued")["items"]
83
+ assert len(stored_data) == 1
84
+ assert json.loads(stored_data[0].json(by_alias=True)) == response.json()
85
+
86
+ def test_validates_missing_collection(
87
+ self, client_authenticated, collection_missing, asset_exists
88
+ ):
89
+ response = self.api_client.post(
90
+ ingestion_endpoint,
91
+ json=jsonable_encoder(self.example_ingestion.item),
92
+ )
93
+
94
+ collection_missing.assert_called_once_with(
95
+ collection_id=self.example_ingestion.item.collection
96
+ )
97
+ assert response.status_code == 422, "should get validation error"
98
+ assert (
99
+ len(self.db.fetch_many(status="queued")["items"]) == 0
100
+ ), "data should not be stored in DB"
101
+
102
+ def test_validates_missing_assets(
103
+ self, client_authenticated, collection_exists, asset_missing
104
+ ):
105
+ response = self.api_client.post(
106
+ ingestion_endpoint,
107
+ json=jsonable_encoder(self.example_ingestion.item),
108
+ )
109
+
110
+ collection_exists.assert_called_once_with(
111
+ collection_id=self.example_ingestion.item.collection
112
+ )
113
+ asset_missing.assert_has_calls(
114
+ [
115
+ call(href=asset.href)
116
+ for asset in self.example_ingestion.item.assets.values()
117
+ ],
118
+ any_order=True,
119
+ )
120
+ assert response.status_code == 422, "should get validation error"
121
+ for asset_type in self.example_ingestion.item.assets.keys():
122
+ assert any(
123
+ [
124
+ err["loc"] == ["body", "assets", asset_type, "href"]
125
+ for err in response.json()["detail"]
126
+ ]
127
+ ), "should reference asset type in validation error response"
128
+ assert (
129
+ len(self.db.fetch_many(status="queued")["items"]) == 0
130
+ ), "data should not be stored in DB"
131
+
132
+
133
+ class TestList:
134
+ @pytest.fixture(autouse=True)
135
+ def setup(
136
+ self,
137
+ api_client: "TestClient",
138
+ mock_table: "services.Table",
139
+ example_ingestion: "schemas.Ingestion",
140
+ ):
141
+ self.api_client = api_client
142
+ self.mock_table = mock_table
143
+ self.example_ingestion = example_ingestion
144
+
145
+ def populate_table(self, count=100) -> List["schemas.Ingestion"]:
146
+ example_ingestions = []
147
+ for i in range(count):
148
+ ingestion = self.example_ingestion.copy()
149
+ ingestion.id = str(i)
150
+ ingestion.created_at = ingestion.created_at + timedelta(hours=i)
151
+ self.mock_table.put_item(Item=ingestion.dynamodb_dict())
152
+ example_ingestions.append(ingestion)
153
+ return example_ingestions
154
+
155
+ def test_simple_lookup(self):
156
+ self.mock_table.put_item(Item=self.example_ingestion.dynamodb_dict())
157
+ ingestion = jsonable_encoder(self.example_ingestion)
158
+ response = self.api_client.get(ingestion_endpoint)
159
+ assert response.status_code == 200
160
+ assert response.json() == {
161
+ "items": [ingestion],
162
+ "next": None,
163
+ }
164
+
165
+ def test_next_response(self):
166
+ example_ingestions = self.populate_table(100)
167
+
168
+ limit = 25
169
+ expected_next = json.loads(
170
+ example_ingestions[limit - 1].json(
171
+ include={"created_by", "id", "status", "created_at"}
172
+ )
173
+ )
174
+
175
+ response = self.api_client.get(ingestion_endpoint, params={"limit": limit})
176
+ assert response.status_code == 200
177
+ assert json.loads(base64.b64decode(response.json()["next"])) == expected_next
178
+ assert response.json()["items"] == jsonable_encoder(example_ingestions[:limit])
179
+
180
+ @pytest.mark.skip(reason="Test is currently broken")
181
+ def test_get_next_page(self):
182
+ example_ingestions = self.populate_table(100)
183
+
184
+ limit = 25
185
+ next_param = base64.b64encode(
186
+ example_ingestions[limit - 1]
187
+ .json(include={"created_by", "id", "status", "created_at"})
188
+ .encode()
189
+ )
190
+
191
+ response = self.api_client.get(
192
+ ingestion_endpoint, params={"limit": limit, "next": next_param}
193
+ )
194
+ assert response.status_code == 200
195
+ assert response.json()["items"] == [
196
+ json.loads(ingestion.json(by_alias=True))
197
+ for ingestion in example_ingestions[limit : limit * 2]
198
+ ]
@@ -0,0 +1,35 @@
1
+ from unittest.mock import Mock, patch
2
+
3
+ import pytest
4
+ from pypgstac.load import Methods
5
+ from fastapi.encoders import jsonable_encoder
6
+ from src.utils import DbCreds
7
+
8
+
9
+ @pytest.fixture()
10
+ def loader():
11
+ with patch("src.utils.Loader", autospec=True) as m:
12
+ yield m
13
+
14
+
15
+ @pytest.fixture()
16
+ def pgstacdb():
17
+ with patch("src.utils.PgstacDB", autospec=True) as m:
18
+ m.return_value.__enter__.return_value = Mock()
19
+ yield m
20
+
21
+
22
+ @pytest.fixture()
23
+ def dbcreds():
24
+ dbcreds = DbCreds(username="", password="", host="", port=1, dbname="", engine="")
25
+ return dbcreds
26
+
27
+
28
+ def test_load_items(loader, pgstacdb, example_ingestion, dbcreds):
29
+ import src.utils as utils
30
+
31
+ utils.load_items(dbcreds, list([example_ingestion]))
32
+ loader.return_value.load_items.assert_called_once_with(
33
+ file=jsonable_encoder([example_ingestion.item]),
34
+ insert_mode=Methods.upsert,
35
+ )
@@ -0,0 +1,50 @@
1
+ import { aws_ec2 as ec2, aws_rds as rds, aws_secretsmanager as secretsmanager } from "aws-cdk-lib";
2
+ import { PythonFunction, PythonFunctionProps } from "@aws-cdk/aws-lambda-python-alpha";
3
+ import { Construct } from "constructs";
4
+ export declare class PgStacApiLambda extends Construct {
5
+ readonly url: string;
6
+ stacApiLambdaFunction: PythonFunction;
7
+ constructor(scope: Construct, id: string, props: PgStacApiLambdaProps);
8
+ }
9
+ export interface PgStacApiLambdaProps {
10
+ /**
11
+ * VPC into which the lambda should be deployed.
12
+ */
13
+ readonly vpc: ec2.IVpc;
14
+ /**
15
+ * RDS Instance with installed pgSTAC.
16
+ */
17
+ readonly db: rds.IDatabaseInstance;
18
+ /**
19
+ * Subnet into which the lambda should be deployed.
20
+ */
21
+ readonly subnetSelection: ec2.SubnetSelection;
22
+ /**
23
+ * Secret containing connection information for pgSTAC database.
24
+ */
25
+ readonly dbSecret: secretsmanager.ISecret;
26
+ /**
27
+ * Custom code to run for fastapi-pgstac.
28
+ *
29
+ * @default - simplified version of fastapi-pgstac
30
+ */
31
+ readonly apiCode?: ApiEntrypoint;
32
+ /**
33
+ * Customized environment variables to send to fastapi-pgstac runtime.
34
+ */
35
+ readonly apiEnv?: Record<string, string>;
36
+ }
37
+ export interface ApiEntrypoint {
38
+ /**
39
+ * Path to the source of the function or the location for dependencies.
40
+ */
41
+ readonly entry: PythonFunctionProps["entry"];
42
+ /**
43
+ * The path (relative to entry) to the index file containing the exported handler.
44
+ */
45
+ readonly index: PythonFunctionProps["index"];
46
+ /**
47
+ * The name of the exported handler in the index file.
48
+ */
49
+ readonly handler: PythonFunctionProps["handler"];
50
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var _a;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.PgStacApiLambda = 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 aws_apigatewayv2_alpha_1 = require("@aws-cdk/aws-apigatewayv2-alpha");
9
+ const aws_apigatewayv2_integrations_alpha_1 = require("@aws-cdk/aws-apigatewayv2-integrations-alpha");
10
+ const constructs_1 = require("constructs");
11
+ class PgStacApiLambda extends constructs_1.Construct {
12
+ constructor(scope, id, props) {
13
+ super(scope, id);
14
+ const apiCode = props.apiCode || {
15
+ entry: `${__dirname}/runtime`,
16
+ index: "src/handler.py",
17
+ handler: "handler",
18
+ };
19
+ this.stacApiLambdaFunction = new aws_lambda_python_alpha_1.PythonFunction(this, "stac-api", {
20
+ ...apiCode,
21
+ /**
22
+ * NOTE: Unable to use Py3.9, due to issues with hashes:
23
+ *
24
+ * ERROR: Hashes are required in --require-hashes mode, but they are missing
25
+ * from some requirements. Here is a list of those requirements along with the
26
+ * hashes their downloaded archives actually had. Add lines like these to your
27
+ * requirements files to prevent tampering. (If you did not enable
28
+ * --require-hashes manually, note that it turns on automatically when any
29
+ * package has a hash.)
30
+ * anyio==3.6.1 --hash=sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be
31
+ * */
32
+ runtime: aws_cdk_lib_1.aws_lambda.Runtime.PYTHON_3_8,
33
+ architecture: aws_cdk_lib_1.aws_lambda.Architecture.X86_64,
34
+ environment: {
35
+ PGSTAC_SECRET_ARN: props.dbSecret.secretArn,
36
+ DB_MIN_CONN_SIZE: "0",
37
+ DB_MAX_CONN_SIZE: "1",
38
+ ...props.apiEnv,
39
+ },
40
+ vpc: props.vpc,
41
+ vpcSubnets: props.subnetSelection,
42
+ allowPublicSubnet: true,
43
+ memorySize: 8192,
44
+ });
45
+ props.dbSecret.grantRead(this.stacApiLambdaFunction);
46
+ this.stacApiLambdaFunction.connections.allowTo(props.db, aws_cdk_lib_1.aws_ec2.Port.tcp(5432));
47
+ const stacApi = new aws_apigatewayv2_alpha_1.HttpApi(this, `${aws_cdk_lib_1.Stack.of(this).stackName}-stac-api`, {
48
+ defaultIntegration: new aws_apigatewayv2_integrations_alpha_1.HttpLambdaIntegration("integration", this.stacApiLambdaFunction),
49
+ });
50
+ this.url = stacApi.url;
51
+ new aws_cdk_lib_1.CfnOutput(this, "stac-api-output", {
52
+ exportName: `${aws_cdk_lib_1.Stack.of(this).stackName}-url`,
53
+ value: this.url,
54
+ });
55
+ }
56
+ }
57
+ exports.PgStacApiLambda = PgStacApiLambda;
58
+ _a = JSII_RTTI_SYMBOL_1;
59
+ PgStacApiLambda[_a] = { fqn: "eoapi-cdk.PgStacApiLambda", version: "5.0.0" };
60
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7OztBQUFBLDZDQU9xQjtBQUNyQiw4RUFHMEM7QUFDMUMsNEVBQTBEO0FBQzFELHNHQUFxRjtBQUNyRiwyQ0FBdUM7QUFFdkMsTUFBYSxlQUFnQixTQUFRLHNCQUFTO0lBSTVDLFlBQVksS0FBZ0IsRUFBRSxFQUFVLEVBQUUsS0FBMkI7UUFDbkUsS0FBSyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztRQUVqQixNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsT0FBTyxJQUFJO1lBQy9CLEtBQUssRUFBRSxHQUFHLFNBQVMsVUFBVTtZQUM3QixLQUFLLEVBQUUsZ0JBQWdCO1lBQ3ZCLE9BQU8sRUFBRSxTQUFTO1NBQ25CLENBQUM7UUFFRixJQUFJLENBQUMscUJBQXFCLEdBQUcsSUFBSSx3Q0FBYyxDQUFDLElBQUksRUFBRSxVQUFVLEVBQUU7WUFDaEUsR0FBRyxPQUFPO1lBQ1Y7Ozs7Ozs7Ozs7aUJBVUs7WUFDTCxPQUFPLEVBQUUsd0JBQU0sQ0FBQyxPQUFPLENBQUMsVUFBVTtZQUNsQyxZQUFZLEVBQUUsd0JBQU0sQ0FBQyxZQUFZLENBQUMsTUFBTTtZQUN4QyxXQUFXLEVBQUU7Z0JBQ1gsaUJBQWlCLEVBQUUsS0FBSyxDQUFDLFFBQVEsQ0FBQyxTQUFTO2dCQUMzQyxnQkFBZ0IsRUFBRSxHQUFHO2dCQUNyQixnQkFBZ0IsRUFBRSxHQUFHO2dCQUNyQixHQUFHLEtBQUssQ0FBQyxNQUFNO2FBQ2hCO1lBQ0QsR0FBRyxFQUFFLEtBQUssQ0FBQyxHQUFHO1lBQ2QsVUFBVSxFQUFFLEtBQUssQ0FBQyxlQUFlO1lBQ2pDLGlCQUFpQixFQUFFLElBQUk7WUFDdkIsVUFBVSxFQUFFLElBQUk7U0FDakIsQ0FBQyxDQUFDO1FBRUgsS0FBSyxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLHFCQUFxQixDQUFDLENBQUM7UUFDckQsSUFBSSxDQUFDLHFCQUFxQixDQUFDLFdBQVcsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLEVBQUUsRUFBRSxxQkFBRyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztRQUU3RSxNQUFNLE9BQU8sR0FBRyxJQUFJLGdDQUFPLENBQUMsSUFBSSxFQUFFLEdBQUcsbUJBQUssQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLENBQUMsU0FBUyxXQUFXLEVBQUU7WUFDeEUsa0JBQWtCLEVBQUUsSUFBSSwyREFBcUIsQ0FBQyxhQUFhLEVBQUUsSUFBSSxDQUFDLHFCQUFxQixDQUFDO1NBQ3pGLENBQUMsQ0FBQztRQUVILElBQUksQ0FBQyxHQUFHLEdBQUcsT0FBTyxDQUFDLEdBQUksQ0FBQztRQUV4QixJQUFJLHVCQUFTLENBQUMsSUFBSSxFQUFFLGlCQUFpQixFQUFFO1lBQ3JDLFVBQVUsRUFBRSxHQUFHLG1CQUFLLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxDQUFDLFNBQVMsTUFBTTtZQUM3QyxLQUFLLEVBQUUsSUFBSSxDQUFDLEdBQUc7U0FDaEIsQ0FBQyxDQUFDO0lBQ0wsQ0FBQzs7QUFyREgsMENBc0RDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHtcbiAgU3RhY2ssXG4gIGF3c19lYzIgYXMgZWMyLFxuICBhd3NfcmRzIGFzIHJkcyxcbiAgYXdzX2xhbWJkYSBhcyBsYW1iZGEsXG4gIGF3c19zZWNyZXRzbWFuYWdlciBhcyBzZWNyZXRzbWFuYWdlcixcbiAgQ2ZuT3V0cHV0LFxufSBmcm9tIFwiYXdzLWNkay1saWJcIjtcbmltcG9ydCB7XG4gIFB5dGhvbkZ1bmN0aW9uLFxuICBQeXRob25GdW5jdGlvblByb3BzLFxufSBmcm9tIFwiQGF3cy1jZGsvYXdzLWxhbWJkYS1weXRob24tYWxwaGFcIjtcbmltcG9ydCB7IEh0dHBBcGkgfSBmcm9tIFwiQGF3cy1jZGsvYXdzLWFwaWdhdGV3YXl2Mi1hbHBoYVwiO1xuaW1wb3J0IHsgSHR0cExhbWJkYUludGVncmF0aW9uIH0gZnJvbSBcIkBhd3MtY2RrL2F3cy1hcGlnYXRld2F5djItaW50ZWdyYXRpb25zLWFscGhhXCI7XG5pbXBvcnQgeyBDb25zdHJ1Y3QgfSBmcm9tIFwiY29uc3RydWN0c1wiO1xuXG5leHBvcnQgY2xhc3MgUGdTdGFjQXBpTGFtYmRhIGV4dGVuZHMgQ29uc3RydWN0IHtcbiAgcmVhZG9ubHkgdXJsOiBzdHJpbmc7XG4gIHB1YmxpYyBzdGFjQXBpTGFtYmRhRnVuY3Rpb246IFB5dGhvbkZ1bmN0aW9uO1xuXG4gIGNvbnN0cnVjdG9yKHNjb3BlOiBDb25zdHJ1Y3QsIGlkOiBzdHJpbmcsIHByb3BzOiBQZ1N0YWNBcGlMYW1iZGFQcm9wcykge1xuICAgIHN1cGVyKHNjb3BlLCBpZCk7XG5cbiAgICBjb25zdCBhcGlDb2RlID0gcHJvcHMuYXBpQ29kZSB8fCB7XG4gICAgICBlbnRyeTogYCR7X19kaXJuYW1lfS9ydW50aW1lYCxcbiAgICAgIGluZGV4OiBcInNyYy9oYW5kbGVyLnB5XCIsXG4gICAgICBoYW5kbGVyOiBcImhhbmRsZXJcIixcbiAgICB9O1xuXG4gICAgdGhpcy5zdGFjQXBpTGFtYmRhRnVuY3Rpb24gPSBuZXcgUHl0aG9uRnVuY3Rpb24odGhpcywgXCJzdGFjLWFwaVwiLCB7XG4gICAgICAuLi5hcGlDb2RlLFxuICAgICAgLyoqXG4gICAgICAgKiBOT1RFOiBVbmFibGUgdG8gdXNlIFB5My45LCBkdWUgdG8gaXNzdWVzIHdpdGggaGFzaGVzOlxuICAgICAgICpcbiAgICAgICAqICAgIEVSUk9SOiBIYXNoZXMgYXJlIHJlcXVpcmVkIGluIC0tcmVxdWlyZS1oYXNoZXMgbW9kZSwgYnV0IHRoZXkgYXJlIG1pc3NpbmdcbiAgICAgICAqICAgIGZyb20gc29tZSByZXF1aXJlbWVudHMuIEhlcmUgaXMgYSBsaXN0IG9mIHRob3NlIHJlcXVpcmVtZW50cyBhbG9uZyB3aXRoIHRoZVxuICAgICAgICogICAgaGFzaGVzIHRoZWlyIGRvd25sb2FkZWQgYXJjaGl2ZXMgYWN0dWFsbHkgaGFkLiBBZGQgbGluZXMgbGlrZSB0aGVzZSB0byB5b3VyXG4gICAgICAgKiAgICByZXF1aXJlbWVudHMgZmlsZXMgdG8gcHJldmVudCB0YW1wZXJpbmcuIChJZiB5b3UgZGlkIG5vdCBlbmFibGVcbiAgICAgICAqICAgIC0tcmVxdWlyZS1oYXNoZXMgbWFudWFsbHksIG5vdGUgdGhhdCBpdCB0dXJucyBvbiBhdXRvbWF0aWNhbGx5IHdoZW4gYW55XG4gICAgICAgKiAgICBwYWNrYWdlIGhhcyBhIGhhc2guKVxuICAgICAgICogICAgICAgIGFueWlvPT0zLjYuMSAtLWhhc2g9c2hhMjU2OmNiMjliOWM3MDYyMDUwNmE5YThmODdhMzA5NTkxNzEzNDQ2OTUzMzAyZDdkOTk1MzQ0ZDBkN2M2YzBjOWE3YmVcbiAgICAgICAqICovXG4gICAgICBydW50aW1lOiBsYW1iZGEuUnVudGltZS5QWVRIT05fM184LFxuICAgICAgYXJjaGl0ZWN0dXJlOiBsYW1iZGEuQXJjaGl0ZWN0dXJlLlg4Nl82NCxcbiAgICAgIGVudmlyb25tZW50OiB7XG4gICAgICAgIFBHU1RBQ19TRUNSRVRfQVJOOiBwcm9wcy5kYlNlY3JldC5zZWNyZXRBcm4sXG4gICAgICAgIERCX01JTl9DT05OX1NJWkU6IFwiMFwiLFxuICAgICAgICBEQl9NQVhfQ09OTl9TSVpFOiBcIjFcIixcbiAgICAgICAgLi4ucHJvcHMuYXBpRW52LFxuICAgICAgfSxcbiAgICAgIHZwYzogcHJvcHMudnBjLFxuICAgICAgdnBjU3VibmV0czogcHJvcHMuc3VibmV0U2VsZWN0aW9uLFxuICAgICAgYWxsb3dQdWJsaWNTdWJuZXQ6IHRydWUsXG4gICAgICBtZW1vcnlTaXplOiA4MTkyLFxuICAgIH0pO1xuXG4gICAgcHJvcHMuZGJTZWNyZXQuZ3JhbnRSZWFkKHRoaXMuc3RhY0FwaUxhbWJkYUZ1bmN0aW9uKTtcbiAgICB0aGlzLnN0YWNBcGlMYW1iZGFGdW5jdGlvbi5jb25uZWN0aW9ucy5hbGxvd1RvKHByb3BzLmRiLCBlYzIuUG9ydC50Y3AoNTQzMikpO1xuXG4gICAgY29uc3Qgc3RhY0FwaSA9IG5ldyBIdHRwQXBpKHRoaXMsIGAke1N0YWNrLm9mKHRoaXMpLnN0YWNrTmFtZX0tc3RhYy1hcGlgLCB7XG4gICAgICBkZWZhdWx0SW50ZWdyYXRpb246IG5ldyBIdHRwTGFtYmRhSW50ZWdyYXRpb24oXCJpbnRlZ3JhdGlvblwiLCB0aGlzLnN0YWNBcGlMYW1iZGFGdW5jdGlvbiksXG4gICAgfSk7XG5cbiAgICB0aGlzLnVybCA9IHN0YWNBcGkudXJsITtcblxuICAgIG5ldyBDZm5PdXRwdXQodGhpcywgXCJzdGFjLWFwaS1vdXRwdXRcIiwge1xuICAgICAgZXhwb3J0TmFtZTogYCR7U3RhY2sub2YodGhpcykuc3RhY2tOYW1lfS11cmxgLFxuICAgICAgdmFsdWU6IHRoaXMudXJsLFxuICAgIH0pO1xuICB9XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgUGdTdGFjQXBpTGFtYmRhUHJvcHMge1xuICAvKipcbiAgICogVlBDIGludG8gd2hpY2ggdGhlIGxhbWJkYSBzaG91bGQgYmUgZGVwbG95ZWQuXG4gICAqL1xuICByZWFkb25seSB2cGM6IGVjMi5JVnBjO1xuXG4gIC8qKlxuICAgKiBSRFMgSW5zdGFuY2Ugd2l0aCBpbnN0YWxsZWQgcGdTVEFDLlxuICAgKi9cbiAgcmVhZG9ubHkgZGI6IHJkcy5JRGF0YWJhc2VJbnN0YW5jZTtcblxuICAvKipcbiAgICogU3VibmV0IGludG8gd2hpY2ggdGhlIGxhbWJkYSBzaG91bGQgYmUgZGVwbG95ZWQuXG4gICAqL1xuICByZWFkb25seSBzdWJuZXRTZWxlY3Rpb246IGVjMi5TdWJuZXRTZWxlY3Rpb247XG5cbiAgLyoqXG4gICAqIFNlY3JldCBjb250YWluaW5nIGNvbm5lY3Rpb24gaW5mb3JtYXRpb24gZm9yIHBnU1RBQyBkYXRhYmFzZS5cbiAgICovXG4gIHJlYWRvbmx5IGRiU2VjcmV0OiBzZWNyZXRzbWFuYWdlci5JU2VjcmV0O1xuXG4gIC8qKlxuICAgKiBDdXN0b20gY29kZSB0byBydW4gZm9yIGZhc3RhcGktcGdzdGFjLlxuICAgKlxuICAgKiBAZGVmYXVsdCAtIHNpbXBsaWZpZWQgdmVyc2lvbiBvZiBmYXN0YXBpLXBnc3RhY1xuICAgKi9cbiAgcmVhZG9ubHkgYXBpQ29kZT86IEFwaUVudHJ5cG9pbnQ7XG5cbiAgLyoqXG4gICAqIEN1c3RvbWl6ZWQgZW52aXJvbm1lbnQgdmFyaWFibGVzIHRvIHNlbmQgdG8gZmFzdGFwaS1wZ3N0YWMgcnVudGltZS5cbiAgICovXG4gIHJlYWRvbmx5IGFwaUVudj86IFJlY29yZDxzdHJpbmcsIHN0cmluZz47XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgQXBpRW50cnlwb2ludCB7XG4gIC8qKlxuICAgKiBQYXRoIHRvIHRoZSBzb3VyY2Ugb2YgdGhlIGZ1bmN0aW9uIG9yIHRoZSBsb2NhdGlvbiBmb3IgZGVwZW5kZW5jaWVzLlxuICAgKi9cbiAgcmVhZG9ubHkgZW50cnk6IFB5dGhvbkZ1bmN0aW9uUHJvcHNbXCJlbnRyeVwiXTtcbiAgLyoqXG4gICAqIFRoZSBwYXRoIChyZWxhdGl2ZSB0byBlbnRyeSkgdG8gdGhlIGluZGV4IGZpbGUgY29udGFpbmluZyB0aGUgZXhwb3J0ZWQgaGFuZGxlci5cbiAgICovXG4gIHJlYWRvbmx5IGluZGV4OiBQeXRob25GdW5jdGlvblByb3BzW1wiaW5kZXhcIl07XG4gIC8qKlxuICAgKiBUaGUgbmFtZSBvZiB0aGUgZXhwb3J0ZWQgaGFuZGxlciBpbiB0aGUgaW5kZXggZmlsZS5cbiAgICovXG4gIHJlYWRvbmx5IGhhbmRsZXI6IFB5dGhvbkZ1bmN0aW9uUHJvcHNbXCJoYW5kbGVyXCJdO1xufVxuIl19
@@ -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
File without changes
@@ -0,0 +1,58 @@
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 ApiSettings
13
+ from .config import extensions as PgStacExtensions
14
+ from .config import get_request_model as GETModel
15
+ from .config import post_request_model as POSTModel
16
+
17
+ api_settings = ApiSettings()
18
+
19
+ api = StacApi(
20
+ title=api_settings.name,
21
+ api_version=api_settings.version,
22
+ description=api_settings.description or api_settings.name,
23
+ settings=api_settings.load_postgres_settings(),
24
+ extensions=PgStacExtensions,
25
+ client=CoreCrudClient(post_request_model=POSTModel),
26
+ search_get_request_model=GETModel,
27
+ search_post_request_model=POSTModel,
28
+ response_class=ORJSONResponse,
29
+ middlewares=[CompressionMiddleware],
30
+ )
31
+
32
+ app = api.app
33
+
34
+ # Set all CORS enabled origins
35
+ if api_settings.cors_origins:
36
+ app.add_middleware(
37
+ CORSMiddleware,
38
+ allow_origins=api_settings.cors_origins,
39
+ allow_credentials=True,
40
+ allow_methods=["GET", "POST", "OPTIONS"],
41
+ allow_headers=["*"],
42
+ )
43
+
44
+
45
+ @app.on_event("startup")
46
+ async def startup_event():
47
+ """Connect to database on startup."""
48
+ print("Setting up DB connection...")
49
+ await connect_to_db(app)
50
+ print("DB connection setup.")
51
+
52
+
53
+ @app.on_event("shutdown")
54
+ async def shutdown_event():
55
+ """Close database connection."""
56
+ print("Closing up DB connection...")
57
+ await close_db_connection(app)
58
+ print("DB connection closed.")
@@ -0,0 +1,96 @@
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
+ from stac_fastapi.api.models import create_get_request_model, create_post_request_model
10
+
11
+ # from stac_fastapi.pgstac.extensions import QueryExtension
12
+ from stac_fastapi.extensions.core import (
13
+ ContextExtension,
14
+ FieldsExtension,
15
+ FilterExtension,
16
+ QueryExtension,
17
+ SortExtension,
18
+ TokenPaginationExtension,
19
+ )
20
+ from stac_fastapi.pgstac.config import Settings
21
+ from stac_fastapi.pgstac.types.search import PgstacSearch
22
+
23
+
24
+ def get_secret_dict(secret_name: str):
25
+ """Retrieve secrets from AWS Secrets Manager
26
+
27
+ Args:
28
+ secret_name (str): name of aws secrets manager secret containing database connection secrets
29
+ profile_name (str, optional): optional name of aws profile for use in debugger only
30
+
31
+ Returns:
32
+ secrets (dict): decrypted secrets in dict
33
+ """
34
+
35
+ # Create a Secrets Manager client
36
+ session = boto3.session.Session()
37
+ client = session.client(service_name="secretsmanager")
38
+
39
+ get_secret_value_response = client.get_secret_value(SecretId=secret_name)
40
+
41
+ if "SecretString" in get_secret_value_response:
42
+ return json.loads(get_secret_value_response["SecretString"])
43
+ else:
44
+ return json.loads(base64.b64decode(get_secret_value_response["SecretBinary"]))
45
+
46
+
47
+ class ApiSettings(pydantic.BaseSettings):
48
+ """API settings"""
49
+
50
+ name: str = "asdi-stac-api"
51
+ version: str = "0.1"
52
+ description: Optional[str] = None
53
+ cors_origins: str = "*"
54
+ cachecontrol: str = "public, max-age=3600"
55
+ debug: bool = False
56
+
57
+ pgstac_secret_arn: Optional[str]
58
+
59
+ @pydantic.validator("cors_origins")
60
+ def parse_cors_origin(cls, v):
61
+ """Parse CORS origins."""
62
+ return [origin.strip() for origin in v.split(",")]
63
+
64
+ def load_postgres_settings(self) -> "Settings":
65
+ """Load postgres connection params from AWS secret"""
66
+
67
+ if self.pgstac_secret_arn:
68
+ secret = get_secret_dict(self.pgstac_secret_arn)
69
+
70
+ return Settings(
71
+ postgres_host_reader=secret["host"],
72
+ postgres_host_writer=secret["host"],
73
+ postgres_dbname=secret["dbname"],
74
+ postgres_user=secret["username"],
75
+ postgres_pass=secret["password"],
76
+ postgres_port=secret["port"],
77
+ )
78
+ else:
79
+ return Settings()
80
+
81
+ class Config:
82
+ """model config"""
83
+
84
+ env_file = ".env"
85
+
86
+
87
+ extensions = [
88
+ FilterExtension(),
89
+ QueryExtension(),
90
+ SortExtension(),
91
+ FieldsExtension(),
92
+ TokenPaginationExtension(),
93
+ ContextExtension(),
94
+ ]
95
+ post_request_model = create_post_request_model(extensions, base_model=PgstacSearch)
96
+ get_request_model = create_get_request_model(extensions)
@@ -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,0 +1,33 @@
1
+ import { aws_ec2 as ec2, aws_rds as rds, aws_lambda as lambda, aws_secretsmanager as secretsmanager } from "aws-cdk-lib";
2
+ import { Construct } from "constructs";
3
+ export declare class TitilerPgstacApiLambda extends Construct {
4
+ readonly url: string;
5
+ titilerPgstacLambdaFunction: lambda.Function;
6
+ constructor(scope: Construct, id: string, props: TitilerPgStacApiLambdaProps);
7
+ }
8
+ export interface TitilerPgStacApiLambdaProps {
9
+ /**
10
+ * VPC into which the lambda should be deployed.
11
+ */
12
+ readonly vpc: ec2.IVpc;
13
+ /**
14
+ * RDS Instance with installed pgSTAC.
15
+ */
16
+ readonly db: rds.IDatabaseInstance;
17
+ /**
18
+ * Subnet into which the lambda should be deployed.
19
+ */
20
+ readonly subnetSelection: ec2.SubnetSelection;
21
+ /**
22
+ * Secret containing connection information for pgSTAC database.
23
+ */
24
+ readonly dbSecret: secretsmanager.ISecret;
25
+ /**
26
+ * Customized environment variables to send to titiler-pgstac runtime.
27
+ */
28
+ readonly apiEnv?: Record<string, string>;
29
+ /**
30
+ * list of buckets the lambda will be granted access to.
31
+ */
32
+ readonly buckets?: string[];
33
+ }