rhinestone 0.1.0__py3-none-any.whl
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.
- rhinestone/__init__.py +60 -0
- rhinestone/_http.py +152 -0
- rhinestone/adapters/__init__.py +33 -0
- rhinestone/adapters/execution/__init__.py +17 -0
- rhinestone/adapters/execution/_resource.py +14 -0
- rhinestone/adapters/execution/base.py +47 -0
- rhinestone/adapters/execution/gdal/README.md +10 -0
- rhinestone/adapters/execution/gdal/__init__.py +114 -0
- rhinestone/adapters/execution/json_service/README.md +10 -0
- rhinestone/adapters/execution/json_service/__init__.py +103 -0
- rhinestone/adapters/execution/pyogrio/README.md +10 -0
- rhinestone/adapters/execution/pyogrio/__init__.py +44 -0
- rhinestone/adapters/execution/rasterio/README.md +9 -0
- rhinestone/adapters/execution/rasterio/__init__.py +38 -0
- rhinestone/adapters/source/__init__.py +29 -0
- rhinestone/adapters/source/_knowledge.py +59 -0
- rhinestone/adapters/source/_uri.py +20 -0
- rhinestone/adapters/source/base.py +234 -0
- rhinestone/adapters/source/ckan/README.md +12 -0
- rhinestone/adapters/source/ckan/__init__.py +176 -0
- rhinestone/adapters/source/ckan/schema.json +11 -0
- rhinestone/adapters/source/dcat/README.md +12 -0
- rhinestone/adapters/source/dcat/__init__.py +178 -0
- rhinestone/adapters/source/dcat/schema.json +13 -0
- rhinestone/adapters/source/direct/README.md +10 -0
- rhinestone/adapters/source/direct/__init__.py +56 -0
- rhinestone/adapters/source/direct/schema.json +17 -0
- rhinestone/adapters/source/gsi_fundamental/README.md +11 -0
- rhinestone/adapters/source/gsi_fundamental/__init__.py +43 -0
- rhinestone/adapters/source/gsi_fundamental/schema.json +32 -0
- rhinestone/adapters/source/odpt/README.md +11 -0
- rhinestone/adapters/source/odpt/__init__.py +140 -0
- rhinestone/adapters/source/odpt/schema.json +15 -0
- rhinestone/adapters/source/ogc/README.md +12 -0
- rhinestone/adapters/source/ogc/__init__.py +171 -0
- rhinestone/adapters/source/ogc/schema.json +12 -0
- rhinestone/adapters/source/plateau/README.md +12 -0
- rhinestone/adapters/source/plateau/__init__.py +96 -0
- rhinestone/adapters/source/plateau/schema.json +24 -0
- rhinestone/adapters/source/search_ckan_jp/__init__.py +154 -0
- rhinestone/adapters/source/search_ckan_jp/schema.json +9 -0
- rhinestone/adapters/source/stac/README.md +11 -0
- rhinestone/adapters/source/stac/__init__.py +188 -0
- rhinestone/adapters/source/stac/schema.json +13 -0
- rhinestone/adapters/source/static/__init__.py +217 -0
- rhinestone/adapters/source/static/schema.json +10 -0
- rhinestone/api.py +396 -0
- rhinestone/catalogs/__init__.py +147 -0
- rhinestone/catalogs/sources.json +188 -0
- rhinestone/errors.py +69 -0
- rhinestone/execution.py +39 -0
- rhinestone/models.py +342 -0
- rhinestone/pipeline.py +96 -0
- rhinestone/py.typed +0 -0
- rhinestone/registry.py +99 -0
- rhinestone/resolution.py +102 -0
- rhinestone/search.py +172 -0
- rhinestone/security.py +231 -0
- rhinestone/sources.py +31 -0
- rhinestone-0.1.0.dist-info/METADATA +140 -0
- rhinestone-0.1.0.dist-info/RECORD +63 -0
- rhinestone-0.1.0.dist-info/WHEEL +4 -0
- rhinestone-0.1.0.dist-info/licenses/LICENSE +21 -0
rhinestone/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Rhinestone public package."""
|
|
2
|
+
|
|
3
|
+
from . import sources
|
|
4
|
+
from .api import Rhinestone, configure
|
|
5
|
+
from .catalogs import Catalog
|
|
6
|
+
from .models import (
|
|
7
|
+
AccessPlan,
|
|
8
|
+
Config,
|
|
9
|
+
Dependencies,
|
|
10
|
+
FileAccessPlan,
|
|
11
|
+
LibraryName,
|
|
12
|
+
Metadata,
|
|
13
|
+
Provenance,
|
|
14
|
+
Provider,
|
|
15
|
+
RemoteDatasetPlan,
|
|
16
|
+
Resource,
|
|
17
|
+
ResourceCandidate,
|
|
18
|
+
Result,
|
|
19
|
+
Runtime,
|
|
20
|
+
RuntimeFactory,
|
|
21
|
+
SearchDiagnostic,
|
|
22
|
+
SearchQuery,
|
|
23
|
+
SearchResult,
|
|
24
|
+
ServiceQueryPlan,
|
|
25
|
+
Source,
|
|
26
|
+
SourceDefinition,
|
|
27
|
+
)
|
|
28
|
+
from .search import SearchResults
|
|
29
|
+
from .security import DestinationPolicy, DestinationRule, NetworkPolicyLevel
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AccessPlan",
|
|
33
|
+
"Catalog",
|
|
34
|
+
"Config",
|
|
35
|
+
"DestinationPolicy",
|
|
36
|
+
"DestinationRule",
|
|
37
|
+
"Dependencies",
|
|
38
|
+
"FileAccessPlan",
|
|
39
|
+
"LibraryName",
|
|
40
|
+
"Metadata",
|
|
41
|
+
"NetworkPolicyLevel",
|
|
42
|
+
"Provenance",
|
|
43
|
+
"Provider",
|
|
44
|
+
"RemoteDatasetPlan",
|
|
45
|
+
"Rhinestone",
|
|
46
|
+
"Resource",
|
|
47
|
+
"ResourceCandidate",
|
|
48
|
+
"Result",
|
|
49
|
+
"Runtime",
|
|
50
|
+
"RuntimeFactory",
|
|
51
|
+
"SearchDiagnostic",
|
|
52
|
+
"SearchQuery",
|
|
53
|
+
"SearchResult",
|
|
54
|
+
"SearchResults",
|
|
55
|
+
"ServiceQueryPlan",
|
|
56
|
+
"Source",
|
|
57
|
+
"SourceDefinition",
|
|
58
|
+
"configure",
|
|
59
|
+
"sources",
|
|
60
|
+
]
|
rhinestone/_http.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Built-in standard-library HTTP transport."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Mapping, Optional, cast
|
|
5
|
+
from urllib.error import HTTPError
|
|
6
|
+
from urllib.parse import urlencode, urlsplit, urlunsplit
|
|
7
|
+
from urllib.request import HTTPRedirectHandler, Request, build_opener, urlopen
|
|
8
|
+
|
|
9
|
+
from .errors import ProviderResponseError
|
|
10
|
+
|
|
11
|
+
_TIMEOUT_SECONDS = 30
|
|
12
|
+
_DEFAULT_HEADERS = {
|
|
13
|
+
"Accept": "application/json",
|
|
14
|
+
"User-Agent": "rhinestone",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _JsonIntegerDecodeError(ValueError):
|
|
19
|
+
"""JSON integer conversion failed inside the decoder."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_json_int(value: str) -> int:
|
|
23
|
+
try:
|
|
24
|
+
return int(value)
|
|
25
|
+
except ValueError as error:
|
|
26
|
+
raise _JsonIntegerDecodeError from error
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class JsonDocument(dict[str, Any]):
|
|
30
|
+
"""Decoded JSON object together with the URI that supplied it."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, value: Mapping[str, Any], response_uri: str) -> None:
|
|
33
|
+
super().__init__(value)
|
|
34
|
+
self.response_uri = response_uri
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_json(
|
|
38
|
+
url: str,
|
|
39
|
+
params: Mapping[str, Any],
|
|
40
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
41
|
+
) -> Any:
|
|
42
|
+
"""Fetch and decode JSON over HTTP using the standard library."""
|
|
43
|
+
request = _request(url, params, headers)
|
|
44
|
+
opener = (
|
|
45
|
+
build_opener(_NoRedirectHandler())
|
|
46
|
+
if getattr(headers, "_rhinestone_no_redirects", False)
|
|
47
|
+
else None
|
|
48
|
+
)
|
|
49
|
+
open_request = opener.open if opener is not None else urlopen
|
|
50
|
+
with open_request(request, timeout=_TIMEOUT_SECONDS) as response:
|
|
51
|
+
try:
|
|
52
|
+
decoded = json.load(response, parse_int=_parse_json_int)
|
|
53
|
+
except (
|
|
54
|
+
UnicodeDecodeError,
|
|
55
|
+
json.JSONDecodeError,
|
|
56
|
+
_JsonIntegerDecodeError,
|
|
57
|
+
):
|
|
58
|
+
raise ProviderResponseError("Provider response is not valid JSON") from None
|
|
59
|
+
if isinstance(decoded, Mapping):
|
|
60
|
+
response_uri = getattr(response, "geturl", lambda: request.full_url)()
|
|
61
|
+
return JsonDocument(
|
|
62
|
+
cast(Mapping[str, Any], decoded), response_uri or request.full_url
|
|
63
|
+
)
|
|
64
|
+
return decoded
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_text(url: str) -> str:
|
|
68
|
+
"""Fetch a text document without following HTTP redirects."""
|
|
69
|
+
request = Request(url, headers={"User-Agent": "rhinestone"})
|
|
70
|
+
opener = build_opener(_NoRedirectHandler())
|
|
71
|
+
with opener.open(request, timeout=_TIMEOUT_SECONDS) as response:
|
|
72
|
+
charset = response.headers.get_content_charset() or "utf-8"
|
|
73
|
+
return response.read().decode(charset)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class JsonServiceRuntime:
|
|
77
|
+
"""Small requests-compatible runtime used by ``JsonServiceAdapter``."""
|
|
78
|
+
|
|
79
|
+
def get(
|
|
80
|
+
self,
|
|
81
|
+
url: str,
|
|
82
|
+
*,
|
|
83
|
+
params: Mapping[str, Any],
|
|
84
|
+
headers: Mapping[str, str],
|
|
85
|
+
timeout: int,
|
|
86
|
+
allow_redirects: bool,
|
|
87
|
+
) -> "JsonResponse":
|
|
88
|
+
request = _request(url, params, headers)
|
|
89
|
+
opener = build_opener(_NoRedirectHandler())
|
|
90
|
+
try:
|
|
91
|
+
response = opener.open(request, timeout=timeout)
|
|
92
|
+
except HTTPError as error:
|
|
93
|
+
response = error
|
|
94
|
+
with response:
|
|
95
|
+
charset = response.headers.get_content_charset() or "utf-8"
|
|
96
|
+
status_code = cast(int, response.getcode())
|
|
97
|
+
return JsonResponse(status_code, response.read(), charset)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class JsonResponse:
|
|
101
|
+
"""Subset of the requests response contract used by ``JsonServiceAdapter``."""
|
|
102
|
+
|
|
103
|
+
def __init__(self, status_code: int, body: bytes, charset: str) -> None:
|
|
104
|
+
self.status_code = status_code
|
|
105
|
+
self._body = body
|
|
106
|
+
self._charset = charset
|
|
107
|
+
|
|
108
|
+
def raise_for_status(self) -> None:
|
|
109
|
+
if self.status_code >= 400:
|
|
110
|
+
raise OSError(f"HTTP request failed with status {self.status_code}")
|
|
111
|
+
|
|
112
|
+
def json(self) -> Any:
|
|
113
|
+
return json.loads(self._body.decode(self._charset))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _NoRedirectHandler(HTTPRedirectHandler):
|
|
117
|
+
def redirect_request(
|
|
118
|
+
self,
|
|
119
|
+
req: Request,
|
|
120
|
+
fp: Any,
|
|
121
|
+
code: int,
|
|
122
|
+
msg: str,
|
|
123
|
+
headers: Any,
|
|
124
|
+
newurl: str,
|
|
125
|
+
) -> Optional[Request]:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _request(
|
|
130
|
+
url: str,
|
|
131
|
+
params: Mapping[str, Any],
|
|
132
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
133
|
+
) -> Request:
|
|
134
|
+
request_url = _append_query(url, params)
|
|
135
|
+
return Request(
|
|
136
|
+
request_url,
|
|
137
|
+
headers={**_DEFAULT_HEADERS, **dict(headers or {})},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _append_query(url: str, params: Mapping[str, Any]) -> str:
|
|
142
|
+
"""Append parameters without replacing or normalizing an existing query."""
|
|
143
|
+
additional_query = urlencode(params, doseq=True)
|
|
144
|
+
if not additional_query:
|
|
145
|
+
return url
|
|
146
|
+
components = urlsplit(url)
|
|
147
|
+
query = (
|
|
148
|
+
f"{components.query}&{additional_query}"
|
|
149
|
+
if components.query
|
|
150
|
+
else additional_query
|
|
151
|
+
)
|
|
152
|
+
return urlunsplit(components._replace(query=query))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Compatibility exports for built-in source adapters."""
|
|
2
|
+
|
|
3
|
+
from .execution import ExecutionAdapter
|
|
4
|
+
from .source import (
|
|
5
|
+
CkanAdapter,
|
|
6
|
+
DcatAdapter,
|
|
7
|
+
DirectAdapter,
|
|
8
|
+
GsiFundamentalAdapter,
|
|
9
|
+
JsonGetter,
|
|
10
|
+
JsonObject,
|
|
11
|
+
OdptAdapter,
|
|
12
|
+
OgcFeaturesAdapter,
|
|
13
|
+
PlateauAdapter,
|
|
14
|
+
ProviderAdapter,
|
|
15
|
+
StacAdapter,
|
|
16
|
+
StaticAdapter,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"CkanAdapter",
|
|
21
|
+
"DirectAdapter",
|
|
22
|
+
"DcatAdapter",
|
|
23
|
+
"GsiFundamentalAdapter",
|
|
24
|
+
"JsonGetter",
|
|
25
|
+
"JsonObject",
|
|
26
|
+
"OdptAdapter",
|
|
27
|
+
"OgcFeaturesAdapter",
|
|
28
|
+
"PlateauAdapter",
|
|
29
|
+
"ProviderAdapter",
|
|
30
|
+
"StacAdapter",
|
|
31
|
+
"StaticAdapter",
|
|
32
|
+
"ExecutionAdapter",
|
|
33
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Built-in execution adapters."""
|
|
2
|
+
|
|
3
|
+
from ._resource import resource_attributes
|
|
4
|
+
from .base import ExecutionAdapter
|
|
5
|
+
from .gdal import GdalAdapter
|
|
6
|
+
from .json_service import JsonServiceAdapter
|
|
7
|
+
from .pyogrio import PyogrioAdapter
|
|
8
|
+
from .rasterio import RasterioAdapter
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ExecutionAdapter",
|
|
12
|
+
"GdalAdapter",
|
|
13
|
+
"JsonServiceAdapter",
|
|
14
|
+
"PyogrioAdapter",
|
|
15
|
+
"RasterioAdapter",
|
|
16
|
+
"resource_attributes",
|
|
17
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Shared helpers for selected execution adapters."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Mapping
|
|
4
|
+
|
|
5
|
+
from ...models import Resource
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def resource_attributes(resource: Resource) -> Mapping[str, Any]:
|
|
9
|
+
for candidate in resource.source.candidates:
|
|
10
|
+
if candidate.uri == resource.uri and candidate.attributes.get(
|
|
11
|
+
"matches_config", True
|
|
12
|
+
):
|
|
13
|
+
return candidate.attributes
|
|
14
|
+
return {}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Public base class for execution adapters."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any, FrozenSet
|
|
5
|
+
|
|
6
|
+
from ...models import Resource
|
|
7
|
+
from ...security import DestinationPolicy
|
|
8
|
+
|
|
9
|
+
__all__ = ["ExecutionAdapter"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExecutionAdapter(ABC):
|
|
13
|
+
"""Base for adapters that translate a selected ``Resource`` to an OSS call."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
"""Stable dependency and selection name."""
|
|
17
|
+
|
|
18
|
+
priority: int
|
|
19
|
+
"""Deterministic selection priority."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, destination_policy: DestinationPolicy | None = None) -> None:
|
|
22
|
+
self._destination_policy = (
|
|
23
|
+
destination_policy or DestinationPolicy.unrestricted()
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def supports(self, resource: Resource, dependencies: FrozenSet[str]) -> bool:
|
|
28
|
+
"""Whether this adapter can open the already selected resource."""
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def open(
|
|
32
|
+
self,
|
|
33
|
+
resource: Resource,
|
|
34
|
+
runtime: Any,
|
|
35
|
+
*,
|
|
36
|
+
destination_policy: DestinationPolicy | None = None,
|
|
37
|
+
) -> Any:
|
|
38
|
+
"""Delegate the already selected resource to the supplied runtime."""
|
|
39
|
+
|
|
40
|
+
def authorize(
|
|
41
|
+
self,
|
|
42
|
+
resource: Resource,
|
|
43
|
+
*,
|
|
44
|
+
destination_policy: DestinationPolicy | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Authorize a resource before resolving its user-owned runtime."""
|
|
47
|
+
(destination_policy or self._destination_policy).authorize(resource.uri)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# GDAL Execution Adapter
|
|
2
|
+
|
|
3
|
+
`GdalAdapter` は選択済み Resource を利用者提供の GDAL runtime へ渡します。
|
|
4
|
+
|
|
5
|
+
- `name`: `gdal`、優先度: `20`
|
|
6
|
+
- 対応形式: Shapefile, GeoTIFF, COG, NetCDF, WMS, GML, CityGML、または XYZ tile
|
|
7
|
+
- 翻訳: ZIP の `/vsizip/` URI、remote ZIP の `/vsicurl/`、encoding の open option、XYZ の GDAL WMS XML
|
|
8
|
+
- runtime: `OpenEx(uri, open_options=...)`
|
|
9
|
+
|
|
10
|
+
Resource の再選択、データ解析、形式・CRS 変換は行いません。
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Built-in adapters that delegate selected Resources to user-owned runtimes."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, FrozenSet, List, Mapping, cast
|
|
4
|
+
from xml.etree.ElementTree import Element, SubElement, tostring
|
|
5
|
+
|
|
6
|
+
from ....errors import ResourceAccessError
|
|
7
|
+
from ....models import FileAccessPlan, Resource
|
|
8
|
+
from ....security import DestinationPolicy
|
|
9
|
+
from .._resource import resource_attributes
|
|
10
|
+
from ..base import ExecutionAdapter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GdalAdapter(ExecutionAdapter):
|
|
14
|
+
"""Translate Resources into calls understood by a supplied GDAL module."""
|
|
15
|
+
|
|
16
|
+
name = "gdal"
|
|
17
|
+
priority = 20
|
|
18
|
+
_formats = frozenset(
|
|
19
|
+
{"shapefile", "geotiff", "cog", "netcdf", "wms", "gml", "citygml"}
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def __init__(self, destination_policy: DestinationPolicy | None = None) -> None:
|
|
23
|
+
super().__init__(destination_policy)
|
|
24
|
+
|
|
25
|
+
def supports(self, resource: Resource, dependencies: FrozenSet[str]) -> bool:
|
|
26
|
+
tile = resource.access_plan.options.get("tile")
|
|
27
|
+
return self.name in dependencies and (
|
|
28
|
+
(resource.format or "").lower() in self._formats
|
|
29
|
+
or (
|
|
30
|
+
isinstance(tile, Mapping)
|
|
31
|
+
and cast(Mapping[str, Any], tile).get("scheme") == "xyz"
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def open(
|
|
36
|
+
self,
|
|
37
|
+
resource: Resource,
|
|
38
|
+
runtime: Any,
|
|
39
|
+
*,
|
|
40
|
+
destination_policy: DestinationPolicy | None = None,
|
|
41
|
+
) -> Any:
|
|
42
|
+
uri = self._runtime_uri(resource)
|
|
43
|
+
attributes = resource_attributes(resource)
|
|
44
|
+
options: List[str] = []
|
|
45
|
+
encoding = attributes.get("encoding")
|
|
46
|
+
if isinstance(encoding, str):
|
|
47
|
+
options.append("ENCODING=" + encoding.upper())
|
|
48
|
+
try:
|
|
49
|
+
runtime_options: dict[str, Any] = {"open_options": tuple(options)}
|
|
50
|
+
result = runtime.OpenEx(uri, **runtime_options)
|
|
51
|
+
if result is None:
|
|
52
|
+
raise ResourceAccessError("GDAL returned no dataset")
|
|
53
|
+
return result
|
|
54
|
+
except Exception as error:
|
|
55
|
+
raise ResourceAccessError(
|
|
56
|
+
f"GDAL could not open {resource.uri!r}"
|
|
57
|
+
) from error
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _runtime_uri(resource: Resource) -> str:
|
|
61
|
+
attributes = resource_attributes(resource)
|
|
62
|
+
uri = resource.uri
|
|
63
|
+
tile = resource.access_plan.options.get("tile")
|
|
64
|
+
if tile is not None:
|
|
65
|
+
if not isinstance(tile, Mapping):
|
|
66
|
+
raise ResourceAccessError("GDAL tile options must be an object")
|
|
67
|
+
tile = cast(Mapping[str, Any], tile)
|
|
68
|
+
tile_url = tile.get("url")
|
|
69
|
+
if not isinstance(tile_url, str):
|
|
70
|
+
raise ResourceAccessError("GDAL tile URL must be a string")
|
|
71
|
+
uri = GdalAdapter.tile_xml(tile)
|
|
72
|
+
archive = attributes.get("archive")
|
|
73
|
+
if isinstance(resource.access_plan, FileAccessPlan):
|
|
74
|
+
archive = resource.access_plan.archive or archive
|
|
75
|
+
if archive == "zip":
|
|
76
|
+
uri = (
|
|
77
|
+
"/vsizip//vsicurl/" + uri
|
|
78
|
+
if uri.startswith(("http://", "https://"))
|
|
79
|
+
else "/vsizip/" + uri
|
|
80
|
+
)
|
|
81
|
+
member = resource.access_plan.options.get("entry_point")
|
|
82
|
+
if member:
|
|
83
|
+
uri += "/" + member
|
|
84
|
+
return uri
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def tile_xml(tile: Mapping[str, Any]) -> str:
|
|
88
|
+
root = Element("GDAL_WMS")
|
|
89
|
+
service = SubElement(root, "Service", name="TMS")
|
|
90
|
+
template = tile["url"]
|
|
91
|
+
for name in ("x", "y", "z"):
|
|
92
|
+
template = template.replace("{" + name + "}", "${" + name + "}")
|
|
93
|
+
SubElement(service, "ServerUrl").text = template
|
|
94
|
+
window = SubElement(root, "DataWindow")
|
|
95
|
+
for key, value in {
|
|
96
|
+
"UpperLeftX": "-20037508.342789244",
|
|
97
|
+
"UpperLeftY": "20037508.342789244",
|
|
98
|
+
"LowerRightX": "20037508.342789244",
|
|
99
|
+
"LowerRightY": "-20037508.342789244",
|
|
100
|
+
"TileLevel": str(tile["max_zoom"]),
|
|
101
|
+
"TileCountX": "1",
|
|
102
|
+
"TileCountY": "1",
|
|
103
|
+
"YOrigin": "top",
|
|
104
|
+
}.items():
|
|
105
|
+
SubElement(window, key).text = value
|
|
106
|
+
for key, value in {
|
|
107
|
+
"Projection": "EPSG:3857",
|
|
108
|
+
"BlockSizeX": "256",
|
|
109
|
+
"BlockSizeY": "256",
|
|
110
|
+
"BandsCount": "3",
|
|
111
|
+
"OverviewCount": str(tile["max_zoom"] - tile["min_zoom"]),
|
|
112
|
+
}.items():
|
|
113
|
+
SubElement(root, key).text = value
|
|
114
|
+
return tostring(root, encoding="unicode")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# JSON Service Execution Adapter
|
|
2
|
+
|
|
3
|
+
`JsonServiceAdapter` は選択済み JSON service query をHTTP runtimeへ渡します。標準の`configure()`経路ではRhinestoneの組み込みruntimeを使用します。
|
|
4
|
+
|
|
5
|
+
- `name`: `json-service`、優先度: `10`
|
|
6
|
+
- 対応条件: `ServiceQueryPlan`、`application/json`、一致する service 名
|
|
7
|
+
- 注入: request を作る callback、service 名、任意の `CredentialRegistry`
|
|
8
|
+
- runtime contract: `get(uri, params=..., headers=..., timeout=30, allow_redirects=False)`
|
|
9
|
+
|
|
10
|
+
redirect は拒否し、secret を含む可能性のある下位例外は公開しません。JSON schema の検証以外の変換は行いません。Adapter単体では同じcontractを満たすruntimeを直接渡せます。
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Selected JSON service requests delegated to a requests-compatible runtime."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, FrozenSet, List, Mapping, Optional, Tuple, cast
|
|
4
|
+
|
|
5
|
+
from ....errors import ProviderResponseError, ResourceAccessError
|
|
6
|
+
from ....models import AccessPlan, Resource, ServiceQueryPlan
|
|
7
|
+
from ....registry import CredentialRegistry
|
|
8
|
+
from ....security import DestinationPolicy
|
|
9
|
+
from ..base import ExecutionAdapter
|
|
10
|
+
|
|
11
|
+
RequestPreparer = Callable[
|
|
12
|
+
[AccessPlan, CredentialRegistry], Tuple[Mapping[str, Any], Mapping[str, str]]
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class JsonServiceAdapter(ExecutionAdapter):
|
|
17
|
+
name = "json-service"
|
|
18
|
+
priority = 10
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
prepare_request: RequestPreparer,
|
|
23
|
+
service: str,
|
|
24
|
+
credentials: Optional[CredentialRegistry] = None,
|
|
25
|
+
destination_policy: Optional[DestinationPolicy] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__(destination_policy)
|
|
28
|
+
self._prepare_request = prepare_request
|
|
29
|
+
self._service = service
|
|
30
|
+
self._credentials = credentials or CredentialRegistry({})
|
|
31
|
+
|
|
32
|
+
def bind_credentials(self, credentials: CredentialRegistry) -> "JsonServiceAdapter":
|
|
33
|
+
return JsonServiceAdapter(
|
|
34
|
+
self._prepare_request,
|
|
35
|
+
self._service,
|
|
36
|
+
credentials,
|
|
37
|
+
self._destination_policy,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def supports(self, resource: Resource, dependencies: FrozenSet[str]) -> bool:
|
|
41
|
+
return (
|
|
42
|
+
self.name in dependencies
|
|
43
|
+
and isinstance(resource.access_plan, ServiceQueryPlan)
|
|
44
|
+
and resource.media_type == "application/json"
|
|
45
|
+
and resource.access_plan.options.get("service") == self._service
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def open(
|
|
49
|
+
self,
|
|
50
|
+
resource: Resource,
|
|
51
|
+
runtime: Any,
|
|
52
|
+
*,
|
|
53
|
+
destination_policy: DestinationPolicy | None = None,
|
|
54
|
+
) -> Any:
|
|
55
|
+
self._authorize_resource(resource, destination_policy)
|
|
56
|
+
params, headers = self._prepare_request(resource.access_plan, self._credentials)
|
|
57
|
+
try:
|
|
58
|
+
response = runtime.get(
|
|
59
|
+
resource.uri,
|
|
60
|
+
params=params,
|
|
61
|
+
headers=headers,
|
|
62
|
+
timeout=30,
|
|
63
|
+
allow_redirects=False,
|
|
64
|
+
)
|
|
65
|
+
if 300 <= response.status_code < 400:
|
|
66
|
+
raise ResourceAccessError("Service redirects are not supported")
|
|
67
|
+
response.raise_for_status()
|
|
68
|
+
except Exception:
|
|
69
|
+
# requests exceptions may include a URL containing the secret query.
|
|
70
|
+
raise ResourceAccessError("JSON service request failed") from None
|
|
71
|
+
try:
|
|
72
|
+
data = response.json()
|
|
73
|
+
except Exception:
|
|
74
|
+
raise ProviderResponseError("Service returned invalid JSON") from None
|
|
75
|
+
if resource.access_plan.options.get("response_type") == "array" and (
|
|
76
|
+
not isinstance(data, list)
|
|
77
|
+
or any(not isinstance(item, Mapping) for item in cast(List[Any], data))
|
|
78
|
+
):
|
|
79
|
+
raise ProviderResponseError("Service must return an array of objects")
|
|
80
|
+
return cast(Any, data)
|
|
81
|
+
|
|
82
|
+
def authorize(
|
|
83
|
+
self,
|
|
84
|
+
resource: Resource,
|
|
85
|
+
*,
|
|
86
|
+
destination_policy: DestinationPolicy | None = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Authorize credential release before resolving the service runtime."""
|
|
89
|
+
self._authorize_resource(resource, destination_policy)
|
|
90
|
+
|
|
91
|
+
def _authorize_resource(
|
|
92
|
+
self,
|
|
93
|
+
resource: Resource,
|
|
94
|
+
destination_policy: DestinationPolicy | None,
|
|
95
|
+
) -> None:
|
|
96
|
+
credential = resource.access_plan.options.get("credential")
|
|
97
|
+
(destination_policy or self._destination_policy).authorize(
|
|
98
|
+
resource.uri,
|
|
99
|
+
credentialed=True,
|
|
100
|
+
provider=resource.provenance.provider,
|
|
101
|
+
service=self._service,
|
|
102
|
+
credential=credential if isinstance(credential, str) else None,
|
|
103
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# pyogrio Execution Adapter
|
|
2
|
+
|
|
3
|
+
`PyogrioAdapter` は選択済み vector Resource を利用者提供の pyogrio runtime へ渡します。
|
|
4
|
+
|
|
5
|
+
- `name`: `pyogrio`、優先度: `10`
|
|
6
|
+
- 対応形式: Shapefile, GeoJSON, GeoPackage, FlatGeobuf
|
|
7
|
+
- 翻訳: 確定済み `encoding` を `read_dataframe()` option へ渡します。
|
|
8
|
+
- runtime: `read_dataframe(uri, **options)`
|
|
9
|
+
|
|
10
|
+
Resource の再選択、データ解析、形式・CRS 変換は行いません。
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Pyogrio execution adapter."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, FrozenSet
|
|
4
|
+
|
|
5
|
+
from ....errors import ResourceAccessError
|
|
6
|
+
from ....models import Resource
|
|
7
|
+
from ....security import DestinationPolicy
|
|
8
|
+
from .._resource import resource_attributes
|
|
9
|
+
from ..base import ExecutionAdapter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PyogrioAdapter(ExecutionAdapter):
|
|
13
|
+
"""Delegate vector Resources to a supplied pyogrio module."""
|
|
14
|
+
|
|
15
|
+
name = "pyogrio"
|
|
16
|
+
priority = 10
|
|
17
|
+
_formats = frozenset({"shapefile", "geojson", "gpkg", "flatgeobuf"})
|
|
18
|
+
|
|
19
|
+
def __init__(self, destination_policy: DestinationPolicy | None = None) -> None:
|
|
20
|
+
super().__init__(destination_policy)
|
|
21
|
+
|
|
22
|
+
def supports(self, resource: Resource, dependencies: FrozenSet[str]) -> bool:
|
|
23
|
+
return (
|
|
24
|
+
resource.format or ""
|
|
25
|
+
).lower() in self._formats and self.name in dependencies
|
|
26
|
+
|
|
27
|
+
def open(
|
|
28
|
+
self,
|
|
29
|
+
resource: Resource,
|
|
30
|
+
runtime: Any,
|
|
31
|
+
*,
|
|
32
|
+
destination_policy: DestinationPolicy | None = None,
|
|
33
|
+
) -> Any:
|
|
34
|
+
attributes = resource_attributes(resource)
|
|
35
|
+
options: Dict[str, Any] = {}
|
|
36
|
+
encoding = attributes.get("encoding")
|
|
37
|
+
if isinstance(encoding, str):
|
|
38
|
+
options["encoding"] = encoding
|
|
39
|
+
try:
|
|
40
|
+
return runtime.read_dataframe(resource.uri, **options)
|
|
41
|
+
except Exception as error:
|
|
42
|
+
raise ResourceAccessError(
|
|
43
|
+
f"pyogrio could not open {resource.uri!r}"
|
|
44
|
+
) from error
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Rasterio execution adapter."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, FrozenSet
|
|
4
|
+
|
|
5
|
+
from ....errors import ResourceAccessError
|
|
6
|
+
from ....models import Resource
|
|
7
|
+
from ....security import DestinationPolicy
|
|
8
|
+
from ..base import ExecutionAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RasterioAdapter(ExecutionAdapter):
|
|
12
|
+
"""Delegate raster Resources to a supplied Rasterio module."""
|
|
13
|
+
|
|
14
|
+
name = "rasterio"
|
|
15
|
+
priority = 15
|
|
16
|
+
_formats = frozenset({"cog", "geotiff"})
|
|
17
|
+
|
|
18
|
+
def __init__(self, destination_policy: DestinationPolicy | None = None) -> None:
|
|
19
|
+
super().__init__(destination_policy)
|
|
20
|
+
|
|
21
|
+
def supports(self, resource: Resource, dependencies: FrozenSet[str]) -> bool:
|
|
22
|
+
return (
|
|
23
|
+
resource.format or ""
|
|
24
|
+
).lower() in self._formats and self.name in dependencies
|
|
25
|
+
|
|
26
|
+
def open(
|
|
27
|
+
self,
|
|
28
|
+
resource: Resource,
|
|
29
|
+
runtime: Any,
|
|
30
|
+
*,
|
|
31
|
+
destination_policy: DestinationPolicy | None = None,
|
|
32
|
+
) -> Any:
|
|
33
|
+
try:
|
|
34
|
+
return runtime.open(resource.uri)
|
|
35
|
+
except Exception as error:
|
|
36
|
+
raise ResourceAccessError(
|
|
37
|
+
f"Rasterio could not open {resource.uri!r}"
|
|
38
|
+
) from error
|