Skip to content

Commit 2f85e8c

Browse files
committed
feat: added load features
1 parent a941571 commit 2f85e8c

File tree

8 files changed

+232
-50
lines changed

8 files changed

+232
-50
lines changed

.github/workflows/main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ jobs:
3232
- name: Check with mypy
3333
run: poetry run mypy .
3434
- name: Run tests
35-
run: poetry run pytest -v
35+
run: poetry run pytest -v --cov=src

poetry.lock

+15-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ mkdocs-autorefs = "^1.2.0"
3737
mkdocs-material-extensions = "^1.3.1"
3838
pytest-cov = "^6.0.0"
3939
pytest-mock = "^3.14.0"
40+
respx = "^0.21.1"
4041

4142

4243
[tool.commitizen]
@@ -76,8 +77,6 @@ exclude = [
7677
module = ["pkg_resources"]
7778
ignore_missing_imports = true
7879

79-
[tool.pytest.ini_options]
80-
addopts = "--cov=src"
8180

8281
[tool.coverage.report]
8382
exclude_also = [

src/eodm/cli/load/apps/stac_api.py

+33-40
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
import typer
88

99
from eodm.cli._globals import DEFAULT_EXTENT
10+
from eodm.cli._serialization import serialize
11+
from eodm.cli._types import Output, OutputType
12+
from eodm.load import load_stac_api_collections, load_stac_api_items
1013

1114
app = typer.Typer(no_args_is_help=True)
1215

13-
HEADERS = {"Content-Type": "application/json"}
14-
1516

1617
@app.callback()
1718
def main():
@@ -40,13 +41,13 @@ def collection(
4041
)
4142

4243
response = httpx.post(
43-
collections_endpoint, json=collection.to_dict(), headers=HEADERS, verify=verify
44+
collections_endpoint, json=collection.to_dict(), headers={}, verify=verify
4445
)
4546

4647
if update and response.status_code == 409:
4748
collection_endpoint = f"{collections_endpoint}/{collection.id}"
4849
response = httpx.put(
49-
collection_endpoint, json=collection.to_dict(), headers=HEADERS, verify=verify
50+
collection_endpoint, json=collection.to_dict(), headers={}, verify=verify
5051
)
5152

5253
response.raise_for_status()
@@ -59,30 +60,26 @@ def collections(
5960
verify: bool = True,
6061
update: bool = False,
6162
skip_existing: bool = False,
63+
output: OutputType = Output.default,
6264
) -> None:
6365
"""
64-
Load multiple collections to a stac API
66+
Load multiple collections to a stac API. Collections can be piped from STDIN or a file
67+
with Collection jsons on each line
6568
"""
6669

67-
collections_endpoint = f"{url}/collections"
68-
for line in collections.readlines():
69-
collection_dict = json.loads(line)
70-
response = httpx.post(
71-
collections_endpoint, json=collection_dict, headers=HEADERS, verify=verify
72-
)
73-
if response.status_code == 409:
74-
if update:
75-
collection_endpoint = f"{collections_endpoint}/{collection_dict['id']}"
76-
response = httpx.put(
77-
collection_endpoint,
78-
json=collection_dict,
79-
headers=HEADERS,
80-
verify=verify,
81-
)
82-
if skip_existing:
83-
continue
84-
85-
response.raise_for_status()
70+
_collections = [
71+
pystac.Collection.from_dict(json.loads(line)) for line in collections.readlines()
72+
]
73+
serialize(
74+
load_stac_api_collections(
75+
url=url,
76+
collections=_collections,
77+
verify=verify,
78+
update=update,
79+
skip_existing=skip_existing,
80+
),
81+
output_type=output,
82+
)
8683

8784

8885
@app.command(no_args_is_help=True)
@@ -92,24 +89,20 @@ def items(
9289
verify: bool = True,
9390
update: bool = False,
9491
skip_existing: bool = False,
92+
output: OutputType = Output.default,
9593
) -> None:
9694
"""
9795
Load multiple items into a STAC API
9896
"""
9997

100-
for line in items.readlines():
101-
item_dict = json.loads(line)
102-
collection_id = item_dict["collection"]
103-
items_endpoint = f"{url}/collections/{collection_id}/items"
104-
response = httpx.post(
105-
items_endpoint, json=item_dict, headers=HEADERS, verify=verify
106-
)
107-
if response.status_code == 409:
108-
if update:
109-
item_endpoint = f"{items_endpoint}/{item_dict['id']}"
110-
response = httpx.put(
111-
item_endpoint, json=item_dict, headers=HEADERS, verify=verify
112-
)
113-
if skip_existing:
114-
continue
115-
response.raise_for_status()
98+
_items = [pystac.Item.from_dict(json.loads(line)) for line in items.readlines()]
99+
serialize(
100+
load_stac_api_items(
101+
url=url,
102+
items=_items,
103+
verify=verify,
104+
update=update,
105+
skip_existing=skip_existing,
106+
),
107+
output_type=output,
108+
)

src/eodm/load.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from typing import Iterable
2+
3+
import httpx
4+
from pystac import Collection, Item
5+
6+
DEFAULT_HEADERS = {"Content-Type": "application/json"}
7+
8+
9+
def load_stac_api_items(
10+
url: str,
11+
items: Iterable[Item],
12+
headers: dict[str, str] | None = None,
13+
verify: bool = True,
14+
update: bool = False,
15+
skip_existing: bool = False,
16+
) -> Iterable[Item]:
17+
"""Load multiple items into a STAC API
18+
19+
Args:
20+
url (str): STAC API url
21+
items (Iterable[Item]): A collection of STAC Items
22+
headers (dict[str, str] | None, optional): Headers to add to the request. Defaults to None.
23+
verify (bool, optional): Verify SSL request. Defaults to True.
24+
update (bool, optional): Update STAC Item with new content. Defaults to False.
25+
skip_existing (bool, optional): Skip Item if exists. Defaults to False.
26+
"""
27+
if not headers:
28+
headers = DEFAULT_HEADERS
29+
30+
for item in items:
31+
collection_id = item.collection_id
32+
items_endpoint = f"{url}/collections/{collection_id}/items"
33+
response = httpx.post(
34+
items_endpoint,
35+
json=item.to_dict(),
36+
headers=headers,
37+
verify=verify,
38+
)
39+
if response.status_code == 409:
40+
if update:
41+
item_endpoint = f"{items_endpoint}/{item.id}"
42+
response = httpx.put(
43+
item_endpoint, json=item.to_dict(), headers=headers, verify=verify
44+
)
45+
if skip_existing:
46+
continue
47+
response.raise_for_status()
48+
yield item
49+
50+
51+
def load_stac_api_collections(
52+
url: str,
53+
collections: Iterable[Collection],
54+
headers: dict[str, str] | None = None,
55+
verify: bool = True,
56+
update: bool = False,
57+
skip_existing: bool = False,
58+
) -> Iterable[Collection]:
59+
"""Load multiple collections to a stac API
60+
61+
Args:
62+
url (str): STAC API URL
63+
collections (Iterable[Collection]): A collection of STAC Collections
64+
headers (dict[str, str] | None, optional): Additional headers to send. Defaults to None.
65+
verify (bool, optional): Verify TLS request. Defaults to True.
66+
update (bool, optional): Update the destination Collections. Defaults to False.
67+
skip_existing (bool, optional): Skip existing Collections. Defaults to False.
68+
69+
Returns:
70+
Iterable[Collection]:
71+
"""
72+
73+
if not headers:
74+
headers = DEFAULT_HEADERS
75+
76+
collections_endpoint = f"{url}/collections"
77+
for collection in collections:
78+
response = httpx.post(
79+
collections_endpoint,
80+
json=collection.to_dict(),
81+
headers=headers,
82+
verify=verify,
83+
)
84+
if response.status_code == 409:
85+
if update:
86+
collection_endpoint = f"{collections_endpoint}/{collection.id}"
87+
response = httpx.put(
88+
collection_endpoint,
89+
json=collection.to_dict(),
90+
headers=headers,
91+
verify=verify,
92+
)
93+
if skip_existing:
94+
continue
95+
96+
response.raise_for_status()
97+
yield collection

tests/conftest.py

+47-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,60 @@
1+
import json
2+
from pathlib import Path
3+
14
import pytest
5+
import respx
6+
from httpx import Response
27
from pytest_mock import MockerFixture
38
from test_data import STAC_COLLECTIONS, STAC_ITEMS
49

510

611
@pytest.fixture()
7-
def mock_stac_collections(mocker: MockerFixture):
12+
def mock_extract_stac_collections(mocker: MockerFixture):
813
client_mock = mocker.patch("eodm.extract.pystac_client.Client")
914
client_mock.open().get_collections().__iter__.return_value = STAC_COLLECTIONS
1015

1116

1217
@pytest.fixture()
13-
def mock_stac_items(mocker: MockerFixture):
18+
def mock_extract_stac_items(mocker: MockerFixture):
1419
client_mock = mocker.patch("eodm.extract.pystac_client.Client")
1520
client_mock.open().search().item_collection().__iter__.return_value = STAC_ITEMS
21+
22+
23+
@pytest.fixture()
24+
def stac_collections(tmp_path: Path):
25+
collection_json_path = tmp_path / "collections"
26+
27+
with collection_json_path.open("a", encoding="utf-8", newline="\n") as f:
28+
for collection in STAC_COLLECTIONS:
29+
f.write(json.dumps(collection.to_dict()))
30+
f.write("\n")
31+
32+
return str(collection_json_path)
33+
34+
35+
@pytest.fixture()
36+
def stac_items(tmp_path):
37+
item_json_path = tmp_path / "items"
38+
39+
with item_json_path.open("a", encoding="utf-8", newline="\n") as f:
40+
for item in STAC_ITEMS:
41+
f.write(json.dumps(item.to_dict()))
42+
f.write("\n")
43+
44+
return str(item_json_path)
45+
46+
47+
@pytest.fixture()
48+
def mock_post_stac_api_items_endpoint(respx_mock: respx.MockRouter):
49+
post_mock = respx_mock.post(
50+
"https://example.com/stac-api/collections/sentinel-2-l2a/items"
51+
).mock(return_value=Response(204))
52+
return post_mock
53+
54+
55+
@pytest.fixture()
56+
def mock_post_stac_api_collections_endpoint(respx_mock: respx.MockRouter):
57+
post_mock = respx_mock.post("https://example.com/stac-api/collections").mock(
58+
return_value=Response(204)
59+
)
60+
return post_mock

tests/test_cli.py

+36-2
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,50 @@
44
runner = CliRunner()
55

66

7-
def test_extract_stacapi_items(mock_stac_items):
7+
def test_extract_stacapi_items(mock_extract_stac_items):
88
result = runner.invoke(app, ["extract", "stac-api", "items", "sample", "sample"])
99
assert result.exit_code == 0
1010
assert "Feature" in result.stdout
1111

1212

13-
def test_extract_stacapi_collections(mock_stac_collections):
13+
def test_extract_stacapi_collections(mock_extract_stac_collections):
1414
result = runner.invoke(
1515
app,
1616
["extract", "stac-api", "collections", "sample"],
1717
)
1818
assert result.exit_code == 0
1919
assert "Collection" in result.stdout
20+
21+
22+
def test_load_stacapi_items(stac_items, mock_post_stac_api_items_endpoint):
23+
result = runner.invoke(
24+
app,
25+
[
26+
"load",
27+
"stac-api",
28+
"items",
29+
"https://example.com/stac-api",
30+
stac_items,
31+
],
32+
)
33+
assert mock_post_stac_api_items_endpoint.called
34+
assert result.exit_code == 0
35+
assert "Feature" in result.stdout
36+
37+
38+
def test_load_stacapi_collections(
39+
stac_collections, mock_post_stac_api_collections_endpoint
40+
):
41+
result = runner.invoke(
42+
app,
43+
[
44+
"load",
45+
"stac-api",
46+
"collections",
47+
"https://example.com/stac-api",
48+
stac_collections,
49+
],
50+
)
51+
assert mock_post_stac_api_collections_endpoint.called
52+
assert result.exit_code == 0
53+
assert "Collection" in result.stdout

tests/test_data.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pystac import Collection, Item
22

3-
STAC_ITEMS = (
3+
STAC_ITEMS = list(
44
Item.from_dict(item)
55
for item in [
66
{
@@ -2901,7 +2901,7 @@
29012901
]
29022902
)
29032903

2904-
STAC_COLLECTIONS = (
2904+
STAC_COLLECTIONS = list(
29052905
Collection.from_dict(collection)
29062906
for collection in [
29072907
{

0 commit comments

Comments
 (0)