earthscope-sdk 1.1.0__py3-none-any.whl → 1.2.0b0__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.
- earthscope_sdk/__init__.py +1 -1
- earthscope_sdk/client/_client.py +47 -0
- earthscope_sdk/client/data_access/__init__.py +0 -0
- earthscope_sdk/client/data_access/_arrow/__init__.py +0 -0
- earthscope_sdk/client/data_access/_arrow/_common.py +94 -0
- earthscope_sdk/client/data_access/_arrow/_gnss.py +116 -0
- earthscope_sdk/client/data_access/_base.py +85 -0
- earthscope_sdk/client/data_access/_query_plan/__init__.py +0 -0
- earthscope_sdk/client/data_access/_query_plan/_gnss_observations.py +295 -0
- earthscope_sdk/client/data_access/_query_plan/_query_plan.py +259 -0
- earthscope_sdk/client/data_access/_query_plan/_request_set.py +133 -0
- earthscope_sdk/client/data_access/_service.py +114 -0
- earthscope_sdk/client/discovery/__init__.py +0 -0
- earthscope_sdk/client/discovery/_base.py +303 -0
- earthscope_sdk/client/discovery/_service.py +209 -0
- earthscope_sdk/client/discovery/models.py +144 -0
- earthscope_sdk/common/context.py +71 -1
- earthscope_sdk/common/service.py +10 -8
- earthscope_sdk/config/models.py +14 -1
- earthscope_sdk/util/__init__.py +0 -0
- earthscope_sdk/util/_concurrency.py +64 -0
- earthscope_sdk/util/_itertools.py +57 -0
- earthscope_sdk/util/_time.py +57 -0
- earthscope_sdk/util/_types.py +5 -0
- {earthscope_sdk-1.1.0.dist-info → earthscope_sdk-1.2.0b0.dist-info}/METADATA +11 -1
- earthscope_sdk-1.2.0b0.dist-info/RECORD +49 -0
- earthscope_sdk-1.1.0.dist-info/RECORD +0 -30
- {earthscope_sdk-1.1.0.dist-info → earthscope_sdk-1.2.0b0.dist-info}/WHEEL +0 -0
- {earthscope_sdk-1.1.0.dist-info → earthscope_sdk-1.2.0b0.dist-info}/licenses/LICENSE +0 -0
- {earthscope_sdk-1.1.0.dist-info → earthscope_sdk-1.2.0b0.dist-info}/top_level.txt +0 -0
earthscope_sdk/__init__.py
CHANGED
earthscope_sdk/client/_client.py
CHANGED
@@ -8,6 +8,26 @@ class AsyncEarthScopeClient(AsyncSdkClient):
|
|
8
8
|
An async client for interacting with api.earthscope.org
|
9
9
|
"""
|
10
10
|
|
11
|
+
@cached_property
|
12
|
+
def data(self):
|
13
|
+
"""
|
14
|
+
Data access functionality
|
15
|
+
"""
|
16
|
+
# lazy load
|
17
|
+
from earthscope_sdk.client.data_access._service import AsyncDataAccessService
|
18
|
+
|
19
|
+
return AsyncDataAccessService(self)
|
20
|
+
|
21
|
+
@cached_property
|
22
|
+
def discover(self):
|
23
|
+
"""
|
24
|
+
Data discovery functionality
|
25
|
+
"""
|
26
|
+
# lazy load
|
27
|
+
from earthscope_sdk.client.discovery._service import AsyncDiscoveryService
|
28
|
+
|
29
|
+
return AsyncDiscoveryService(self._ctx)
|
30
|
+
|
11
31
|
@cached_property
|
12
32
|
def user(self):
|
13
33
|
"""
|
@@ -24,6 +44,33 @@ class EarthScopeClient(SdkClient):
|
|
24
44
|
A client for interacting with api.earthscope.org
|
25
45
|
"""
|
26
46
|
|
47
|
+
@cached_property
|
48
|
+
def _async_client(self):
|
49
|
+
"""
|
50
|
+
An async client for interacting with api.earthscope.org
|
51
|
+
"""
|
52
|
+
return AsyncEarthScopeClient(ctx=self._ctx)
|
53
|
+
|
54
|
+
@cached_property
|
55
|
+
def data(self):
|
56
|
+
"""
|
57
|
+
Data access functionality
|
58
|
+
"""
|
59
|
+
# lazy load
|
60
|
+
from earthscope_sdk.client.data_access._service import DataAccessService
|
61
|
+
|
62
|
+
return DataAccessService(self._async_client)
|
63
|
+
|
64
|
+
@cached_property
|
65
|
+
def discover(self):
|
66
|
+
"""
|
67
|
+
Data discovery functionality
|
68
|
+
"""
|
69
|
+
# lazy load
|
70
|
+
from earthscope_sdk.client.discovery._service import DiscoveryService
|
71
|
+
|
72
|
+
return DiscoveryService(self._ctx)
|
73
|
+
|
27
74
|
@cached_property
|
28
75
|
def user(self):
|
29
76
|
"""
|
File without changes
|
File without changes
|
@@ -0,0 +1,94 @@
|
|
1
|
+
try:
|
2
|
+
import pyarrow as pa
|
3
|
+
except ModuleNotFoundError as e:
|
4
|
+
raise ModuleNotFoundError(
|
5
|
+
"Optional dependency 'pyarrow' is required for this feature. "
|
6
|
+
"Install it with: pip install earthscope-sdk[arrow]"
|
7
|
+
) from e
|
8
|
+
|
9
|
+
|
10
|
+
from earthscope_sdk.client.discovery.models import DatasourceBaseModel
|
11
|
+
|
12
|
+
|
13
|
+
def convert_time(
|
14
|
+
table: pa.Table,
|
15
|
+
*,
|
16
|
+
source_field: str = "time",
|
17
|
+
target_field: str = "timestamp",
|
18
|
+
unit: str = "ms",
|
19
|
+
tz: str = "UTC",
|
20
|
+
) -> pa.Table:
|
21
|
+
"""
|
22
|
+
Convert a unix timestamp (integer since epoch) field to a timestamp type.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
table: The table to convert.
|
26
|
+
source_field: The name of the field to convert.
|
27
|
+
target_field: The name of the field to store the result.
|
28
|
+
unit: The unit of the time field.
|
29
|
+
tz: The timezone of the time field.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
The converted table.
|
33
|
+
"""
|
34
|
+
if unit not in {"s", "ms", "us", "ns"}:
|
35
|
+
raise ValueError(f"Invalid time unit: {unit}")
|
36
|
+
|
37
|
+
time_idx = table.schema.get_field_index(source_field)
|
38
|
+
if time_idx < 0:
|
39
|
+
return table
|
40
|
+
|
41
|
+
return table.set_column(
|
42
|
+
time_idx,
|
43
|
+
target_field,
|
44
|
+
table[source_field].cast(pa.timestamp(unit, tz)),
|
45
|
+
)
|
46
|
+
|
47
|
+
|
48
|
+
def get_datasource_metadata_table(
|
49
|
+
datasources: list[DatasourceBaseModel],
|
50
|
+
*,
|
51
|
+
fields: list[str],
|
52
|
+
namespaces: list[str],
|
53
|
+
) -> pa.Table:
|
54
|
+
"""
|
55
|
+
Get a pyarrow table containing metadata columns for a list of datasources.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
datasources: The list of datasources to get metadata for.
|
59
|
+
fields: The fields to include in the metadata table.
|
60
|
+
namespaces: The namespaces to include in the metadata table.
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
The metadata table.
|
64
|
+
"""
|
65
|
+
return pa.Table.from_pylist(
|
66
|
+
[s.to_arrow_columns(fields=fields, namespaces=namespaces) for s in datasources]
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
def load_table_with_extra(content: bytes, *, is_stream=True, **kwargs) -> pa.Table:
|
71
|
+
"""
|
72
|
+
Load a table from an arrow stream or file. Optionally add extra metadata columns.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
content: The content of the stream or file.
|
76
|
+
is_stream: Whether the content is an arrow stream or an arrow file.
|
77
|
+
**kwargs: The extra metadata columns to add to the table.
|
78
|
+
|
79
|
+
Returns:
|
80
|
+
The loaded table.
|
81
|
+
"""
|
82
|
+
if is_stream:
|
83
|
+
reader = pa.ipc.open_stream(content)
|
84
|
+
else:
|
85
|
+
reader = pa.ipc.open_file(content)
|
86
|
+
|
87
|
+
table = reader.read_all()
|
88
|
+
|
89
|
+
if kwargs:
|
90
|
+
count = len(table)
|
91
|
+
for key, value in kwargs.items():
|
92
|
+
table = table.append_column(key, pa.repeat(value, count))
|
93
|
+
|
94
|
+
return table
|
@@ -0,0 +1,116 @@
|
|
1
|
+
import pyarrow as pa
|
2
|
+
import pyarrow.compute as pc
|
3
|
+
|
4
|
+
from earthscope_sdk.client.data_access._arrow._common import convert_time
|
5
|
+
|
6
|
+
_SYSTEM_ID = "system_id"
|
7
|
+
_SYSTEM_TABLE = pa.Table.from_pylist(
|
8
|
+
[
|
9
|
+
{_SYSTEM_ID: 1, "system": "G"}, # GPS
|
10
|
+
{_SYSTEM_ID: 2, "system": "R"}, # GLONASS
|
11
|
+
{_SYSTEM_ID: 3, "system": "S"}, # SBAS
|
12
|
+
{_SYSTEM_ID: 4, "system": "E"}, # Galileo
|
13
|
+
{_SYSTEM_ID: 5, "system": "C"}, # BeiDou
|
14
|
+
{_SYSTEM_ID: 6, "system": "J"}, # QZSS
|
15
|
+
{_SYSTEM_ID: 7, "system": "I"}, # IRNSS/NavIC
|
16
|
+
],
|
17
|
+
schema=pa.schema(
|
18
|
+
[
|
19
|
+
pa.field(_SYSTEM_ID, pa.uint8()),
|
20
|
+
pa.field("system", pa.string()),
|
21
|
+
]
|
22
|
+
),
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
def _convert_obs_code(
|
27
|
+
table: pa.Table,
|
28
|
+
*,
|
29
|
+
source_field: str = "obs",
|
30
|
+
target_field: str = "obs_code",
|
31
|
+
) -> pa.Table:
|
32
|
+
"""
|
33
|
+
Convert the obs_code field from packed chars to a string.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
table: The table to convert.
|
37
|
+
source_field: The name of the field to convert.
|
38
|
+
target_field: The name of the field to store the result.
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
The converted table.
|
42
|
+
"""
|
43
|
+
# extract obs_code (encoded as packed chars)
|
44
|
+
result_chunks = [
|
45
|
+
pc.cast(
|
46
|
+
pa.FixedSizeBinaryArray.from_buffers(
|
47
|
+
type=pa.binary(2),
|
48
|
+
length=len(chunk),
|
49
|
+
buffers=[None, chunk.buffers()[1]],
|
50
|
+
),
|
51
|
+
pa.string(),
|
52
|
+
)
|
53
|
+
for chunk in table[source_field].chunks
|
54
|
+
]
|
55
|
+
|
56
|
+
return table.set_column(
|
57
|
+
table.schema.get_field_index(source_field),
|
58
|
+
target_field,
|
59
|
+
# can't easily swap endianness of packed chars, so reverse the string
|
60
|
+
pc.utf8_reverse(pa.chunked_array(result_chunks, type=pa.string())),
|
61
|
+
)
|
62
|
+
|
63
|
+
|
64
|
+
def _convert_system(table: pa.Table, *, field: str) -> pa.Table:
|
65
|
+
"""
|
66
|
+
Convert the sys field from integer to string.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
table: The table to convert.
|
70
|
+
field: The name of the field to convert.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
The converted table.
|
74
|
+
"""
|
75
|
+
return table.join(_SYSTEM_TABLE, field, right_keys=_SYSTEM_ID).drop(field)
|
76
|
+
|
77
|
+
|
78
|
+
def prettify_observations_table(table: pa.Table) -> pa.Table:
|
79
|
+
"""
|
80
|
+
Clean up a raw observations table (as stored in TileDB) for easier use.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
table: The table to prettify.
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
The prettified table.
|
87
|
+
"""
|
88
|
+
# Cast time column (integer milliseconds since epoch) to timestamp type
|
89
|
+
table = convert_time(table)
|
90
|
+
|
91
|
+
# Expose friendlier column names
|
92
|
+
table = table.rename_columns({"sat": "satellite"})
|
93
|
+
|
94
|
+
# Convert system column from integer to string
|
95
|
+
table = _convert_system(table, field="sys")
|
96
|
+
|
97
|
+
# Convert obs_code column from packed chars to string
|
98
|
+
table = _convert_obs_code(table)
|
99
|
+
|
100
|
+
return table
|
101
|
+
|
102
|
+
|
103
|
+
def prettify_positions_table(table: pa.Table) -> pa.Table:
|
104
|
+
"""
|
105
|
+
Clean up a raw positions table (as stored in TileDB) for easier use.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
table: The table to prettify.
|
109
|
+
|
110
|
+
Returns:
|
111
|
+
The prettified table.
|
112
|
+
"""
|
113
|
+
# Convert time column (integer milliseconds since epoch) to timestamp type
|
114
|
+
table = convert_time(table)
|
115
|
+
|
116
|
+
return table
|
@@ -0,0 +1,85 @@
|
|
1
|
+
import datetime as dt
|
2
|
+
from typing import TYPE_CHECKING, Union
|
3
|
+
|
4
|
+
from earthscope_sdk.client.data_access._arrow._common import load_table_with_extra
|
5
|
+
from earthscope_sdk.common.service import SdkService
|
6
|
+
from earthscope_sdk.util._time import to_utc_dt
|
7
|
+
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from pyarrow import Table
|
10
|
+
|
11
|
+
|
12
|
+
class DataAccessBaseService(SdkService):
|
13
|
+
"""
|
14
|
+
L1 data access service functionality
|
15
|
+
"""
|
16
|
+
|
17
|
+
async def _get_gnss_observations(
|
18
|
+
self,
|
19
|
+
*,
|
20
|
+
session_edid: str,
|
21
|
+
start_datetime: dt.datetime,
|
22
|
+
end_datetime: dt.datetime,
|
23
|
+
system: Union[str, list[str]] = [],
|
24
|
+
satellite: Union[str, list[str]] = [],
|
25
|
+
obs_code: Union[str, list[str]] = [],
|
26
|
+
field: Union[str, list[str]] = [],
|
27
|
+
) -> "Table":
|
28
|
+
"""
|
29
|
+
Retrieve GNSS observations.
|
30
|
+
|
31
|
+
This method retrieves GNSS observations for a given session.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
session_edid: The session EDID to retrieve observations for.
|
35
|
+
start_datetime: The start datetime to retrieve positions for.
|
36
|
+
end_datetime: The end datetime to retrieve positions for.
|
37
|
+
system: The system(s) to retrieve observations for.
|
38
|
+
satellite: The satellite(s) to retrieve observations for.
|
39
|
+
obs_code: The observation code(s) to retrieve observations for.
|
40
|
+
field: The field(s) to retrieve observations for.
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
A pyarrow table containing the GNSS observations.
|
44
|
+
"""
|
45
|
+
|
46
|
+
# lazy import
|
47
|
+
|
48
|
+
headers = {
|
49
|
+
"accept": "application/vnd.apache.arrow.stream",
|
50
|
+
}
|
51
|
+
|
52
|
+
params = {
|
53
|
+
"session_id": session_edid,
|
54
|
+
"start_datetime": to_utc_dt(start_datetime).isoformat(),
|
55
|
+
"end_datetime": to_utc_dt(end_datetime).isoformat(),
|
56
|
+
}
|
57
|
+
|
58
|
+
if system:
|
59
|
+
params["system"] = system
|
60
|
+
|
61
|
+
if satellite:
|
62
|
+
params["satellite"] = satellite
|
63
|
+
|
64
|
+
if obs_code:
|
65
|
+
params["obs_code"] = obs_code
|
66
|
+
|
67
|
+
if field:
|
68
|
+
params["field"] = field
|
69
|
+
|
70
|
+
req = self.ctx.httpx_client.build_request(
|
71
|
+
method="GET",
|
72
|
+
url=f"{self.resources.api_url}beta/data-products/gnss/observations",
|
73
|
+
headers=headers,
|
74
|
+
params=params,
|
75
|
+
timeout=30,
|
76
|
+
)
|
77
|
+
|
78
|
+
resp = await self._send_with_retries(req)
|
79
|
+
|
80
|
+
return await self.ctx.run_in_executor(
|
81
|
+
load_table_with_extra,
|
82
|
+
resp.content,
|
83
|
+
# Add edid column to the table returned by the API
|
84
|
+
edid=session_edid,
|
85
|
+
)
|
File without changes
|
@@ -0,0 +1,295 @@
|
|
1
|
+
import datetime as dt
|
2
|
+
from typing import TYPE_CHECKING, Literal, NamedTuple, Optional
|
3
|
+
|
4
|
+
from typing_extensions import Self
|
5
|
+
|
6
|
+
from earthscope_sdk.client.data_access._arrow._common import (
|
7
|
+
get_datasource_metadata_table,
|
8
|
+
)
|
9
|
+
from earthscope_sdk.client.data_access._arrow._gnss import prettify_observations_table
|
10
|
+
from earthscope_sdk.client.data_access._query_plan._query_plan import (
|
11
|
+
AsyncQueryPlan,
|
12
|
+
QueryPlan,
|
13
|
+
)
|
14
|
+
from earthscope_sdk.client.discovery.models import SessionDatasource
|
15
|
+
from earthscope_sdk.util._itertools import to_set
|
16
|
+
from earthscope_sdk.util._time import TimePeriod, time_range_periods
|
17
|
+
from earthscope_sdk.util._types import ListOrItem
|
18
|
+
|
19
|
+
if TYPE_CHECKING:
|
20
|
+
import pyarrow as pa
|
21
|
+
|
22
|
+
from earthscope_sdk.client import AsyncEarthScopeClient
|
23
|
+
|
24
|
+
|
25
|
+
_MAX_EPOCHS_PER_REQUEST = 11520
|
26
|
+
"""
|
27
|
+
Maximum number of epochs per GNSS observations request.
|
28
|
+
"""
|
29
|
+
|
30
|
+
|
31
|
+
_NAMESPACES = {"igs", "4charid", "dataflow"}
|
32
|
+
|
33
|
+
|
34
|
+
GnssObservationField = Literal[
|
35
|
+
"range",
|
36
|
+
"phase",
|
37
|
+
"doppler",
|
38
|
+
"snr",
|
39
|
+
"slip",
|
40
|
+
"flags",
|
41
|
+
"fcn",
|
42
|
+
]
|
43
|
+
"""
|
44
|
+
Fields available when fetching GNSS observations.
|
45
|
+
|
46
|
+
- range: code / psuedorange
|
47
|
+
- phase: carrier phase
|
48
|
+
- doppler: doppler shift
|
49
|
+
- snr: signal to noise ratio
|
50
|
+
- slip: carrier phase cycle slip occurred
|
51
|
+
- flags: event flags
|
52
|
+
- fcn: GLONASS frequency channel number
|
53
|
+
"""
|
54
|
+
|
55
|
+
|
56
|
+
GnssObservationsMetaField = Literal[
|
57
|
+
"edid",
|
58
|
+
"igs",
|
59
|
+
"4charid",
|
60
|
+
"dataflow",
|
61
|
+
"sample_interval",
|
62
|
+
"roll",
|
63
|
+
]
|
64
|
+
"""
|
65
|
+
Metadata fields available for joining to GNSS observations.
|
66
|
+
"""
|
67
|
+
|
68
|
+
SatelliteSystem = Literal["G", "R", "E", "J", "C", "I", "S"]
|
69
|
+
"""
|
70
|
+
GNSS satellite system abbreviation.
|
71
|
+
|
72
|
+
- G: GPS
|
73
|
+
- R: GLONASS
|
74
|
+
- E: Galileo
|
75
|
+
- J: QZSS
|
76
|
+
- C: BeiDou
|
77
|
+
- I: IRNSS / NavIC
|
78
|
+
- S: SBAS
|
79
|
+
"""
|
80
|
+
|
81
|
+
|
82
|
+
class GnssObservationsRequest(NamedTuple):
|
83
|
+
"""
|
84
|
+
An individual request for GNSS observations.
|
85
|
+
"""
|
86
|
+
|
87
|
+
period: TimePeriod
|
88
|
+
session: SessionDatasource
|
89
|
+
|
90
|
+
|
91
|
+
def _get_max_request_period(session: SessionDatasource) -> dt.timedelta:
|
92
|
+
"""
|
93
|
+
Get the maximum request period for a session.
|
94
|
+
|
95
|
+
Even though the API supports up to 115200 epochs per request, we limit
|
96
|
+
request periods to nice round numbers that easily group into days.
|
97
|
+
"""
|
98
|
+
|
99
|
+
if session.sample_interval == dt.timedelta(milliseconds=200):
|
100
|
+
return dt.timedelta(minutes=30)
|
101
|
+
|
102
|
+
if session.sample_interval == dt.timedelta(seconds=1):
|
103
|
+
return dt.timedelta(hours=3)
|
104
|
+
|
105
|
+
# fallback to actual limit of 11520 epochs per request
|
106
|
+
# 15s -> 2 days
|
107
|
+
# 30s -> 4 days
|
108
|
+
# 1m -> 8 days
|
109
|
+
# etc.
|
110
|
+
return session.sample_interval * _MAX_EPOCHS_PER_REQUEST
|
111
|
+
|
112
|
+
|
113
|
+
class AsyncGnssObservationsQueryPlan(
|
114
|
+
AsyncQueryPlan[GnssObservationsRequest],
|
115
|
+
):
|
116
|
+
"""
|
117
|
+
A query plan for GNSS observations.
|
118
|
+
"""
|
119
|
+
|
120
|
+
def __init__(
|
121
|
+
self,
|
122
|
+
client: "AsyncEarthScopeClient",
|
123
|
+
# observation parameters
|
124
|
+
start_datetime: dt.datetime,
|
125
|
+
end_datetime: dt.datetime,
|
126
|
+
system: ListOrItem[SatelliteSystem] = [],
|
127
|
+
satellite: ListOrItem[str] = [],
|
128
|
+
obs_code: ListOrItem[str] = [],
|
129
|
+
field: ListOrItem[GnssObservationField] = [],
|
130
|
+
# session parameters
|
131
|
+
network_name: ListOrItem[str] = [],
|
132
|
+
network_edid: ListOrItem[str] = [],
|
133
|
+
station_name: ListOrItem[str] = [],
|
134
|
+
station_edid: ListOrItem[str] = [],
|
135
|
+
session_name: ListOrItem[str] = [],
|
136
|
+
session_edid: ListOrItem[str] = [],
|
137
|
+
roll: Optional[dt.timedelta] = None,
|
138
|
+
sample_interval: Optional[dt.timedelta] = None,
|
139
|
+
# metadata parameters
|
140
|
+
meta_fields: ListOrItem[GnssObservationsMetaField] = ["igs"],
|
141
|
+
):
|
142
|
+
if (
|
143
|
+
not network_name
|
144
|
+
and not network_edid
|
145
|
+
and not station_name
|
146
|
+
and not station_edid
|
147
|
+
and not session_name
|
148
|
+
and not session_edid
|
149
|
+
):
|
150
|
+
raise ValueError("Expected at least one station or session name or edid")
|
151
|
+
|
152
|
+
super().__init__(client)
|
153
|
+
|
154
|
+
# Common parameters
|
155
|
+
self.system = system
|
156
|
+
self.satellite = satellite
|
157
|
+
self.obs_code = obs_code
|
158
|
+
self.field = field
|
159
|
+
|
160
|
+
# Request parameters
|
161
|
+
self.period = TimePeriod(start=start_datetime, end=end_datetime)
|
162
|
+
|
163
|
+
# Session parameters
|
164
|
+
self.network_name = network_name
|
165
|
+
self.network_edid = network_edid
|
166
|
+
self.station_name = station_name
|
167
|
+
self.station_edid = station_edid
|
168
|
+
self.session_name = session_name
|
169
|
+
self.session_edid = session_edid
|
170
|
+
self.roll = roll
|
171
|
+
self.sample_interval = sample_interval
|
172
|
+
|
173
|
+
# Metadata parameters
|
174
|
+
meta_fields = to_set(meta_fields)
|
175
|
+
self.meta_namespaces = list(meta_fields & _NAMESPACES)
|
176
|
+
self.meta_fields = list(meta_fields - _NAMESPACES)
|
177
|
+
|
178
|
+
# Local state
|
179
|
+
self._meta_table: Optional["pa.Table"] = None
|
180
|
+
self._max_request_period: Optional[dt.timedelta] = None
|
181
|
+
|
182
|
+
async def _build_requests(self) -> list[GnssObservationsRequest]:
|
183
|
+
# Expand station/session names into session_edids
|
184
|
+
sessions: list[SessionDatasource]
|
185
|
+
sessions = await self._client.discover.list_session_datasources(
|
186
|
+
network_name=self.network_name,
|
187
|
+
network_edid=self.network_edid,
|
188
|
+
station_name=self.station_name,
|
189
|
+
station_edid=self.station_edid,
|
190
|
+
session_name=self.session_name,
|
191
|
+
session_edid=self.session_edid,
|
192
|
+
roll=self.roll,
|
193
|
+
sample_interval=self.sample_interval,
|
194
|
+
with_parents=True,
|
195
|
+
)
|
196
|
+
|
197
|
+
# Pre-compute metadata table
|
198
|
+
if self.meta_fields or self.meta_namespaces:
|
199
|
+
self._meta_table = get_datasource_metadata_table(
|
200
|
+
sessions,
|
201
|
+
fields=self.meta_fields + ["edid"], # edid is required for join
|
202
|
+
namespaces=self.meta_namespaces,
|
203
|
+
)
|
204
|
+
|
205
|
+
requests: list[GnssObservationsRequest] = []
|
206
|
+
for session in sessions:
|
207
|
+
max_period = _get_max_request_period(session)
|
208
|
+
if self._max_request_period is not None:
|
209
|
+
max_period = min(max_period, self._max_request_period)
|
210
|
+
|
211
|
+
time_periods = time_range_periods(
|
212
|
+
start=self.period.start,
|
213
|
+
end=self.period.end,
|
214
|
+
period=max_period,
|
215
|
+
)
|
216
|
+
requests.extend(GnssObservationsRequest(p, session) for p in time_periods)
|
217
|
+
|
218
|
+
return requests
|
219
|
+
|
220
|
+
async def _execute_one(self, args: GnssObservationsRequest) -> Optional["pa.Table"]:
|
221
|
+
from httpx import HTTPStatusError # lazy import
|
222
|
+
|
223
|
+
try:
|
224
|
+
return await self._client.data._get_gnss_observations(
|
225
|
+
start_datetime=args.period.start,
|
226
|
+
end_datetime=args.period.end,
|
227
|
+
session_edid=args.session.edid,
|
228
|
+
system=self.system,
|
229
|
+
satellite=self.satellite,
|
230
|
+
obs_code=self.obs_code,
|
231
|
+
field=self.field,
|
232
|
+
)
|
233
|
+
except HTTPStatusError as e:
|
234
|
+
if e.response.status_code != 404:
|
235
|
+
raise e
|
236
|
+
|
237
|
+
return None
|
238
|
+
|
239
|
+
def _hook(self, table: "pa.Table") -> "pa.Table":
|
240
|
+
table = prettify_observations_table(table)
|
241
|
+
|
242
|
+
# Add metadata columns
|
243
|
+
if self._meta_table:
|
244
|
+
table = table.join(self._meta_table, "edid")
|
245
|
+
|
246
|
+
# Drop edid column if not requested
|
247
|
+
if "edid" not in self.meta_fields:
|
248
|
+
table = table.drop_columns(["edid"])
|
249
|
+
|
250
|
+
return table
|
251
|
+
|
252
|
+
def group_by_day(self) -> Self:
|
253
|
+
"""
|
254
|
+
Group the requests by day.
|
255
|
+
|
256
|
+
This will configure the query plan to fetch data for all stations
|
257
|
+
one day at a time.
|
258
|
+
|
259
|
+
NOTE: This groups requests by their start times. Should a request span a
|
260
|
+
day boundary, it will only be included in the day of its start time.
|
261
|
+
"""
|
262
|
+
# Truncate requests to the day of their start time
|
263
|
+
self._max_request_period = dt.timedelta(days=1)
|
264
|
+
|
265
|
+
return self.group_by(lambda r: r.period.start.date())
|
266
|
+
|
267
|
+
def group_by_station(self) -> Self:
|
268
|
+
"""
|
269
|
+
Group the requests by station.
|
270
|
+
|
271
|
+
This will configure the query plan to fetch data for the entire time
|
272
|
+
range one station at a time.
|
273
|
+
"""
|
274
|
+
return self.group_by(lambda r: r.session.station.edid)
|
275
|
+
|
276
|
+
def sort_by_station(self, *, reverse: bool = False) -> Self:
|
277
|
+
"""
|
278
|
+
Sort the requests by station name.
|
279
|
+
"""
|
280
|
+
return self.sort_by(lambda r: r.session.station.names["IGS"], reverse=reverse)
|
281
|
+
|
282
|
+
|
283
|
+
class GnssObservationsQueryPlan(
|
284
|
+
QueryPlan[GnssObservationsRequest],
|
285
|
+
):
|
286
|
+
"""
|
287
|
+
A query plan for GNSS observations.
|
288
|
+
"""
|
289
|
+
|
290
|
+
def __init__(self, async_plan: AsyncGnssObservationsQueryPlan):
|
291
|
+
super().__init__(async_plan)
|
292
|
+
|
293
|
+
self.group_by_day = self._wrap_and_return_self(async_plan.group_by_day)
|
294
|
+
self.group_by_station = self._wrap_and_return_self(async_plan.group_by_station)
|
295
|
+
self.sort_by_station = self._wrap_and_return_self(async_plan.sort_by_station)
|