eoapi-cdk 8.1.1 → 8.2.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/.jsii +594 -26
- package/lib/bastion-host/index.js +1 -1
- package/lib/database/index.d.ts +1 -0
- package/lib/database/index.js +5 -5
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -1
- package/lib/ingestor-api/index.js +1 -1
- package/lib/stac-api/index.js +1 -1
- package/lib/stac-browser/index.js +1 -1
- package/lib/stac-item-loader/index.d.ts +322 -0
- package/lib/stac-item-loader/index.js +251 -0
- package/lib/stac-item-loader/runtime/Dockerfile +18 -0
- package/lib/stac-item-loader/runtime/pyproject.toml +17 -0
- package/lib/stac-item-loader/runtime/src/stac_item_loader/handler.py +241 -0
- package/lib/stactools-item-generator/index.d.ts +243 -0
- package/lib/stactools-item-generator/index.js +204 -0
- package/lib/stactools-item-generator/runtime/Dockerfile +20 -0
- package/lib/stactools-item-generator/runtime/pyproject.toml +16 -0
- package/lib/stactools-item-generator/runtime/src/stactools_item_generator/__init__.py +2 -0
- package/lib/stactools-item-generator/runtime/src/stactools_item_generator/handler.py +176 -0
- package/lib/stactools-item-generator/runtime/src/stactools_item_generator/item.py +77 -0
- package/lib/tipg-api/index.js +1 -1
- package/lib/titiler-pgstac-api/index.js +1 -1
- package/package.json +1 -1
- package/pyproject.toml +45 -0
- package/uv.lock +1065 -0
- package/.devcontainer/devcontainer.json +0 -4
- package/.github/pull_request_template.md +0 -4
- package/.github/workflows/build.yaml +0 -73
- package/.github/workflows/build_and_release.yaml +0 -13
- package/.github/workflows/conventional-pr.yaml +0 -26
- package/.github/workflows/deploy.yaml +0 -84
- package/.github/workflows/distribute.yaml +0 -46
- package/.github/workflows/docs.yaml +0 -26
- package/.github/workflows/lint.yaml +0 -26
- package/.github/workflows/tox.yaml +0 -26
- package/.nvmrc +0 -1
- package/.pre-commit-config.yaml +0 -23
- package/CHANGELOG.md +0 -471
- package/diagrams/bastion_diagram.excalidraw +0 -1416
- package/diagrams/bastion_diagram.png +0 -0
- package/diagrams/ingestor_diagram.excalidraw +0 -2274
- package/diagrams/ingestor_diagram.png +0 -0
- package/integration_tests/cdk/README.md +0 -55
- package/integration_tests/cdk/app.py +0 -186
- package/integration_tests/cdk/cdk.json +0 -32
- package/integration_tests/cdk/config.py +0 -52
- package/integration_tests/cdk/package-lock.json +0 -42
- package/integration_tests/cdk/package.json +0 -7
- package/integration_tests/cdk/requirements.txt +0 -7
- package/lib/database/lambda/package-lock.json +0 -1324
- package/lib/ingestor-api/runtime/tests/conftest.py +0 -270
- package/lib/ingestor-api/runtime/tests/test_collection.py +0 -87
- package/lib/ingestor-api/runtime/tests/test_collection_endpoint.py +0 -41
- package/lib/ingestor-api/runtime/tests/test_ingestor.py +0 -60
- package/lib/ingestor-api/runtime/tests/test_registration.py +0 -207
- package/lib/ingestor-api/runtime/tests/test_utils.py +0 -35
- package/lib/ingestor-api/runtime/tests/test_validators.py +0 -164
- package/ruff.toml +0 -23
- package/tox.ini +0 -16
- package/tsconfig.tsbuildinfo +0 -1
- /package/lib/{ingestor-api/runtime/tests → stac-item-loader/runtime/src/stac_item_loader}/__init__.py +0 -0
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import boto3
|
|
2
|
-
import pytest
|
|
3
|
-
from fastapi.testclient import TestClient
|
|
4
|
-
from moto import mock_dynamodb, mock_ssm
|
|
5
|
-
from stac_pydantic import Item
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@pytest.fixture
|
|
9
|
-
def test_environ(monkeypatch):
|
|
10
|
-
# Mocked AWS Credentials for moto
|
|
11
|
-
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
|
|
12
|
-
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
|
|
13
|
-
monkeypatch.setenv("AWS_SECURITY_TOKEN", "testing")
|
|
14
|
-
monkeypatch.setenv("AWS_SESSION_TOKEN", "testing")
|
|
15
|
-
|
|
16
|
-
# Config mocks
|
|
17
|
-
monkeypatch.setenv("DYNAMODB_TABLE", "test_table")
|
|
18
|
-
monkeypatch.setenv("JWKS_URL", "https://test-jwks.url")
|
|
19
|
-
monkeypatch.setenv("STAC_URL", "https://test-stac.url")
|
|
20
|
-
monkeypatch.setenv("DATA_ACCESS_ROLE", "arn:aws:iam::123456789012:role/test-role")
|
|
21
|
-
monkeypatch.setenv("DB_SECRET_ARN", "testing")
|
|
22
|
-
monkeypatch.setenv("ROOT_PATH", "testing")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@pytest.fixture
|
|
26
|
-
def mock_ssm_parameter_store():
|
|
27
|
-
with mock_ssm():
|
|
28
|
-
yield boto3.client("ssm")
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@pytest.fixture
|
|
32
|
-
def app(test_environ, mock_ssm_parameter_store):
|
|
33
|
-
from src.main import app
|
|
34
|
-
|
|
35
|
-
return app
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@pytest.fixture
|
|
39
|
-
def api_client(app):
|
|
40
|
-
return TestClient(app)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@pytest.fixture
|
|
44
|
-
def mock_table(app, test_environ):
|
|
45
|
-
from src import dependencies
|
|
46
|
-
from src.config import settings
|
|
47
|
-
|
|
48
|
-
with mock_dynamodb():
|
|
49
|
-
client = boto3.resource("dynamodb")
|
|
50
|
-
mock_table = client.create_table(
|
|
51
|
-
TableName=settings.dynamodb_table,
|
|
52
|
-
AttributeDefinitions=[
|
|
53
|
-
{"AttributeName": "created_by", "AttributeType": "S"},
|
|
54
|
-
{"AttributeName": "id", "AttributeType": "S"},
|
|
55
|
-
{"AttributeName": "status", "AttributeType": "S"},
|
|
56
|
-
{"AttributeName": "created_at", "AttributeType": "S"},
|
|
57
|
-
],
|
|
58
|
-
KeySchema=[
|
|
59
|
-
{"AttributeName": "created_by", "KeyType": "HASH"},
|
|
60
|
-
{"AttributeName": "id", "KeyType": "RANGE"},
|
|
61
|
-
],
|
|
62
|
-
BillingMode="PAY_PER_REQUEST",
|
|
63
|
-
GlobalSecondaryIndexes=[
|
|
64
|
-
{
|
|
65
|
-
"IndexName": "status",
|
|
66
|
-
"KeySchema": [
|
|
67
|
-
{"AttributeName": "status", "KeyType": "HASH"},
|
|
68
|
-
{"AttributeName": "created_at", "KeyType": "RANGE"},
|
|
69
|
-
],
|
|
70
|
-
"Projection": {"ProjectionType": "ALL"},
|
|
71
|
-
}
|
|
72
|
-
],
|
|
73
|
-
)
|
|
74
|
-
app.dependency_overrides[dependencies.get_table] = lambda: mock_table
|
|
75
|
-
yield mock_table
|
|
76
|
-
app.dependency_overrides.pop(dependencies.get_table)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@pytest.fixture
|
|
80
|
-
def example_stac_item():
|
|
81
|
-
return {
|
|
82
|
-
"stac_version": "1.0.0",
|
|
83
|
-
"stac_extensions": [],
|
|
84
|
-
"type": "Feature",
|
|
85
|
-
"id": "20201211_223832_CS2",
|
|
86
|
-
"bbox": [
|
|
87
|
-
172.91173669923782,
|
|
88
|
-
1.3438851951615003,
|
|
89
|
-
172.95469614953714,
|
|
90
|
-
1.3690476620161975,
|
|
91
|
-
],
|
|
92
|
-
"geometry": {
|
|
93
|
-
"type": "Polygon",
|
|
94
|
-
"coordinates": [
|
|
95
|
-
[
|
|
96
|
-
[172.91173669923782, 1.3438851951615003],
|
|
97
|
-
[172.95469614953714, 1.3438851951615003],
|
|
98
|
-
[172.95469614953714, 1.3690476620161975],
|
|
99
|
-
[172.91173669923782, 1.3690476620161975],
|
|
100
|
-
[172.91173669923782, 1.3438851951615003],
|
|
101
|
-
]
|
|
102
|
-
],
|
|
103
|
-
},
|
|
104
|
-
"properties": {
|
|
105
|
-
"datetime": "2020-12-11T22:38:32.125000Z",
|
|
106
|
-
"eo:cloud_cover": 1,
|
|
107
|
-
},
|
|
108
|
-
"collection": "simple-collection",
|
|
109
|
-
"links": [
|
|
110
|
-
{
|
|
111
|
-
"rel": "collection",
|
|
112
|
-
"href": "./collection.json",
|
|
113
|
-
"type": "application/json",
|
|
114
|
-
"title": "Simple Example Collection",
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
"rel": "root",
|
|
118
|
-
"href": "./collection.json",
|
|
119
|
-
"type": "application/json",
|
|
120
|
-
"title": "Simple Example Collection",
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
"rel": "parent",
|
|
124
|
-
"href": "./collection.json",
|
|
125
|
-
"type": "application/json",
|
|
126
|
-
"title": "Simple Example Collection",
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
"assets": {
|
|
130
|
-
"visual": {
|
|
131
|
-
"href": "https://TEST_API.com/open-cogs/stac-examples/20201211_223832_CS2.tif", # noqa
|
|
132
|
-
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
|
|
133
|
-
"title": "3-Band Visual",
|
|
134
|
-
"roles": ["visual"],
|
|
135
|
-
},
|
|
136
|
-
"thumbnail": {
|
|
137
|
-
"href": "https://TEST_API.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", # noqa
|
|
138
|
-
"title": "Thumbnail",
|
|
139
|
-
"type": "image/jpeg",
|
|
140
|
-
"roles": ["thumbnail"],
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
@pytest.fixture
|
|
147
|
-
def example_stac_collection():
|
|
148
|
-
return {
|
|
149
|
-
"id": "simple-collection",
|
|
150
|
-
"type": "Collection",
|
|
151
|
-
"stac_extensions": [
|
|
152
|
-
"https://stac-extensions.github.io/eo/v1.0.0/schema.json",
|
|
153
|
-
"https://stac-extensions.github.io/projection/v1.0.0/schema.json",
|
|
154
|
-
"https://stac-extensions.github.io/view/v1.0.0/schema.json",
|
|
155
|
-
],
|
|
156
|
-
"item_assets": {
|
|
157
|
-
"data": {
|
|
158
|
-
"type": "image/tiff; application=geotiff; profile=cloud-optimized",
|
|
159
|
-
"roles": ["data"],
|
|
160
|
-
}
|
|
161
|
-
},
|
|
162
|
-
"stac_version": "1.0.0",
|
|
163
|
-
"description": "A simple collection demonstrating core catalog fields with links to a couple of items",
|
|
164
|
-
"title": "Simple Example Collection",
|
|
165
|
-
"providers": [
|
|
166
|
-
{
|
|
167
|
-
"name": "Remote Data, Inc",
|
|
168
|
-
"description": "Producers of awesome spatiotemporal assets",
|
|
169
|
-
"roles": ["producer", "processor"],
|
|
170
|
-
"url": "http://remotedata.io",
|
|
171
|
-
}
|
|
172
|
-
],
|
|
173
|
-
"extent": {
|
|
174
|
-
"spatial": {
|
|
175
|
-
"bbox": [
|
|
176
|
-
[
|
|
177
|
-
172.91173669923782,
|
|
178
|
-
1.3438851951615003,
|
|
179
|
-
172.95469614953714,
|
|
180
|
-
1.3690476620161975,
|
|
181
|
-
]
|
|
182
|
-
]
|
|
183
|
-
},
|
|
184
|
-
"temporal": {
|
|
185
|
-
"interval": [["2020-12-11T22:38:32.125Z", "2020-12-14T18:02:31.437Z"]]
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
"license": "CC-BY-4.0",
|
|
189
|
-
"summaries": {
|
|
190
|
-
"platform": ["cool_sat1", "cool_sat2"],
|
|
191
|
-
"constellation": ["ion"],
|
|
192
|
-
"instruments": ["cool_sensor_v1", "cool_sensor_v2"],
|
|
193
|
-
"gsd": {"minimum": 0.512, "maximum": 0.66},
|
|
194
|
-
"eo:cloud_cover": {"minimum": 1.2, "maximum": 1.2},
|
|
195
|
-
"proj:epsg": {"minimum": 32659, "maximum": 32659},
|
|
196
|
-
"view:sun_elevation": {"minimum": 54.9, "maximum": 54.9},
|
|
197
|
-
"view:off_nadir": {"minimum": 3.8, "maximum": 3.8},
|
|
198
|
-
"view:sun_azimuth": {"minimum": 135.7, "maximum": 135.7},
|
|
199
|
-
},
|
|
200
|
-
"links": [
|
|
201
|
-
{
|
|
202
|
-
"rel": "root",
|
|
203
|
-
"href": "./collection.json",
|
|
204
|
-
"type": "application/json",
|
|
205
|
-
"title": "Simple Example Collection",
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
"rel": "item",
|
|
209
|
-
"href": "./simple-item.json",
|
|
210
|
-
"type": "application/geo+json",
|
|
211
|
-
"title": "Simple Item",
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
"rel": "item",
|
|
215
|
-
"href": "./core-item.json",
|
|
216
|
-
"type": "application/geo+json",
|
|
217
|
-
"title": "Core Item",
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
"rel": "item",
|
|
221
|
-
"href": "./extended-item.json",
|
|
222
|
-
"type": "application/geo+json",
|
|
223
|
-
"title": "Extended Item",
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
"rel": "self",
|
|
227
|
-
"href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/collection.json",
|
|
228
|
-
"type": "application/json",
|
|
229
|
-
},
|
|
230
|
-
],
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
@pytest.fixture
|
|
235
|
-
def client(app):
|
|
236
|
-
"""
|
|
237
|
-
Return an API Client
|
|
238
|
-
"""
|
|
239
|
-
app.dependency_overrides = {}
|
|
240
|
-
return TestClient(app)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
@pytest.fixture
|
|
244
|
-
def client_authenticated(app):
|
|
245
|
-
"""
|
|
246
|
-
Returns an API client which skips the authentication
|
|
247
|
-
"""
|
|
248
|
-
from src.dependencies import get_username
|
|
249
|
-
|
|
250
|
-
app.dependency_overrides[get_username] = lambda: "test_user"
|
|
251
|
-
return TestClient(app)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
@pytest.fixture
|
|
255
|
-
def stac_collection(example_stac_collection):
|
|
256
|
-
from src import schemas
|
|
257
|
-
|
|
258
|
-
return schemas.StacCollection(**example_stac_collection)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
@pytest.fixture(scope="function")
|
|
262
|
-
def example_ingestion(example_stac_item):
|
|
263
|
-
from src import schemas
|
|
264
|
-
|
|
265
|
-
return schemas.Ingestion(
|
|
266
|
-
id=example_stac_item["id"],
|
|
267
|
-
created_by="test-user",
|
|
268
|
-
status=schemas.Status.queued,
|
|
269
|
-
item=Item.model_validate(example_stac_item),
|
|
270
|
-
)
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from unittest.mock import Mock, patch
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
import src.collection as collection
|
|
6
|
-
from fastapi import HTTPException
|
|
7
|
-
from pypgstac.load import Methods
|
|
8
|
-
from src.utils import DbCreds
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@pytest.fixture()
|
|
12
|
-
def loader():
|
|
13
|
-
with patch("src.collection.Loader", autospec=True) as m:
|
|
14
|
-
yield m
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest.fixture()
|
|
18
|
-
def pgstacdb():
|
|
19
|
-
with patch("src.collection.PgstacDB", autospec=True) as m:
|
|
20
|
-
m.return_value.__enter__.return_value = Mock()
|
|
21
|
-
yield m
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_load_collections(stac_collection, loader, pgstacdb):
|
|
25
|
-
with patch(
|
|
26
|
-
"src.collection.get_db_credentials",
|
|
27
|
-
return_value=DbCreds(
|
|
28
|
-
username="", password="", host="", port=1, dbname="", engine=""
|
|
29
|
-
),
|
|
30
|
-
):
|
|
31
|
-
os.environ["DB_SECRET_ARN"] = ""
|
|
32
|
-
collection.ingest(stac_collection)
|
|
33
|
-
|
|
34
|
-
loader.return_value.load_collections.assert_called_once_with(
|
|
35
|
-
file=[stac_collection.model_dump(mode="json")],
|
|
36
|
-
insert_mode=Methods.upsert,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_ingest_loader_error(stac_collection, pgstacdb):
|
|
41
|
-
"""Test handling of errors during the loading process."""
|
|
42
|
-
with patch(
|
|
43
|
-
"src.collection.get_db_credentials",
|
|
44
|
-
return_value=DbCreds(
|
|
45
|
-
username="", password="", host="", port=1, dbname="", engine=""
|
|
46
|
-
),
|
|
47
|
-
):
|
|
48
|
-
os.environ["DB_SECRET_ARN"] = ""
|
|
49
|
-
|
|
50
|
-
with patch("src.collection.Loader") as mock_loader:
|
|
51
|
-
mock_loader_instance = Mock()
|
|
52
|
-
mock_loader.return_value = mock_loader_instance
|
|
53
|
-
mock_loader_instance.load_collections.side_effect = Exception(
|
|
54
|
-
"Invalid collection data"
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
with pytest.raises(HTTPException) as excinfo:
|
|
58
|
-
collection.ingest(stac_collection)
|
|
59
|
-
|
|
60
|
-
assert excinfo.value.status_code == 500
|
|
61
|
-
assert "Invalid collection data" in str(excinfo.value.detail)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def test_ingest_missing_environment_variable(stac_collection):
|
|
65
|
-
"""Test handling when the required environment variable is missing."""
|
|
66
|
-
if "DB_SECRET_ARN" in os.environ:
|
|
67
|
-
del os.environ["DB_SECRET_ARN"]
|
|
68
|
-
|
|
69
|
-
with pytest.raises(HTTPException) as excinfo:
|
|
70
|
-
collection.ingest(stac_collection)
|
|
71
|
-
|
|
72
|
-
assert "DB_SECRET_ARN" in str(excinfo.value)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def test_ingest_credentials_error(stac_collection):
|
|
76
|
-
"""Test handling of errors during credential retrieval."""
|
|
77
|
-
with patch(
|
|
78
|
-
"src.collection.get_db_credentials",
|
|
79
|
-
side_effect=Exception("Failed to retrieve credentials"),
|
|
80
|
-
):
|
|
81
|
-
os.environ["DB_SECRET_ARN"] = ""
|
|
82
|
-
|
|
83
|
-
with pytest.raises(HTTPException) as excinfo:
|
|
84
|
-
collection.ingest(stac_collection)
|
|
85
|
-
|
|
86
|
-
assert excinfo.value.status_code == 500
|
|
87
|
-
assert "Failed to retrieve credentials" in str(excinfo.value.detail)
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
from unittest.mock import patch
|
|
2
|
-
|
|
3
|
-
publish_collections_endpoint = "/collections"
|
|
4
|
-
delete_collection_endpoint = "/collections/{collection_id}"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@patch("src.collection.ingest")
|
|
8
|
-
def test_auth_publish_collection(
|
|
9
|
-
ingest, stac_collection, example_stac_collection, client_authenticated
|
|
10
|
-
):
|
|
11
|
-
token = "token"
|
|
12
|
-
response = client_authenticated.post(
|
|
13
|
-
publish_collections_endpoint,
|
|
14
|
-
headers={"Authorization": f"bearer {token}"},
|
|
15
|
-
json=example_stac_collection,
|
|
16
|
-
)
|
|
17
|
-
ingest.assert_called_once_with(stac_collection)
|
|
18
|
-
assert response.status_code == 201
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def test_unauth_publish_collection(client, example_stac_collection):
|
|
22
|
-
response = client.post(publish_collections_endpoint, json=example_stac_collection)
|
|
23
|
-
assert response.status_code == 403
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
@patch("src.collection.delete")
|
|
27
|
-
def test_auth_delete_collection(delete, example_stac_collection, client_authenticated):
|
|
28
|
-
token = "token"
|
|
29
|
-
response = client_authenticated.delete(
|
|
30
|
-
delete_collection_endpoint.format(collection_id=example_stac_collection["id"]),
|
|
31
|
-
headers={"Authorization": f"bearer {token}"},
|
|
32
|
-
)
|
|
33
|
-
delete.assert_called_once_with(collection_id=example_stac_collection["id"])
|
|
34
|
-
assert response.status_code == 200
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_unauth_delete_collection(client, example_stac_collection):
|
|
38
|
-
response = client.delete(
|
|
39
|
-
delete_collection_endpoint.format(collection_id=example_stac_collection["id"]),
|
|
40
|
-
)
|
|
41
|
-
assert response.status_code == 403
|
|
@@ -1,60 +0,0 @@
|
|
|
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=[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"
|
|
@@ -1,207 +0,0 @@
|
|
|
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
|
-
import pytest
|
|
8
|
-
from fastapi.encoders import jsonable_encoder
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from fastapi.testclient import TestClient
|
|
12
|
-
from src import schemas, services
|
|
13
|
-
|
|
14
|
-
ingestion_endpoint = "/ingestions"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest.fixture()
|
|
18
|
-
def collection_exists():
|
|
19
|
-
with patch("src.validators.collection_exists", return_value=True) as m:
|
|
20
|
-
yield m
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@pytest.fixture()
|
|
24
|
-
def collection_missing():
|
|
25
|
-
def bad_collection(collection_id: str):
|
|
26
|
-
raise ValueError("MOCKED MISSING COLLECTION ERROR")
|
|
27
|
-
|
|
28
|
-
with patch("src.validators.collection_exists", side_effect=bad_collection) as m:
|
|
29
|
-
yield m
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@pytest.fixture()
|
|
33
|
-
def asset_exists():
|
|
34
|
-
with patch("src.validators.url_is_accessible", return_value=True) as m:
|
|
35
|
-
yield m
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@pytest.fixture()
|
|
39
|
-
def asset_missing():
|
|
40
|
-
def bad_asset_url(href: str):
|
|
41
|
-
raise ValueError("MOCKED INACCESSIBLE URL ERROR")
|
|
42
|
-
|
|
43
|
-
with patch("src.validators.url_is_accessible", side_effect=bad_asset_url) as m:
|
|
44
|
-
yield m
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class TestCreate:
|
|
48
|
-
@pytest.fixture(autouse=True)
|
|
49
|
-
def setup(
|
|
50
|
-
self,
|
|
51
|
-
api_client: "TestClient",
|
|
52
|
-
mock_table: "services.Table",
|
|
53
|
-
example_ingestion: "schemas.Ingestion",
|
|
54
|
-
):
|
|
55
|
-
from src import services
|
|
56
|
-
|
|
57
|
-
self.api_client = api_client
|
|
58
|
-
self.mock_table = mock_table
|
|
59
|
-
self.db = services.Database(self.mock_table)
|
|
60
|
-
self.example_ingestion = example_ingestion
|
|
61
|
-
|
|
62
|
-
def test_unauthenticated_create(self):
|
|
63
|
-
response = self.api_client.post(
|
|
64
|
-
ingestion_endpoint,
|
|
65
|
-
json=jsonable_encoder(self.example_ingestion.item),
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
assert response.status_code == 403
|
|
69
|
-
|
|
70
|
-
def test_create(self, client_authenticated, collection_exists, asset_exists):
|
|
71
|
-
response = self.api_client.post(
|
|
72
|
-
ingestion_endpoint,
|
|
73
|
-
json=jsonable_encoder(self.example_ingestion.item),
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
assert response.status_code == 201
|
|
77
|
-
collection_exists.assert_called_once_with(
|
|
78
|
-
collection_id=self.example_ingestion.item.collection
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
stored_data = self.db.fetch_many(status="queued")["items"]
|
|
82
|
-
assert len(stored_data) == 1
|
|
83
|
-
assert json.loads(stored_data[0].json(by_alias=True)) == response.json()
|
|
84
|
-
|
|
85
|
-
def test_validates_missing_collection(
|
|
86
|
-
self, client_authenticated, collection_missing, asset_exists
|
|
87
|
-
):
|
|
88
|
-
response = self.api_client.post(
|
|
89
|
-
ingestion_endpoint,
|
|
90
|
-
json=jsonable_encoder(self.example_ingestion.item),
|
|
91
|
-
)
|
|
92
|
-
collection_missing.assert_called_once_with(
|
|
93
|
-
collection_id=self.example_ingestion.item.collection
|
|
94
|
-
)
|
|
95
|
-
assert response.status_code == 422, "should get validation error"
|
|
96
|
-
assert (
|
|
97
|
-
len(self.db.fetch_many(status="queued")["items"]) == 0
|
|
98
|
-
), "data should not be stored in DB"
|
|
99
|
-
|
|
100
|
-
def test_validates_no_collection_id(self, asset_exists):
|
|
101
|
-
item_sans_id = self.example_ingestion.item.model_copy()
|
|
102
|
-
item_sans_id.collection = None
|
|
103
|
-
|
|
104
|
-
response = self.api_client.post(
|
|
105
|
-
ingestion_endpoint,
|
|
106
|
-
json=item_sans_id.model_dump(mode="json"),
|
|
107
|
-
)
|
|
108
|
-
assert response.status_code == 422, "should get validation error"
|
|
109
|
-
assert (
|
|
110
|
-
len(self.db.fetch_many(status="queued")["items"]) == 0
|
|
111
|
-
), "data should not be stored in DB"
|
|
112
|
-
|
|
113
|
-
def test_validates_missing_assets(
|
|
114
|
-
self, client_authenticated, collection_exists, asset_missing
|
|
115
|
-
):
|
|
116
|
-
response = self.api_client.post(
|
|
117
|
-
ingestion_endpoint,
|
|
118
|
-
json=jsonable_encoder(self.example_ingestion.item),
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
collection_exists.assert_called_once_with(
|
|
122
|
-
collection_id=self.example_ingestion.item.collection
|
|
123
|
-
)
|
|
124
|
-
asset_missing.assert_has_calls(
|
|
125
|
-
[
|
|
126
|
-
call(href=asset.href)
|
|
127
|
-
for asset in self.example_ingestion.item.assets.values()
|
|
128
|
-
],
|
|
129
|
-
any_order=True,
|
|
130
|
-
)
|
|
131
|
-
assert response.status_code == 422, "should get validation error"
|
|
132
|
-
for asset_type in self.example_ingestion.item.assets.keys():
|
|
133
|
-
assert any(
|
|
134
|
-
err["loc"] == ["body", "assets", asset_type, "href"]
|
|
135
|
-
for err in response.json()["detail"]
|
|
136
|
-
), "should reference asset type in validation error response"
|
|
137
|
-
assert (
|
|
138
|
-
len(self.db.fetch_many(status="queued")["items"]) == 0
|
|
139
|
-
), "data should not be stored in DB"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
class TestList:
|
|
143
|
-
@pytest.fixture(autouse=True)
|
|
144
|
-
def setup(
|
|
145
|
-
self,
|
|
146
|
-
api_client: "TestClient",
|
|
147
|
-
mock_table: "services.Table",
|
|
148
|
-
example_ingestion: "schemas.Ingestion",
|
|
149
|
-
):
|
|
150
|
-
self.api_client = api_client
|
|
151
|
-
self.mock_table = mock_table
|
|
152
|
-
self.example_ingestion = example_ingestion
|
|
153
|
-
|
|
154
|
-
def populate_table(self, count=100) -> List["schemas.Ingestion"]:
|
|
155
|
-
example_ingestions = []
|
|
156
|
-
for i in range(count):
|
|
157
|
-
ingestion = self.example_ingestion.model_copy()
|
|
158
|
-
ingestion.id = str(i)
|
|
159
|
-
ingestion.created_at = ingestion.created_at + timedelta(hours=i)
|
|
160
|
-
self.mock_table.put_item(Item=ingestion.dynamodb_dict())
|
|
161
|
-
example_ingestions.append(ingestion)
|
|
162
|
-
return example_ingestions
|
|
163
|
-
|
|
164
|
-
def test_simple_lookup(self):
|
|
165
|
-
self.mock_table.put_item(Item=self.example_ingestion.dynamodb_dict())
|
|
166
|
-
ingestion = jsonable_encoder(self.example_ingestion)
|
|
167
|
-
response = self.api_client.get(ingestion_endpoint)
|
|
168
|
-
assert response.status_code == 200
|
|
169
|
-
assert response.json() == {
|
|
170
|
-
"items": [ingestion],
|
|
171
|
-
"next": None,
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
def test_next_response(self):
|
|
175
|
-
example_ingestions = self.populate_table(100)
|
|
176
|
-
|
|
177
|
-
limit = 25
|
|
178
|
-
expected_next = json.loads(
|
|
179
|
-
example_ingestions[limit - 1].json(
|
|
180
|
-
include={"created_by", "id", "status", "created_at"}
|
|
181
|
-
)
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
response = self.api_client.get(ingestion_endpoint, params={"limit": limit})
|
|
185
|
-
assert response.status_code == 200
|
|
186
|
-
assert json.loads(base64.b64decode(response.json()["next"])) == expected_next
|
|
187
|
-
assert response.json()["items"] == jsonable_encoder(example_ingestions[:limit])
|
|
188
|
-
|
|
189
|
-
@pytest.mark.skip(reason="Test is currently broken")
|
|
190
|
-
def test_get_next_page(self):
|
|
191
|
-
example_ingestions = self.populate_table(100)
|
|
192
|
-
|
|
193
|
-
limit = 25
|
|
194
|
-
next_param = base64.b64encode(
|
|
195
|
-
example_ingestions[limit - 1]
|
|
196
|
-
.json(include={"created_by", "id", "status", "created_at"})
|
|
197
|
-
.encode()
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
response = self.api_client.get(
|
|
201
|
-
ingestion_endpoint, params={"limit": limit, "next": next_param}
|
|
202
|
-
)
|
|
203
|
-
assert response.status_code == 200
|
|
204
|
-
assert response.json()["items"] == [
|
|
205
|
-
json.loads(ingestion.json(by_alias=True))
|
|
206
|
-
for ingestion in example_ingestions[limit : limit * 2]
|
|
207
|
-
]
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
from unittest.mock import Mock, patch
|
|
2
|
-
|
|
3
|
-
import pytest
|
|
4
|
-
from pypgstac.load import Methods
|
|
5
|
-
from src.utils import DbCreds
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@pytest.fixture()
|
|
9
|
-
def loader():
|
|
10
|
-
with patch("src.utils.Loader", autospec=True) as m:
|
|
11
|
-
yield m
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@pytest.fixture()
|
|
15
|
-
def pgstacdb():
|
|
16
|
-
with patch("src.utils.PgstacDB", autospec=True) as m:
|
|
17
|
-
m.return_value.__enter__.return_value = Mock()
|
|
18
|
-
yield m
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@pytest.fixture()
|
|
22
|
-
def dbcreds():
|
|
23
|
-
dbcreds = DbCreds(username="", password="", host="", port=1, dbname="", engine="")
|
|
24
|
-
return dbcreds
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def test_load_items(loader, pgstacdb, example_ingestion, dbcreds):
|
|
28
|
-
import src.utils as utils
|
|
29
|
-
|
|
30
|
-
ingestions = [example_ingestion]
|
|
31
|
-
utils.load_items(dbcreds, ingestions)
|
|
32
|
-
loader.return_value.load_items.assert_called_once_with(
|
|
33
|
-
file=[i.item.model_dump(mode="json") for i in ingestions],
|
|
34
|
-
insert_mode=Methods.upsert,
|
|
35
|
-
)
|