sfeos-tools 0.1.0__tar.gz → 0.1.1__tar.gz
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.
- {sfeos_tools-0.1.0/sfeos_tools.egg-info → sfeos_tools-0.1.1}/PKG-INFO +24 -6
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1}/README.md +16 -0
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1}/pyproject.toml +13 -7
- sfeos_tools-0.1.1/sfeos_tools/bbox_shape.py +114 -0
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1}/sfeos_tools/cli.py +1 -108
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1/sfeos_tools.egg-info}/PKG-INFO +24 -6
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1}/sfeos_tools.egg-info/SOURCES.txt +3 -1
- sfeos_tools-0.1.1/sfeos_tools.egg-info/requires.txt +18 -0
- sfeos_tools-0.1.1/tests/test_bbox_shape.py +275 -0
- sfeos_tools-0.1.0/sfeos_tools.egg-info/requires.txt +0 -17
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1}/LICENSE +0 -0
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1}/setup.cfg +0 -0
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1}/sfeos_tools/__init__.py +0 -0
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1}/sfeos_tools/reindex.py +0 -0
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1}/sfeos_tools.egg-info/dependency_links.txt +0 -0
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1}/sfeos_tools.egg-info/entry_points.txt +0 -0
- {sfeos_tools-0.1.0 → sfeos_tools-0.1.1}/sfeos_tools.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: sfeos-tools
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.1
|
4
4
|
Summary: CLI tools for managing stac-fastapi-elasticsearch-opensearch deployments
|
5
5
|
Author: CloudFerro S.A.
|
6
6
|
Author-email: Jonathan Healy <jon@healy-hypersaptial.dev>
|
@@ -35,11 +35,12 @@ Classifier: Development Status :: 4 - Beta
|
|
35
35
|
Classifier: Intended Audience :: Developers
|
36
36
|
Classifier: Intended Audience :: Science/Research
|
37
37
|
Classifier: License :: OSI Approved :: MIT License
|
38
|
-
Classifier: Programming Language :: Python :: 3.8
|
39
38
|
Classifier: Programming Language :: Python :: 3.9
|
40
39
|
Classifier: Programming Language :: Python :: 3.10
|
41
40
|
Classifier: Programming Language :: Python :: 3.11
|
42
41
|
Classifier: Programming Language :: Python :: 3.12
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
43
|
+
Classifier: Programming Language :: Python :: 3.14
|
43
44
|
Classifier: Topic :: Scientific/Engineering
|
44
45
|
Classifier: Topic :: Scientific/Engineering :: GIS
|
45
46
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
@@ -48,13 +49,14 @@ Description-Content-Type: text/markdown
|
|
48
49
|
License-File: LICENSE
|
49
50
|
Requires-Dist: click>=8.0.0
|
50
51
|
Provides-Extra: elasticsearch
|
51
|
-
Requires-Dist: stac-fastapi-elasticsearch; extra == "elasticsearch"
|
52
|
+
Requires-Dist: stac-fastapi-elasticsearch>=6.6.0; extra == "elasticsearch"
|
52
53
|
Provides-Extra: opensearch
|
53
|
-
Requires-Dist: stac-fastapi-opensearch; extra == "opensearch"
|
54
|
+
Requires-Dist: stac-fastapi-opensearch>=6.6.0; extra == "opensearch"
|
54
55
|
Provides-Extra: dev
|
55
|
-
Requires-Dist: stac-fastapi-elasticsearch; extra == "dev"
|
56
|
-
Requires-Dist: stac-fastapi-opensearch; extra == "dev"
|
56
|
+
Requires-Dist: stac-fastapi-elasticsearch>=6.6.0; extra == "dev"
|
57
|
+
Requires-Dist: stac-fastapi-opensearch>=6.6.0; extra == "dev"
|
57
58
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
59
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
58
60
|
Requires-Dist: pytest-cov>=2.0; extra == "dev"
|
59
61
|
Requires-Dist: black>=21.0; extra == "dev"
|
60
62
|
Requires-Dist: isort>=5.0; extra == "dev"
|
@@ -66,6 +68,22 @@ Dynamic: license-file
|
|
66
68
|
|
67
69
|
CLI tools for managing [stac-fastapi-elasticsearch-opensearch](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch) deployments.
|
68
70
|
|
71
|
+
<!-- markdownlint-disable MD033 MD041 -->
|
72
|
+
|
73
|
+
|
74
|
+
<p align="left">
|
75
|
+
<img src="https://raw.githubusercontent.com/stac-utils/stac-fastapi-elasticsearch-opensearch/refs/heads/main/assets/sfeos.png" width=1000>
|
76
|
+
</p>
|
77
|
+
|
78
|
+
<!-- **Jump to:** [Project Introduction](#project-introduction---what-is-sfeos) | [Quick Start](#quick-start) | [Table of Contents](#table-of-contents) -->
|
79
|
+
|
80
|
+
[](https://pepy.tech/project/sfeos-tools)
|
81
|
+
[](https://github.com/healy-hyperspatial/sfeos-tools/graphs/contributors)
|
82
|
+
[](https://github.com/healy-hyperspatial/sfeos-tools/stargazers)
|
83
|
+
[](https://github.com/healy-hyperspatial/sfeos-tools/network/members)
|
84
|
+
[](https://pypi.org/project/sfeos-tools/)
|
85
|
+
[](https://github.com/radiantearth/stac-spec/tree/v1.1.0)
|
86
|
+
|
69
87
|
## Table of Contents
|
70
88
|
|
71
89
|
- [Installation](#installation)
|
@@ -2,6 +2,22 @@
|
|
2
2
|
|
3
3
|
CLI tools for managing [stac-fastapi-elasticsearch-opensearch](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch) deployments.
|
4
4
|
|
5
|
+
<!-- markdownlint-disable MD033 MD041 -->
|
6
|
+
|
7
|
+
|
8
|
+
<p align="left">
|
9
|
+
<img src="https://raw.githubusercontent.com/stac-utils/stac-fastapi-elasticsearch-opensearch/refs/heads/main/assets/sfeos.png" width=1000>
|
10
|
+
</p>
|
11
|
+
|
12
|
+
<!-- **Jump to:** [Project Introduction](#project-introduction---what-is-sfeos) | [Quick Start](#quick-start) | [Table of Contents](#table-of-contents) -->
|
13
|
+
|
14
|
+
[](https://pepy.tech/project/sfeos-tools)
|
15
|
+
[](https://github.com/healy-hyperspatial/sfeos-tools/graphs/contributors)
|
16
|
+
[](https://github.com/healy-hyperspatial/sfeos-tools/stargazers)
|
17
|
+
[](https://github.com/healy-hyperspatial/sfeos-tools/network/members)
|
18
|
+
[](https://pypi.org/project/sfeos-tools/)
|
19
|
+
[](https://github.com/radiantearth/stac-spec/tree/v1.1.0)
|
20
|
+
|
5
21
|
## Table of Contents
|
6
22
|
|
7
23
|
- [Installation](#installation)
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "sfeos-tools"
|
7
|
-
version = "0.1.
|
7
|
+
version = "0.1.1"
|
8
8
|
description = "CLI tools for managing stac-fastapi-elasticsearch-opensearch deployments"
|
9
9
|
readme = "README.md"
|
10
10
|
authors = [
|
@@ -21,11 +21,12 @@ classifiers = [
|
|
21
21
|
"Intended Audience :: Developers",
|
22
22
|
"Intended Audience :: Science/Research",
|
23
23
|
"License :: OSI Approved :: MIT License",
|
24
|
-
"Programming Language :: Python :: 3.8",
|
25
24
|
"Programming Language :: Python :: 3.9",
|
26
25
|
"Programming Language :: Python :: 3.10",
|
27
26
|
"Programming Language :: Python :: 3.11",
|
28
27
|
"Programming Language :: Python :: 3.12",
|
28
|
+
"Programming Language :: Python :: 3.13",
|
29
|
+
"Programming Language :: Python :: 3.14",
|
29
30
|
"Topic :: Scientific/Engineering",
|
30
31
|
"Topic :: Scientific/Engineering :: GIS",
|
31
32
|
"Topic :: Software Development :: Libraries :: Python Modules"
|
@@ -42,16 +43,17 @@ Source = "https://github.com/Healy-Hyperspatial/sfeos-tools"
|
|
42
43
|
|
43
44
|
[project.optional-dependencies]
|
44
45
|
elasticsearch = [
|
45
|
-
"stac-fastapi-elasticsearch",
|
46
|
+
"stac-fastapi-elasticsearch>=6.6.0",
|
46
47
|
]
|
47
48
|
opensearch = [
|
48
|
-
"stac-fastapi-opensearch",
|
49
|
+
"stac-fastapi-opensearch>=6.6.0",
|
49
50
|
]
|
50
51
|
dev = [
|
51
|
-
"stac-fastapi-elasticsearch",
|
52
|
-
"stac-fastapi-opensearch",
|
52
|
+
"stac-fastapi-elasticsearch>=6.6.0",
|
53
|
+
"stac-fastapi-opensearch>=6.6.0",
|
53
54
|
# Development tools
|
54
55
|
"pytest>=6.0",
|
56
|
+
"pytest-asyncio>=0.21.0",
|
55
57
|
"pytest-cov>=2.0",
|
56
58
|
"black>=21.0",
|
57
59
|
"isort>=5.0",
|
@@ -79,7 +81,11 @@ line_length = 88
|
|
79
81
|
[tool.pytest.ini_options]
|
80
82
|
testpaths = ["tests"]
|
81
83
|
python_files = "test_*.py"
|
82
|
-
addopts = "-v --cov=
|
84
|
+
addopts = "-v --cov=sfeos_tools --cov-report=term-missing"
|
85
|
+
asyncio_mode = "auto"
|
86
|
+
filterwarnings = [
|
87
|
+
"ignore::DeprecationWarning:pydantic._internal._config",
|
88
|
+
]
|
83
89
|
|
84
90
|
[tool.coverage.report]
|
85
91
|
exclude_lines = [
|
@@ -0,0 +1,114 @@
|
|
1
|
+
"""Bbox shape migration utilities for SFEOS collections."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
|
5
|
+
from stac_fastapi.sfeos_helpers.database import add_bbox_shape_to_collection
|
6
|
+
from stac_fastapi.sfeos_helpers.mappings import COLLECTIONS_INDEX
|
7
|
+
|
8
|
+
logger = logging.getLogger(__name__)
|
9
|
+
|
10
|
+
|
11
|
+
async def process_collection_bbox_shape(client, collection_doc, backend):
|
12
|
+
"""Process a single collection document to add bbox_shape field.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
client: Elasticsearch/OpenSearch client
|
16
|
+
collection_doc: Collection document from database
|
17
|
+
backend: Backend type ('elasticsearch' or 'opensearch')
|
18
|
+
|
19
|
+
Returns:
|
20
|
+
bool: True if collection was updated, False if no update was needed
|
21
|
+
"""
|
22
|
+
collection = collection_doc["_source"]
|
23
|
+
collection_id = collection.get("id", collection_doc["_id"])
|
24
|
+
|
25
|
+
# Use the shared function to add bbox_shape
|
26
|
+
was_added = add_bbox_shape_to_collection(collection)
|
27
|
+
|
28
|
+
if not was_added:
|
29
|
+
return False
|
30
|
+
|
31
|
+
# Update the collection in the database
|
32
|
+
if backend == "elasticsearch":
|
33
|
+
await client.index(
|
34
|
+
index=COLLECTIONS_INDEX,
|
35
|
+
id=collection_id,
|
36
|
+
document=collection,
|
37
|
+
refresh=True,
|
38
|
+
)
|
39
|
+
else: # opensearch
|
40
|
+
await client.index(
|
41
|
+
index=COLLECTIONS_INDEX,
|
42
|
+
id=collection_id,
|
43
|
+
body=collection,
|
44
|
+
refresh=True,
|
45
|
+
)
|
46
|
+
|
47
|
+
logger.info(f"Collection '{collection_id}': Added bbox_shape field")
|
48
|
+
return True
|
49
|
+
|
50
|
+
|
51
|
+
async def run_add_bbox_shape(backend):
|
52
|
+
"""Add bbox_shape field to all existing collections.
|
53
|
+
|
54
|
+
Args:
|
55
|
+
backend: Backend type ('elasticsearch' or 'opensearch')
|
56
|
+
"""
|
57
|
+
import os
|
58
|
+
|
59
|
+
logger.info(
|
60
|
+
f"Starting migration: Adding bbox_shape to existing collections ({backend})"
|
61
|
+
)
|
62
|
+
|
63
|
+
# Log connection info (showing what will be used by the client)
|
64
|
+
es_host = os.getenv("ES_HOST", "localhost")
|
65
|
+
es_port = os.getenv(
|
66
|
+
"ES_PORT", "9200"
|
67
|
+
) # Both backends default to 9200 in their config
|
68
|
+
es_use_ssl = os.getenv("ES_USE_SSL", "true")
|
69
|
+
logger.info(f"Connecting to {backend} at {es_host}:{es_port} (SSL: {es_use_ssl})")
|
70
|
+
|
71
|
+
# Create client based on backend
|
72
|
+
if backend == "elasticsearch":
|
73
|
+
from stac_fastapi.elasticsearch.config import AsyncElasticsearchSettings
|
74
|
+
|
75
|
+
settings = AsyncElasticsearchSettings()
|
76
|
+
else: # opensearch
|
77
|
+
from stac_fastapi.opensearch.config import AsyncOpensearchSettings
|
78
|
+
|
79
|
+
settings = AsyncOpensearchSettings()
|
80
|
+
|
81
|
+
client = settings.create_client
|
82
|
+
|
83
|
+
try:
|
84
|
+
# Get all collections
|
85
|
+
response = await client.search(
|
86
|
+
index=COLLECTIONS_INDEX,
|
87
|
+
body={
|
88
|
+
"query": {"match_all": {}},
|
89
|
+
"size": 10000,
|
90
|
+
}, # Adjust size if you have more collections
|
91
|
+
)
|
92
|
+
|
93
|
+
total_collections = response["hits"]["total"]["value"]
|
94
|
+
logger.info(f"Found {total_collections} collections to process")
|
95
|
+
|
96
|
+
updated_count = 0
|
97
|
+
skipped_count = 0
|
98
|
+
|
99
|
+
for hit in response["hits"]["hits"]:
|
100
|
+
was_updated = await process_collection_bbox_shape(client, hit, backend)
|
101
|
+
if was_updated:
|
102
|
+
updated_count += 1
|
103
|
+
else:
|
104
|
+
skipped_count += 1
|
105
|
+
|
106
|
+
logger.info(
|
107
|
+
f"Migration complete: {updated_count} collections updated, {skipped_count} skipped"
|
108
|
+
)
|
109
|
+
|
110
|
+
except Exception as e:
|
111
|
+
logger.error(f"Migration failed with error: {e}")
|
112
|
+
raise
|
113
|
+
finally:
|
114
|
+
await client.close()
|
@@ -13,121 +13,14 @@ import logging
|
|
13
13
|
import sys
|
14
14
|
|
15
15
|
import click
|
16
|
-
from stac_fastapi.sfeos_helpers.database import add_bbox_shape_to_collection
|
17
|
-
from stac_fastapi.sfeos_helpers.mappings import COLLECTIONS_INDEX
|
18
16
|
|
17
|
+
from .bbox_shape import run_add_bbox_shape
|
19
18
|
from .reindex import run as unified_reindex_run
|
20
19
|
|
21
20
|
logging.basicConfig(level=logging.INFO)
|
22
21
|
logger = logging.getLogger(__name__)
|
23
22
|
|
24
23
|
|
25
|
-
async def process_collection_bbox_shape(client, collection_doc, backend):
|
26
|
-
"""Process a single collection document to add bbox_shape field.
|
27
|
-
|
28
|
-
Args:
|
29
|
-
client: Elasticsearch/OpenSearch client
|
30
|
-
collection_doc: Collection document from database
|
31
|
-
backend: Backend type ('elasticsearch' or 'opensearch')
|
32
|
-
|
33
|
-
Returns:
|
34
|
-
bool: True if collection was updated, False if no update was needed
|
35
|
-
"""
|
36
|
-
collection = collection_doc["_source"]
|
37
|
-
collection_id = collection.get("id", collection_doc["_id"])
|
38
|
-
|
39
|
-
# Use the shared function to add bbox_shape
|
40
|
-
was_added = add_bbox_shape_to_collection(collection)
|
41
|
-
|
42
|
-
if not was_added:
|
43
|
-
return False
|
44
|
-
|
45
|
-
# Update the collection in the database
|
46
|
-
if backend == "elasticsearch":
|
47
|
-
await client.index(
|
48
|
-
index=COLLECTIONS_INDEX,
|
49
|
-
id=collection_id,
|
50
|
-
document=collection,
|
51
|
-
refresh=True,
|
52
|
-
)
|
53
|
-
else: # opensearch
|
54
|
-
await client.index(
|
55
|
-
index=COLLECTIONS_INDEX,
|
56
|
-
id=collection_id,
|
57
|
-
body=collection,
|
58
|
-
refresh=True,
|
59
|
-
)
|
60
|
-
|
61
|
-
logger.info(f"Collection '{collection_id}': Added bbox_shape field")
|
62
|
-
return True
|
63
|
-
|
64
|
-
|
65
|
-
async def run_add_bbox_shape(backend):
|
66
|
-
"""Add bbox_shape field to all existing collections.
|
67
|
-
|
68
|
-
Args:
|
69
|
-
backend: Backend type ('elasticsearch' or 'opensearch')
|
70
|
-
"""
|
71
|
-
import os
|
72
|
-
|
73
|
-
logger.info(
|
74
|
-
f"Starting migration: Adding bbox_shape to existing collections ({backend})"
|
75
|
-
)
|
76
|
-
|
77
|
-
# Log connection info (showing what will be used by the client)
|
78
|
-
es_host = os.getenv("ES_HOST", "localhost")
|
79
|
-
es_port = os.getenv(
|
80
|
-
"ES_PORT", "9200"
|
81
|
-
) # Both backends default to 9200 in their config
|
82
|
-
es_use_ssl = os.getenv("ES_USE_SSL", "true")
|
83
|
-
logger.info(f"Connecting to {backend} at {es_host}:{es_port} (SSL: {es_use_ssl})")
|
84
|
-
|
85
|
-
# Create client based on backend
|
86
|
-
if backend == "elasticsearch":
|
87
|
-
from stac_fastapi.elasticsearch.config import AsyncElasticsearchSettings
|
88
|
-
|
89
|
-
settings = AsyncElasticsearchSettings()
|
90
|
-
else: # opensearch
|
91
|
-
from stac_fastapi.opensearch.config import AsyncOpensearchSettings
|
92
|
-
|
93
|
-
settings = AsyncOpensearchSettings()
|
94
|
-
|
95
|
-
client = settings.create_client
|
96
|
-
|
97
|
-
try:
|
98
|
-
# Get all collections
|
99
|
-
response = await client.search(
|
100
|
-
index=COLLECTIONS_INDEX,
|
101
|
-
body={
|
102
|
-
"query": {"match_all": {}},
|
103
|
-
"size": 10000,
|
104
|
-
}, # Adjust size if you have more collections
|
105
|
-
)
|
106
|
-
|
107
|
-
total_collections = response["hits"]["total"]["value"]
|
108
|
-
logger.info(f"Found {total_collections} collections to process")
|
109
|
-
|
110
|
-
updated_count = 0
|
111
|
-
skipped_count = 0
|
112
|
-
|
113
|
-
for hit in response["hits"]["hits"]:
|
114
|
-
was_updated = await process_collection_bbox_shape(client, hit, backend)
|
115
|
-
if was_updated:
|
116
|
-
updated_count += 1
|
117
|
-
else:
|
118
|
-
skipped_count += 1
|
119
|
-
|
120
|
-
logger.info(
|
121
|
-
f"Migration complete: {updated_count} collections updated, {skipped_count} skipped"
|
122
|
-
)
|
123
|
-
|
124
|
-
except Exception as e:
|
125
|
-
logger.error(f"Migration failed with error: {e}")
|
126
|
-
raise
|
127
|
-
finally:
|
128
|
-
await client.close()
|
129
|
-
|
130
|
-
|
131
24
|
@click.group()
|
132
25
|
@click.version_option(version="0.1.0", prog_name="sfeos-tools")
|
133
26
|
def cli():
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: sfeos-tools
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.1
|
4
4
|
Summary: CLI tools for managing stac-fastapi-elasticsearch-opensearch deployments
|
5
5
|
Author: CloudFerro S.A.
|
6
6
|
Author-email: Jonathan Healy <jon@healy-hypersaptial.dev>
|
@@ -35,11 +35,12 @@ Classifier: Development Status :: 4 - Beta
|
|
35
35
|
Classifier: Intended Audience :: Developers
|
36
36
|
Classifier: Intended Audience :: Science/Research
|
37
37
|
Classifier: License :: OSI Approved :: MIT License
|
38
|
-
Classifier: Programming Language :: Python :: 3.8
|
39
38
|
Classifier: Programming Language :: Python :: 3.9
|
40
39
|
Classifier: Programming Language :: Python :: 3.10
|
41
40
|
Classifier: Programming Language :: Python :: 3.11
|
42
41
|
Classifier: Programming Language :: Python :: 3.12
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
43
|
+
Classifier: Programming Language :: Python :: 3.14
|
43
44
|
Classifier: Topic :: Scientific/Engineering
|
44
45
|
Classifier: Topic :: Scientific/Engineering :: GIS
|
45
46
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
@@ -48,13 +49,14 @@ Description-Content-Type: text/markdown
|
|
48
49
|
License-File: LICENSE
|
49
50
|
Requires-Dist: click>=8.0.0
|
50
51
|
Provides-Extra: elasticsearch
|
51
|
-
Requires-Dist: stac-fastapi-elasticsearch; extra == "elasticsearch"
|
52
|
+
Requires-Dist: stac-fastapi-elasticsearch>=6.6.0; extra == "elasticsearch"
|
52
53
|
Provides-Extra: opensearch
|
53
|
-
Requires-Dist: stac-fastapi-opensearch; extra == "opensearch"
|
54
|
+
Requires-Dist: stac-fastapi-opensearch>=6.6.0; extra == "opensearch"
|
54
55
|
Provides-Extra: dev
|
55
|
-
Requires-Dist: stac-fastapi-elasticsearch; extra == "dev"
|
56
|
-
Requires-Dist: stac-fastapi-opensearch; extra == "dev"
|
56
|
+
Requires-Dist: stac-fastapi-elasticsearch>=6.6.0; extra == "dev"
|
57
|
+
Requires-Dist: stac-fastapi-opensearch>=6.6.0; extra == "dev"
|
57
58
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
59
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
58
60
|
Requires-Dist: pytest-cov>=2.0; extra == "dev"
|
59
61
|
Requires-Dist: black>=21.0; extra == "dev"
|
60
62
|
Requires-Dist: isort>=5.0; extra == "dev"
|
@@ -66,6 +68,22 @@ Dynamic: license-file
|
|
66
68
|
|
67
69
|
CLI tools for managing [stac-fastapi-elasticsearch-opensearch](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch) deployments.
|
68
70
|
|
71
|
+
<!-- markdownlint-disable MD033 MD041 -->
|
72
|
+
|
73
|
+
|
74
|
+
<p align="left">
|
75
|
+
<img src="https://raw.githubusercontent.com/stac-utils/stac-fastapi-elasticsearch-opensearch/refs/heads/main/assets/sfeos.png" width=1000>
|
76
|
+
</p>
|
77
|
+
|
78
|
+
<!-- **Jump to:** [Project Introduction](#project-introduction---what-is-sfeos) | [Quick Start](#quick-start) | [Table of Contents](#table-of-contents) -->
|
79
|
+
|
80
|
+
[](https://pepy.tech/project/sfeos-tools)
|
81
|
+
[](https://github.com/healy-hyperspatial/sfeos-tools/graphs/contributors)
|
82
|
+
[](https://github.com/healy-hyperspatial/sfeos-tools/stargazers)
|
83
|
+
[](https://github.com/healy-hyperspatial/sfeos-tools/network/members)
|
84
|
+
[](https://pypi.org/project/sfeos-tools/)
|
85
|
+
[](https://github.com/radiantearth/stac-spec/tree/v1.1.0)
|
86
|
+
|
69
87
|
## Table of Contents
|
70
88
|
|
71
89
|
- [Installation](#installation)
|
@@ -2,6 +2,7 @@ LICENSE
|
|
2
2
|
README.md
|
3
3
|
pyproject.toml
|
4
4
|
sfeos_tools/__init__.py
|
5
|
+
sfeos_tools/bbox_shape.py
|
5
6
|
sfeos_tools/cli.py
|
6
7
|
sfeos_tools/reindex.py
|
7
8
|
sfeos_tools.egg-info/PKG-INFO
|
@@ -9,4 +10,5 @@ sfeos_tools.egg-info/SOURCES.txt
|
|
9
10
|
sfeos_tools.egg-info/dependency_links.txt
|
10
11
|
sfeos_tools.egg-info/entry_points.txt
|
11
12
|
sfeos_tools.egg-info/requires.txt
|
12
|
-
sfeos_tools.egg-info/top_level.txt
|
13
|
+
sfeos_tools.egg-info/top_level.txt
|
14
|
+
tests/test_bbox_shape.py
|
@@ -0,0 +1,18 @@
|
|
1
|
+
click>=8.0.0
|
2
|
+
|
3
|
+
[dev]
|
4
|
+
stac-fastapi-elasticsearch>=6.6.0
|
5
|
+
stac-fastapi-opensearch>=6.6.0
|
6
|
+
pytest>=6.0
|
7
|
+
pytest-asyncio>=0.21.0
|
8
|
+
pytest-cov>=2.0
|
9
|
+
black>=21.0
|
10
|
+
isort>=5.0
|
11
|
+
mypy>=0.900
|
12
|
+
flake8>=4.0.0
|
13
|
+
|
14
|
+
[elasticsearch]
|
15
|
+
stac-fastapi-elasticsearch>=6.6.0
|
16
|
+
|
17
|
+
[opensearch]
|
18
|
+
stac-fastapi-opensearch>=6.6.0
|
@@ -0,0 +1,275 @@
|
|
1
|
+
"""Tests for bbox_shape module."""
|
2
|
+
|
3
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
|
7
|
+
from sfeos_tools.bbox_shape import process_collection_bbox_shape, run_add_bbox_shape
|
8
|
+
|
9
|
+
|
10
|
+
class TestProcessCollectionBboxShape:
|
11
|
+
"""Tests for process_collection_bbox_shape function."""
|
12
|
+
|
13
|
+
@pytest.mark.asyncio
|
14
|
+
async def test_process_collection_bbox_shape_elasticsearch_updated(self):
|
15
|
+
"""Test processing a collection with Elasticsearch backend when bbox_shape is added."""
|
16
|
+
mock_client = AsyncMock()
|
17
|
+
collection_doc = {
|
18
|
+
"_id": "test-collection",
|
19
|
+
"_source": {
|
20
|
+
"id": "test-collection",
|
21
|
+
"bbox": [[-180, -90, 180, 90]],
|
22
|
+
},
|
23
|
+
}
|
24
|
+
|
25
|
+
with patch(
|
26
|
+
"sfeos_tools.bbox_shape.add_bbox_shape_to_collection", return_value=True
|
27
|
+
):
|
28
|
+
result = await process_collection_bbox_shape(
|
29
|
+
mock_client, collection_doc, "elasticsearch"
|
30
|
+
)
|
31
|
+
|
32
|
+
assert result is True
|
33
|
+
mock_client.index.assert_called_once()
|
34
|
+
call_kwargs = mock_client.index.call_args[1]
|
35
|
+
assert call_kwargs["index"] == "collections"
|
36
|
+
assert call_kwargs["id"] == "test-collection"
|
37
|
+
assert call_kwargs["refresh"] is True
|
38
|
+
assert "document" in call_kwargs
|
39
|
+
|
40
|
+
@pytest.mark.asyncio
|
41
|
+
async def test_process_collection_bbox_shape_opensearch_updated(self):
|
42
|
+
"""Test processing a collection with OpenSearch backend when bbox_shape is added."""
|
43
|
+
mock_client = AsyncMock()
|
44
|
+
collection_doc = {
|
45
|
+
"_id": "test-collection",
|
46
|
+
"_source": {
|
47
|
+
"id": "test-collection",
|
48
|
+
"bbox": [[-180, -90, 180, 90]],
|
49
|
+
},
|
50
|
+
}
|
51
|
+
|
52
|
+
with patch(
|
53
|
+
"sfeos_tools.bbox_shape.add_bbox_shape_to_collection", return_value=True
|
54
|
+
):
|
55
|
+
result = await process_collection_bbox_shape(
|
56
|
+
mock_client, collection_doc, "opensearch"
|
57
|
+
)
|
58
|
+
|
59
|
+
assert result is True
|
60
|
+
mock_client.index.assert_called_once()
|
61
|
+
call_kwargs = mock_client.index.call_args[1]
|
62
|
+
assert call_kwargs["index"] == "collections"
|
63
|
+
assert call_kwargs["id"] == "test-collection"
|
64
|
+
assert call_kwargs["refresh"] is True
|
65
|
+
assert "body" in call_kwargs
|
66
|
+
|
67
|
+
@pytest.mark.asyncio
|
68
|
+
async def test_process_collection_bbox_shape_not_updated(self):
|
69
|
+
"""Test processing a collection when bbox_shape is not added."""
|
70
|
+
mock_client = AsyncMock()
|
71
|
+
collection_doc = {
|
72
|
+
"_id": "test-collection",
|
73
|
+
"_source": {
|
74
|
+
"id": "test-collection",
|
75
|
+
"bbox": [[-180, -90, 180, 90]],
|
76
|
+
},
|
77
|
+
}
|
78
|
+
|
79
|
+
with patch(
|
80
|
+
"sfeos_tools.bbox_shape.add_bbox_shape_to_collection", return_value=False
|
81
|
+
):
|
82
|
+
result = await process_collection_bbox_shape(
|
83
|
+
mock_client, collection_doc, "elasticsearch"
|
84
|
+
)
|
85
|
+
|
86
|
+
assert result is False
|
87
|
+
mock_client.index.assert_not_called()
|
88
|
+
|
89
|
+
@pytest.mark.asyncio
|
90
|
+
async def test_process_collection_uses_id_from_source(self):
|
91
|
+
"""Test that collection ID is extracted from _source when available."""
|
92
|
+
mock_client = AsyncMock()
|
93
|
+
collection_doc = {
|
94
|
+
"_id": "doc-id",
|
95
|
+
"_source": {
|
96
|
+
"id": "collection-id",
|
97
|
+
"bbox": [[-180, -90, 180, 90]],
|
98
|
+
},
|
99
|
+
}
|
100
|
+
|
101
|
+
with patch(
|
102
|
+
"sfeos_tools.bbox_shape.add_bbox_shape_to_collection", return_value=True
|
103
|
+
):
|
104
|
+
await process_collection_bbox_shape(
|
105
|
+
mock_client, collection_doc, "elasticsearch"
|
106
|
+
)
|
107
|
+
|
108
|
+
call_kwargs = mock_client.index.call_args[1]
|
109
|
+
assert call_kwargs["id"] == "collection-id"
|
110
|
+
|
111
|
+
@pytest.mark.asyncio
|
112
|
+
async def test_process_collection_falls_back_to_doc_id(self):
|
113
|
+
"""Test that collection ID falls back to _id when not in _source."""
|
114
|
+
mock_client = AsyncMock()
|
115
|
+
collection_doc = {
|
116
|
+
"_id": "doc-id",
|
117
|
+
"_source": {
|
118
|
+
"bbox": [[-180, -90, 180, 90]],
|
119
|
+
},
|
120
|
+
}
|
121
|
+
|
122
|
+
with patch(
|
123
|
+
"sfeos_tools.bbox_shape.add_bbox_shape_to_collection", return_value=True
|
124
|
+
):
|
125
|
+
await process_collection_bbox_shape(
|
126
|
+
mock_client, collection_doc, "elasticsearch"
|
127
|
+
)
|
128
|
+
|
129
|
+
call_kwargs = mock_client.index.call_args[1]
|
130
|
+
assert call_kwargs["id"] == "doc-id"
|
131
|
+
|
132
|
+
|
133
|
+
class TestRunAddBboxShape:
|
134
|
+
"""Tests for run_add_bbox_shape function."""
|
135
|
+
|
136
|
+
@pytest.mark.asyncio
|
137
|
+
async def test_run_add_bbox_shape_elasticsearch(self):
|
138
|
+
"""Test run_add_bbox_shape with Elasticsearch backend."""
|
139
|
+
mock_client = AsyncMock()
|
140
|
+
mock_settings = MagicMock()
|
141
|
+
mock_settings.create_client = mock_client
|
142
|
+
|
143
|
+
mock_client.search.return_value = {
|
144
|
+
"hits": {
|
145
|
+
"total": {"value": 2},
|
146
|
+
"hits": [
|
147
|
+
{
|
148
|
+
"_id": "collection-1",
|
149
|
+
"_source": {
|
150
|
+
"id": "collection-1",
|
151
|
+
"bbox": [[-180, -90, 180, 90]],
|
152
|
+
},
|
153
|
+
},
|
154
|
+
{
|
155
|
+
"_id": "collection-2",
|
156
|
+
"_source": {
|
157
|
+
"id": "collection-2",
|
158
|
+
"bbox": [[-180, -90, 180, 90]],
|
159
|
+
},
|
160
|
+
},
|
161
|
+
],
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
with patch(
|
166
|
+
"stac_fastapi.elasticsearch.config.AsyncElasticsearchSettings",
|
167
|
+
return_value=mock_settings,
|
168
|
+
), patch(
|
169
|
+
"sfeos_tools.bbox_shape.add_bbox_shape_to_collection", return_value=True
|
170
|
+
):
|
171
|
+
await run_add_bbox_shape("elasticsearch")
|
172
|
+
|
173
|
+
mock_client.search.assert_called_once()
|
174
|
+
assert mock_client.index.call_count == 2
|
175
|
+
mock_client.close.assert_called_once()
|
176
|
+
|
177
|
+
@pytest.mark.asyncio
|
178
|
+
async def test_run_add_bbox_shape_opensearch(self):
|
179
|
+
"""Test run_add_bbox_shape with OpenSearch backend."""
|
180
|
+
mock_client = AsyncMock()
|
181
|
+
mock_settings = MagicMock()
|
182
|
+
mock_settings.create_client = mock_client
|
183
|
+
|
184
|
+
mock_client.search.return_value = {
|
185
|
+
"hits": {
|
186
|
+
"total": {"value": 1},
|
187
|
+
"hits": [
|
188
|
+
{
|
189
|
+
"_id": "collection-1",
|
190
|
+
"_source": {
|
191
|
+
"id": "collection-1",
|
192
|
+
"bbox": [[-180, -90, 180, 90]],
|
193
|
+
},
|
194
|
+
},
|
195
|
+
],
|
196
|
+
}
|
197
|
+
}
|
198
|
+
|
199
|
+
with patch(
|
200
|
+
"stac_fastapi.opensearch.config.AsyncOpensearchSettings",
|
201
|
+
return_value=mock_settings,
|
202
|
+
), patch(
|
203
|
+
"sfeos_tools.bbox_shape.add_bbox_shape_to_collection", return_value=True
|
204
|
+
):
|
205
|
+
await run_add_bbox_shape("opensearch")
|
206
|
+
|
207
|
+
mock_client.search.assert_called_once()
|
208
|
+
mock_client.index.assert_called_once()
|
209
|
+
mock_client.close.assert_called_once()
|
210
|
+
|
211
|
+
@pytest.mark.asyncio
|
212
|
+
async def test_run_add_bbox_shape_handles_mixed_results(self):
|
213
|
+
"""Test run_add_bbox_shape with some collections updated and some skipped."""
|
214
|
+
mock_client = AsyncMock()
|
215
|
+
mock_settings = MagicMock()
|
216
|
+
mock_settings.create_client = mock_client
|
217
|
+
|
218
|
+
mock_client.search.return_value = {
|
219
|
+
"hits": {
|
220
|
+
"total": {"value": 3},
|
221
|
+
"hits": [
|
222
|
+
{
|
223
|
+
"_id": "collection-1",
|
224
|
+
"_source": {
|
225
|
+
"id": "collection-1",
|
226
|
+
"bbox": [[-180, -90, 180, 90]],
|
227
|
+
},
|
228
|
+
},
|
229
|
+
{
|
230
|
+
"_id": "collection-2",
|
231
|
+
"_source": {
|
232
|
+
"id": "collection-2",
|
233
|
+
"bbox": [[-180, -90, 180, 90]],
|
234
|
+
},
|
235
|
+
},
|
236
|
+
{
|
237
|
+
"_id": "collection-3",
|
238
|
+
"_source": {
|
239
|
+
"id": "collection-3",
|
240
|
+
"bbox": [[-180, -90, 180, 90]],
|
241
|
+
},
|
242
|
+
},
|
243
|
+
],
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
side_effects = [True, False, True]
|
248
|
+
|
249
|
+
with patch(
|
250
|
+
"stac_fastapi.elasticsearch.config.AsyncElasticsearchSettings",
|
251
|
+
return_value=mock_settings,
|
252
|
+
), patch(
|
253
|
+
"sfeos_tools.bbox_shape.add_bbox_shape_to_collection",
|
254
|
+
side_effect=side_effects,
|
255
|
+
):
|
256
|
+
await run_add_bbox_shape("elasticsearch")
|
257
|
+
|
258
|
+
assert mock_client.index.call_count == 2
|
259
|
+
|
260
|
+
@pytest.mark.asyncio
|
261
|
+
async def test_run_add_bbox_shape_closes_client_on_error(self):
|
262
|
+
"""Test that client is closed even when an error occurs."""
|
263
|
+
mock_client = AsyncMock()
|
264
|
+
mock_settings = MagicMock()
|
265
|
+
mock_settings.create_client = mock_client
|
266
|
+
|
267
|
+
mock_client.search.side_effect = Exception("Connection error")
|
268
|
+
|
269
|
+
with patch(
|
270
|
+
"stac_fastapi.elasticsearch.config.AsyncElasticsearchSettings",
|
271
|
+
return_value=mock_settings,
|
272
|
+
), pytest.raises(Exception, match="Connection error"):
|
273
|
+
await run_add_bbox_shape("elasticsearch")
|
274
|
+
|
275
|
+
mock_client.close.assert_called_once()
|
@@ -1,17 +0,0 @@
|
|
1
|
-
click>=8.0.0
|
2
|
-
|
3
|
-
[dev]
|
4
|
-
stac-fastapi-elasticsearch
|
5
|
-
stac-fastapi-opensearch
|
6
|
-
pytest>=6.0
|
7
|
-
pytest-cov>=2.0
|
8
|
-
black>=21.0
|
9
|
-
isort>=5.0
|
10
|
-
mypy>=0.900
|
11
|
-
flake8>=4.0.0
|
12
|
-
|
13
|
-
[elasticsearch]
|
14
|
-
stac-fastapi-elasticsearch
|
15
|
-
|
16
|
-
[opensearch]
|
17
|
-
stac-fastapi-opensearch
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|