-
Notifications
You must be signed in to change notification settings - Fork 2
/
stac_utils.py
181 lines (155 loc) · 5.43 KB
/
stac_utils.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import logging
import os
import re
from enum import Enum
from typing import Any, Literal, MutableMapping, Type, Union
import numpy as np
import pystac
import yaml
LOGGER = logging.getLogger(__name__)
def url_validate(target: str) -> bool:
"""Validate whether a supplied URL is reliably written.
Parameters
----------
target : str
References
----------
https://stackoverflow.com/a/7160778/7322852
"""
url_regex = re.compile(
r"^(?:http|ftp)s?://" # http:// or https://
# domain...
r"(?:(?:[A-Z\d](?:[A-Z\d-]{0,61}[A-Z\d])?\.)+(?:[A-Z]{2,6}\.?|[A-Z\d-]{2,}\.?)|"
r"localhost|" # localhost...
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
r"(?::\d+)?" # optional port
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
return True if re.match(url_regex, target) else False
def load_config(
config_file: Union[os.PathLike[str], str],
) -> MutableMapping[str, Any]:
"""Reads a generic YAML or JSON configuration file.
:raises OSError: If the configuration file is not present
:raises ValueError: If the configuration file is not correctly formatted.
:return: A python dictionary describing a generic configuration.
:rtype: MutableMapping[str, Any]
"""
if not os.path.isfile(config_file):
raise OSError(f"Missing configuration file does not exist: [{config_file}]")
with open(config_file) as f:
config_info = yaml.load(f, yaml.Loader)
if not isinstance(config_info, dict) or not config_info:
raise ValueError(f"Invalid configuration file does not define a mapping: [{config_file}]")
return config_info
def collection2literal(collection, property="label") -> "Type[Literal]":
terms = tuple(getattr(term, property) for term in collection)
return Literal[terms] # noqa
def ncattrs_to_geometry(attrs: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
"""Create Polygon geometry from CFMetadata."""
attrs = attrs["groups"]["CFMetadata"]["attributes"]
return {
"type": "Polygon",
"coordinates": [
[
[
float(attrs["geospatial_lon_min"][0]),
float(attrs["geospatial_lat_min"][0]),
],
[
float(attrs["geospatial_lon_min"][0]),
float(attrs["geospatial_lat_max"][0]),
],
[
float(attrs["geospatial_lon_max"][0]),
float(attrs["geospatial_lat_max"][0]),
],
[
float(attrs["geospatial_lon_max"][0]),
float(attrs["geospatial_lat_min"][0]),
],
[
float(attrs["geospatial_lon_min"][0]),
float(attrs["geospatial_lat_min"][0]),
],
]
],
}
def ncattrs_to_bbox(attrs: MutableMapping[str, Any]) -> list[float]:
"""Create BBOX from CFMetadata."""
attrs = attrs["groups"]["CFMetadata"]["attributes"]
return [
float(attrs["geospatial_lon_min"][0]),
float(attrs["geospatial_lat_min"][0]),
float(attrs["geospatial_lon_max"][0]),
float(attrs["geospatial_lat_max"][0]),
]
def numpy_to_python_datatypes(data: MutableMapping[str, Any]) -> MutableMapping[str, Any]:
# Converting numpy datatypes to python standard datatypes
for key, value in data.items():
if isinstance(value, list):
newlist = []
for item in value:
if issubclass(type(item), np.integer):
newlist.append(int(item))
elif issubclass(type(item), np.floating):
newlist.append(float(item))
else:
newlist.append(item)
data[key] = newlist
elif isinstance(type(value), np.integer):
data[key] = int(value)
return data
def magpie_resource_link(url: str) -> pystac.Link:
"""Creates a link that will be used by Cowbird to create a resource in Magpie
associated with the STAC item.
:param url: HTTPServer access URL for a STAC item
:type url: str
:return: A PySTAC Link
:rtype: pystac.Link
"""
url_ = url.replace("fileServer", "*")
i = url_.find("*")
title = url_[i + 2 :]
link = pystac.Link(rel="source", title=title, target=url, media_type="application/x-netcdf")
return link
class ServiceType(Enum):
adde = "ADDE"
dap4 = "DAP4"
dods = "DODS" # same as OpenDAP
opendap = "OpenDAP"
opendapg = "OpenDAPG"
netcdfsubset = "NetcdfSubset"
cdmremote = "CdmRemote"
cdmfeature = "CdmFeature"
ncjson = "ncJSON"
h5service = "H5Service"
httpserver = "HTTPServer"
ftp = "FTP"
gridftp = "GridFTP"
file = "File"
iso = "ISO"
las = "LAS"
ncml = "NcML"
uddc = "UDDC"
wcs = "WCS"
wms = "WMS"
wsdl = "WSDL"
webform = "WebForm"
catalog = "Catalog"
compound = "Compound"
resolver = "Resolver"
thredds = "THREDDS"
@classmethod
def from_value(cls, value: str, default: Any = KeyError) -> "ServiceType":
"""Return value irrespective of case."""
try:
svc = value.lower()
if svc.endswith("_service"): # handle NCML edge cases
svc = svc.rsplit("_", 1)[0]
return cls[svc]
except KeyError:
if default is not KeyError:
return default
raise