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,148 @@
1
+ import base64
2
+ import binascii
3
+ import enum
4
+ import json
5
+ from datetime import datetime
6
+ from typing import TYPE_CHECKING, Dict, List, Optional, Union
7
+ from urllib.parse import urlparse
8
+
9
+ from fastapi.encoders import jsonable_encoder
10
+ from fastapi.exceptions import RequestValidationError
11
+ from pydantic import (
12
+ BaseModel,
13
+ Json,
14
+ PositiveInt,
15
+ dataclasses,
16
+ error_wrappers,
17
+ validator,
18
+ )
19
+ from stac_pydantic import Collection, Item, shared
20
+
21
+ from . import validators
22
+
23
+ if TYPE_CHECKING:
24
+ from . import services
25
+
26
+
27
+ class AccessibleAsset(shared.Asset):
28
+ @validator("href")
29
+ def is_accessible(cls, href):
30
+ url = urlparse(href)
31
+
32
+ if url.scheme in ["https", "http"]:
33
+ validators.url_is_accessible(href=href)
34
+ elif url.scheme in ["s3"]:
35
+ validators.s3_object_is_accessible(
36
+ bucket=url.hostname, key=url.path.lstrip("/")
37
+ )
38
+ else:
39
+ ValueError(f"Unsupported scheme: {url.scheme}")
40
+
41
+ return href
42
+
43
+
44
+ class AccessibleItem(Item):
45
+ assets: Dict[str, AccessibleAsset]
46
+
47
+ @validator("collection")
48
+ def exists(cls, collection):
49
+ validators.collection_exists(collection_id=collection)
50
+ return collection
51
+
52
+
53
+ class StacCollection(Collection):
54
+ id: str
55
+ item_assets: Dict
56
+
57
+
58
+ class Status(str, enum.Enum):
59
+ started = "started"
60
+ queued = "queued"
61
+ failed = "failed"
62
+ succeeded = "succeeded"
63
+ cancelled = "cancelled"
64
+
65
+
66
+ class Ingestion(BaseModel):
67
+ id: str
68
+ status: Status
69
+ message: Optional[str]
70
+ created_by: str
71
+ created_at: datetime = None
72
+ updated_at: datetime = None
73
+
74
+ item: Union[Item, Json[Item]]
75
+
76
+ @validator("created_at", pre=True, always=True, allow_reuse=True)
77
+ @validator("updated_at", pre=True, always=True, allow_reuse=True)
78
+ def set_ts_now(cls, v):
79
+ return v or datetime.now()
80
+
81
+ def enqueue(self, db: "services.Database"):
82
+ self.status = Status.queued
83
+ return self.save(db)
84
+
85
+ def cancel(self, db: "services.Database"):
86
+ self.status = Status.cancelled
87
+ return self.save(db)
88
+
89
+ def save(self, db: "services.Database"):
90
+ self.updated_at = datetime.now()
91
+ db.write(self)
92
+ return self
93
+
94
+ def dynamodb_dict(self):
95
+ """DynamoDB-friendly serialization"""
96
+ # convert to dictionary
97
+ output = self.dict(exclude={"item"})
98
+
99
+ # add STAC item as string
100
+ output["item"] = self.item.json()
101
+
102
+ # make JSON-friendly (will be able to do with Pydantic V2, https://github.com/pydantic/pydantic/issues/1409#issuecomment-1423995424)
103
+ return jsonable_encoder(output)
104
+
105
+
106
+ @dataclasses.dataclass
107
+ class ListIngestionRequest:
108
+ status: Status = Status.queued
109
+ limit: PositiveInt = None
110
+ next: Optional[str] = None
111
+
112
+ def __post_init_post_parse__(self) -> None:
113
+ # https://github.com/tiangolo/fastapi/issues/1474#issuecomment-1049987786
114
+ if self.next is None:
115
+ return
116
+
117
+ try:
118
+ self.next = json.loads(base64.b64decode(self.next))
119
+ except (UnicodeDecodeError, binascii.Error):
120
+ raise RequestValidationError(
121
+ [
122
+ error_wrappers.ErrorWrapper(
123
+ ValueError(
124
+ "Unable to decode next token. Should be base64 encoded JSON"
125
+ ),
126
+ "query.next",
127
+ )
128
+ ]
129
+ )
130
+
131
+
132
+ class ListIngestionResponse(BaseModel):
133
+ items: List[Ingestion]
134
+ next: Optional[str]
135
+
136
+ @validator("next", pre=True)
137
+ def b64_encode_next(cls, next):
138
+ """
139
+ Base64 encode next parameter for easier transportability
140
+ """
141
+ if isinstance(next, dict):
142
+ return base64.b64encode(json.dumps(next).encode())
143
+ return next
144
+
145
+
146
+ class UpdateIngestionRequest(BaseModel):
147
+ status: Status = None
148
+ message: str = None
@@ -0,0 +1,44 @@
1
+ from typing import TYPE_CHECKING, List
2
+
3
+ from boto3.dynamodb import conditions
4
+ from pydantic import parse_obj_as
5
+
6
+ from . import schemas
7
+
8
+ if TYPE_CHECKING:
9
+ from mypy_boto3_dynamodb.service_resource import Table
10
+
11
+
12
+ class Database:
13
+ def __init__(self, table: "Table"):
14
+ self.table = table
15
+
16
+ def write(self, ingestion: schemas.Ingestion):
17
+ self.table.put_item(Item=ingestion.dynamodb_dict())
18
+
19
+ def fetch_one(self, username: str, ingestion_id: str):
20
+ response = self.table.get_item(
21
+ Key={"created_by": username, "id": ingestion_id},
22
+ )
23
+ try:
24
+ return schemas.Ingestion.parse_obj(response["Item"])
25
+ except KeyError:
26
+ raise NotInDb("Record not found")
27
+
28
+ def fetch_many(
29
+ self, status: str, next: dict = None, limit: int = None
30
+ ) -> schemas.ListIngestionResponse:
31
+ response = self.table.query(
32
+ IndexName="status",
33
+ KeyConditionExpression=conditions.Key("status").eq(status),
34
+ **{"Limit": limit} if limit else {},
35
+ **{"ExclusiveStartKey": next} if next else {},
36
+ )
37
+ return {
38
+ "items": parse_obj_as(List[schemas.Ingestion], response["Items"]),
39
+ "next": response.get("LastEvaluatedKey"),
40
+ }
41
+
42
+
43
+ class NotInDb(Exception):
44
+ ...
@@ -0,0 +1,52 @@
1
+ from typing import Sequence
2
+
3
+ import boto3
4
+ import pydantic
5
+ from pypgstac.db import PgstacDB
6
+ from pypgstac.load import Methods
7
+ from fastapi.encoders import jsonable_encoder
8
+
9
+ from .loader import Loader
10
+ from .schemas import Ingestion
11
+
12
+
13
+ class DbCreds(pydantic.BaseModel):
14
+ username: str
15
+ password: str
16
+ host: str
17
+ port: int
18
+ dbname: str
19
+ engine: str
20
+
21
+ @property
22
+ def dsn_string(self) -> str:
23
+ return f"{self.engine}://{self.username}:{self.password}@{self.host}:{self.port}/{self.dbname}" # noqa
24
+
25
+
26
+ def get_db_credentials(secret_arn: str) -> DbCreds:
27
+ """
28
+ Load pgSTAC database credentials from AWS Secrets Manager.
29
+ """
30
+ print("Fetching DB credentials...")
31
+ session = boto3.session.Session(region_name=secret_arn.split(":")[3])
32
+ client = session.client(service_name="secretsmanager")
33
+ response = client.get_secret_value(SecretId=secret_arn)
34
+ return DbCreds.parse_raw(response["SecretString"])
35
+
36
+
37
+ def load_items(creds: DbCreds, ingestions: Sequence[Ingestion]):
38
+ """
39
+ Bulk insert STAC records into pgSTAC.
40
+ """
41
+ with PgstacDB(dsn=creds.dsn_string, debug=True) as db:
42
+ loader = Loader(db=db)
43
+
44
+ # serialize to JSON-friendly dicts (won't be necessary in Pydantic v2, https://github.com/pydantic/pydantic/issues/1409#issuecomment-1423995424)
45
+ items = jsonable_encoder(i.item for i in ingestions)
46
+ loading_result = loader.load_items(
47
+ file=items,
48
+ # use insert_ignore to avoid overwritting existing items or upsert to replace
49
+ insert_mode=Methods.upsert,
50
+ )
51
+
52
+ return loading_result
@@ -0,0 +1,72 @@
1
+ import functools
2
+
3
+ import boto3
4
+ import requests
5
+
6
+
7
+ @functools.cache
8
+ def get_s3_credentials():
9
+ from .config import settings
10
+
11
+ print("Fetching S3 Credentials...")
12
+
13
+ response = boto3.client("sts").assume_role(
14
+ RoleArn=settings.data_access_role,
15
+ RoleSessionName="stac-ingestor-data-validation",
16
+ )
17
+ return {
18
+ "aws_access_key_id": response["Credentials"]["AccessKeyId"],
19
+ "aws_secret_access_key": response["Credentials"]["SecretAccessKey"],
20
+ "aws_session_token": response["Credentials"]["SessionToken"],
21
+ }
22
+
23
+
24
+ def s3_object_is_accessible(bucket: str, key: str):
25
+ """
26
+ Ensure we can send HEAD requests to S3 objects.
27
+ """
28
+ from .config import settings
29
+
30
+ client = boto3.client("s3", **get_s3_credentials())
31
+ try:
32
+ client.head_object(
33
+ Bucket=bucket,
34
+ Key=key,
35
+ **{"RequestPayer": "requester"} if settings.requester_pays else {},
36
+ )
37
+ except client.exceptions.ClientError as e:
38
+ raise ValueError(
39
+ f"Asset not accessible: {e.__dict__['response']['Error']['Message']}"
40
+ )
41
+
42
+
43
+ def url_is_accessible(href: str):
44
+ """
45
+ Ensure URLs are accessible via HEAD requests.
46
+ """
47
+ try:
48
+ requests.head(href).raise_for_status()
49
+ except requests.exceptions.HTTPError as e:
50
+ raise ValueError(
51
+ f"Asset not accessible: {e.response.status_code} {e.response.reason}"
52
+ )
53
+
54
+
55
+ @functools.cache
56
+ def collection_exists(collection_id: str) -> bool:
57
+ """
58
+ Ensure collection exists in STAC
59
+ """
60
+ from .config import settings
61
+
62
+ url = "/".join(
63
+ f'{url.strip("/")}' for url in [settings.stac_url, "collections", collection_id]
64
+ )
65
+
66
+ if (response := requests.get(url)).ok:
67
+ return True
68
+
69
+ raise ValueError(
70
+ f"Invalid collection '{collection_id}', received "
71
+ f"{response.status_code} response code from STAC API"
72
+ )
File without changes
@@ -0,0 +1,271 @@
1
+ import os
2
+
3
+ import boto3
4
+ import pytest
5
+ from fastapi.testclient import TestClient
6
+ from moto import mock_dynamodb, mock_ssm
7
+ from stac_pydantic import Item
8
+
9
+
10
+ @pytest.fixture
11
+ def test_environ():
12
+ # Mocked AWS Credentials for moto (best practice recommendation from moto)
13
+ os.environ["AWS_ACCESS_KEY_ID"] = "testing"
14
+ os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
15
+ os.environ["AWS_SECURITY_TOKEN"] = "testing"
16
+ os.environ["AWS_SESSION_TOKEN"] = "testing"
17
+
18
+ # Config mocks
19
+ os.environ["DYNAMODB_TABLE"] = "test_table"
20
+ os.environ["JWKS_URL"] = "https://test-jwks.url"
21
+ os.environ["STAC_URL"] = "https://test-stac.url"
22
+ os.environ["DATA_ACCESS_ROLE"] = "arn:aws:iam::123456789012:role/test-role"
23
+ os.environ["DB_SECRET_ARN"] = "testing"
24
+
25
+
26
+ @pytest.fixture
27
+ def mock_ssm_parameter_store():
28
+ with mock_ssm():
29
+ yield boto3.client("ssm")
30
+
31
+
32
+ @pytest.fixture
33
+ def app(test_environ, mock_ssm_parameter_store):
34
+ from src.main import app
35
+
36
+ return app
37
+
38
+
39
+ @pytest.fixture
40
+ def api_client(app):
41
+ return TestClient(app)
42
+
43
+
44
+ @pytest.fixture
45
+ def mock_table(app, test_environ):
46
+ from src import dependencies
47
+ from src.config import settings
48
+
49
+ with mock_dynamodb():
50
+ client = boto3.resource("dynamodb")
51
+ mock_table = client.create_table(
52
+ TableName=settings.dynamodb_table,
53
+ AttributeDefinitions=[
54
+ {"AttributeName": "created_by", "AttributeType": "S"},
55
+ {"AttributeName": "id", "AttributeType": "S"},
56
+ {"AttributeName": "status", "AttributeType": "S"},
57
+ {"AttributeName": "created_at", "AttributeType": "S"},
58
+ ],
59
+ KeySchema=[
60
+ {"AttributeName": "created_by", "KeyType": "HASH"},
61
+ {"AttributeName": "id", "KeyType": "RANGE"},
62
+ ],
63
+ BillingMode="PAY_PER_REQUEST",
64
+ GlobalSecondaryIndexes=[
65
+ {
66
+ "IndexName": "status",
67
+ "KeySchema": [
68
+ {"AttributeName": "status", "KeyType": "HASH"},
69
+ {"AttributeName": "created_at", "KeyType": "RANGE"},
70
+ ],
71
+ "Projection": {"ProjectionType": "ALL"},
72
+ }
73
+ ],
74
+ )
75
+ app.dependency_overrides[dependencies.get_table] = lambda: mock_table
76
+ yield mock_table
77
+ app.dependency_overrides.pop(dependencies.get_table)
78
+
79
+
80
+ @pytest.fixture
81
+ def example_stac_item():
82
+ return {
83
+ "stac_version": "1.0.0",
84
+ "stac_extensions": [],
85
+ "type": "Feature",
86
+ "id": "20201211_223832_CS2",
87
+ "bbox": [
88
+ 172.91173669923782,
89
+ 1.3438851951615003,
90
+ 172.95469614953714,
91
+ 1.3690476620161975,
92
+ ],
93
+ "geometry": {
94
+ "type": "Polygon",
95
+ "coordinates": [
96
+ [
97
+ [172.91173669923782, 1.3438851951615003],
98
+ [172.95469614953714, 1.3438851951615003],
99
+ [172.95469614953714, 1.3690476620161975],
100
+ [172.91173669923782, 1.3690476620161975],
101
+ [172.91173669923782, 1.3438851951615003],
102
+ ]
103
+ ],
104
+ },
105
+ "properties": {
106
+ "datetime": "2020-12-11T22:38:32.125000Z",
107
+ "eo:cloud_cover": 1,
108
+ },
109
+ "collection": "simple-collection",
110
+ "links": [
111
+ {
112
+ "rel": "collection",
113
+ "href": "./collection.json",
114
+ "type": "application/json",
115
+ "title": "Simple Example Collection",
116
+ },
117
+ {
118
+ "rel": "root",
119
+ "href": "./collection.json",
120
+ "type": "application/json",
121
+ "title": "Simple Example Collection",
122
+ },
123
+ {
124
+ "rel": "parent",
125
+ "href": "./collection.json",
126
+ "type": "application/json",
127
+ "title": "Simple Example Collection",
128
+ },
129
+ ],
130
+ "assets": {
131
+ "visual": {
132
+ "href": "https://TEST_API.com/open-cogs/stac-examples/20201211_223832_CS2.tif", # noqa
133
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
134
+ "title": "3-Band Visual",
135
+ "roles": ["visual"],
136
+ },
137
+ "thumbnail": {
138
+ "href": "https://TEST_API.com/open-cogs/stac-examples/20201211_223832_CS2.jpg", # noqa
139
+ "title": "Thumbnail",
140
+ "type": "image/jpeg",
141
+ "roles": ["thumbnail"],
142
+ },
143
+ },
144
+ }
145
+
146
+
147
+ @pytest.fixture
148
+ def example_stac_collection():
149
+ return {
150
+ "id": "simple-collection",
151
+ "type": "Collection",
152
+ "stac_extensions": [
153
+ "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
154
+ "https://stac-extensions.github.io/projection/v1.0.0/schema.json",
155
+ "https://stac-extensions.github.io/view/v1.0.0/schema.json",
156
+ ],
157
+ "item_assets": {
158
+ "data": {
159
+ "type": "image/tiff; application=geotiff; profile=cloud-optimized",
160
+ "roles": ["data"],
161
+ }
162
+ },
163
+ "stac_version": "1.0.0",
164
+ "description": "A simple collection demonstrating core catalog fields with links to a couple of items",
165
+ "title": "Simple Example Collection",
166
+ "providers": [
167
+ {
168
+ "name": "Remote Data, Inc",
169
+ "description": "Producers of awesome spatiotemporal assets",
170
+ "roles": ["producer", "processor"],
171
+ "url": "http://remotedata.io",
172
+ }
173
+ ],
174
+ "extent": {
175
+ "spatial": {
176
+ "bbox": [
177
+ [
178
+ 172.91173669923782,
179
+ 1.3438851951615003,
180
+ 172.95469614953714,
181
+ 1.3690476620161975,
182
+ ]
183
+ ]
184
+ },
185
+ "temporal": {
186
+ "interval": [["2020-12-11T22:38:32.125Z", "2020-12-14T18:02:31.437Z"]]
187
+ },
188
+ },
189
+ "license": "CC-BY-4.0",
190
+ "summaries": {
191
+ "platform": ["cool_sat1", "cool_sat2"],
192
+ "constellation": ["ion"],
193
+ "instruments": ["cool_sensor_v1", "cool_sensor_v2"],
194
+ "gsd": {"minimum": 0.512, "maximum": 0.66},
195
+ "eo:cloud_cover": {"minimum": 1.2, "maximum": 1.2},
196
+ "proj:epsg": {"minimum": 32659, "maximum": 32659},
197
+ "view:sun_elevation": {"minimum": 54.9, "maximum": 54.9},
198
+ "view:off_nadir": {"minimum": 3.8, "maximum": 3.8},
199
+ "view:sun_azimuth": {"minimum": 135.7, "maximum": 135.7},
200
+ },
201
+ "links": [
202
+ {
203
+ "rel": "root",
204
+ "href": "./collection.json",
205
+ "type": "application/json",
206
+ "title": "Simple Example Collection",
207
+ },
208
+ {
209
+ "rel": "item",
210
+ "href": "./simple-item.json",
211
+ "type": "application/geo+json",
212
+ "title": "Simple Item",
213
+ },
214
+ {
215
+ "rel": "item",
216
+ "href": "./core-item.json",
217
+ "type": "application/geo+json",
218
+ "title": "Core Item",
219
+ },
220
+ {
221
+ "rel": "item",
222
+ "href": "./extended-item.json",
223
+ "type": "application/geo+json",
224
+ "title": "Extended Item",
225
+ },
226
+ {
227
+ "rel": "self",
228
+ "href": "https://raw.githubusercontent.com/radiantearth/stac-spec/v1.0.0/examples/collection.json",
229
+ "type": "application/json",
230
+ },
231
+ ],
232
+ }
233
+
234
+
235
+ @pytest.fixture
236
+ def client(app):
237
+ """
238
+ Return an API Client
239
+ """
240
+ app.dependency_overrides = {}
241
+ return TestClient(app)
242
+
243
+
244
+ @pytest.fixture
245
+ def client_authenticated(app):
246
+ """
247
+ Returns an API client which skips the authentication
248
+ """
249
+ from src.dependencies import get_username
250
+
251
+ app.dependency_overrides[get_username] = lambda: "test_user"
252
+ return TestClient(app)
253
+
254
+
255
+ @pytest.fixture
256
+ def stac_collection(example_stac_collection):
257
+ from src import schemas
258
+
259
+ return schemas.StacCollection(**example_stac_collection)
260
+
261
+
262
+ @pytest.fixture
263
+ def example_ingestion(example_stac_item):
264
+ from src import schemas
265
+
266
+ return schemas.Ingestion(
267
+ id=example_stac_item["id"],
268
+ created_by="test-user",
269
+ status=schemas.Status.queued,
270
+ item=Item.parse_obj(example_stac_item),
271
+ )
@@ -0,0 +1,35 @@
1
+ from unittest.mock import Mock, patch
2
+ import pytest
3
+ from pypgstac.load import Methods
4
+ from src.utils import DbCreds
5
+ import src.collection as collection
6
+ import os
7
+
8
+
9
+ @pytest.fixture()
10
+ def loader():
11
+ with patch("src.collection.Loader", autospec=True) as m:
12
+ yield m
13
+
14
+
15
+ @pytest.fixture()
16
+ def pgstacdb():
17
+ with patch("src.collection.PgstacDB", autospec=True) as m:
18
+ m.return_value.__enter__.return_value = Mock()
19
+ yield m
20
+
21
+
22
+ def test_load_collections(stac_collection, loader, pgstacdb):
23
+ with patch(
24
+ "src.collection.get_db_credentials",
25
+ return_value=DbCreds(
26
+ username="", password="", host="", port=1, dbname="", engine=""
27
+ ),
28
+ ):
29
+ os.environ["DB_SECRET_ARN"] = ""
30
+ collection.ingest(stac_collection)
31
+
32
+ loader.return_value.load_collections.assert_called_once_with(
33
+ file=[stac_collection.to_dict()],
34
+ insert_mode=Methods.upsert,
35
+ )
@@ -0,0 +1,41 @@
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