farsounder 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.
- farsounder/__init__.py +5 -0
- farsounder/client/__init__.py +0 -0
- farsounder/client/config.py +157 -0
- farsounder/client/exceptions.py +2 -0
- farsounder/client/history_types.py +91 -0
- farsounder/client/requests.py +307 -0
- farsounder/client/subscriber.py +190 -0
- farsounder/proto/__init__.py +28 -0
- farsounder/proto/proto/__init__.py +1 -0
- farsounder/proto/proto/array_pb2.py +41 -0
- farsounder/proto/proto/array_pb2.pyi +52 -0
- farsounder/proto/proto/grid_description_pb2.py +41 -0
- farsounder/proto/proto/grid_description_pb2.pyi +26 -0
- farsounder/proto/proto/nav_api_pb2.py +103 -0
- farsounder/proto/proto/nav_api_pb2.pyi +263 -0
- farsounder/proto/proto/nav_info_pb2.py +63 -0
- farsounder/proto/proto/nav_info_pb2.pyi +129 -0
- farsounder/proto/proto/nmea_pb2.py +38 -0
- farsounder/proto/proto/nmea_pb2.pyi +17 -0
- farsounder/proto/proto/time_pb2.py +37 -0
- farsounder/proto/proto/time_pb2.pyi +23 -0
- farsounder-0.1.0.dist-info/METADATA +105 -0
- farsounder-0.1.0.dist-info/RECORD +26 -0
- farsounder-0.1.0.dist-info/WHEEL +5 -0
- farsounder-0.1.0.dist-info/entry_points.txt +2 -0
- farsounder-0.1.0.dist-info/top_level.txt +1 -0
farsounder/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Iterable, Literal, Mapping
|
|
5
|
+
|
|
6
|
+
PubSubMessage = Literal[
|
|
7
|
+
"HydrophoneData",
|
|
8
|
+
"TargetData",
|
|
9
|
+
"ProcessorSettings",
|
|
10
|
+
"VesselInfo",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
ReqRepEndpoint = Literal[
|
|
14
|
+
"GetProcessorSettings",
|
|
15
|
+
"SetFieldOfView",
|
|
16
|
+
"SetBottomDetection",
|
|
17
|
+
"SetInWaterSquelch",
|
|
18
|
+
"SetSquelchlessInWaterDetector",
|
|
19
|
+
"GetVesselInfo",
|
|
20
|
+
"SetDraft",
|
|
21
|
+
"SetKeelOffset",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
RestEndpoint = Literal["GetHistoryData",]
|
|
25
|
+
|
|
26
|
+
PUBSUB_DEFAULT_PORTS: dict[PubSubMessage, int] = {
|
|
27
|
+
"HydrophoneData": 61501,
|
|
28
|
+
"TargetData": 61502,
|
|
29
|
+
"ProcessorSettings": 61503,
|
|
30
|
+
"VesselInfo": 61504,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
REQREP_DEFAULT_PORTS: dict[ReqRepEndpoint, int] = {
|
|
34
|
+
"GetProcessorSettings": 60501,
|
|
35
|
+
"SetFieldOfView": 60502,
|
|
36
|
+
"SetBottomDetection": 60503,
|
|
37
|
+
"SetInWaterSquelch": 60504,
|
|
38
|
+
"SetSquelchlessInWaterDetector": 60505,
|
|
39
|
+
"GetVesselInfo": 60506,
|
|
40
|
+
"SetDraft": 60507,
|
|
41
|
+
"SetKeelOffset": 60508,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
REST_DEFAULT_PORTS: dict[RestEndpoint, int] = {
|
|
45
|
+
"GetHistoryData": 3000,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class ClientConfig:
|
|
51
|
+
"""Configuration for the ZeroMQ client.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
host
|
|
56
|
+
Hostname or IP address for all sockets.
|
|
57
|
+
subscribe
|
|
58
|
+
Pub-sub message types to subscribe to.
|
|
59
|
+
pubsub_ports
|
|
60
|
+
Port mapping for pub-sub message types.
|
|
61
|
+
reqrep_ports
|
|
62
|
+
Port mapping for request-reply endpoints.
|
|
63
|
+
rest_ports
|
|
64
|
+
Port mapping for REST endpoints.
|
|
65
|
+
callback_executor
|
|
66
|
+
Whether to run sync callbacks in a thread pool or inline.
|
|
67
|
+
timeout_seconds
|
|
68
|
+
Request-reply timeout in seconds.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
host: str
|
|
72
|
+
subscribe: tuple[PubSubMessage, ...]
|
|
73
|
+
pubsub_ports: dict[PubSubMessage, int]
|
|
74
|
+
reqrep_ports: dict[ReqRepEndpoint, int]
|
|
75
|
+
rest_ports: dict[RestEndpoint, int]
|
|
76
|
+
callback_executor: Literal["threadpool", "inline"]
|
|
77
|
+
timeout_seconds: float
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_config(
|
|
81
|
+
host: str = "127.0.0.1",
|
|
82
|
+
subscribe: Iterable[PubSubMessage] | None = None,
|
|
83
|
+
port_overrides: Mapping[str, int] | None = None,
|
|
84
|
+
callback_executor: Literal["threadpool", "inline"] = "threadpool",
|
|
85
|
+
timeout_seconds: float = 2.0,
|
|
86
|
+
) -> ClientConfig:
|
|
87
|
+
"""Build a client configuration.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
host
|
|
92
|
+
Hostname or IP address for all sockets.
|
|
93
|
+
subscribe
|
|
94
|
+
Iterable of message types to subscribe to. Defaults to all pub-sub
|
|
95
|
+
messages.
|
|
96
|
+
port_overrides
|
|
97
|
+
Mapping of message or endpoint name to port number. Keys can be any
|
|
98
|
+
pub-sub message name, request-reply endpoint name, or REST endpoint
|
|
99
|
+
name such as GetHistoryData.
|
|
100
|
+
callback_executor
|
|
101
|
+
Whether to run sync callbacks in a thread pool or inline.
|
|
102
|
+
timeout_seconds
|
|
103
|
+
Request-reply timeout in seconds.
|
|
104
|
+
|
|
105
|
+
Returns
|
|
106
|
+
-------
|
|
107
|
+
ClientConfig
|
|
108
|
+
The constructed configuration.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
pubsub_ports = dict(PUBSUB_DEFAULT_PORTS)
|
|
112
|
+
reqrep_ports = dict(REQREP_DEFAULT_PORTS)
|
|
113
|
+
rest_ports = dict(REST_DEFAULT_PORTS)
|
|
114
|
+
|
|
115
|
+
if port_overrides:
|
|
116
|
+
for key, port in port_overrides.items():
|
|
117
|
+
if key in pubsub_ports:
|
|
118
|
+
pubsub_ports[key] = port
|
|
119
|
+
elif key in reqrep_ports:
|
|
120
|
+
reqrep_ports[key] = port
|
|
121
|
+
elif key in rest_ports:
|
|
122
|
+
rest_ports[key] = port
|
|
123
|
+
else:
|
|
124
|
+
raise ValueError(f"Unknown port override key: {key}")
|
|
125
|
+
|
|
126
|
+
subscribe_list = tuple(subscribe) if subscribe is not None else tuple(pubsub_ports)
|
|
127
|
+
for message_type in subscribe_list:
|
|
128
|
+
if message_type not in pubsub_ports:
|
|
129
|
+
raise ValueError(f"Unknown pub-sub message type: {message_type}")
|
|
130
|
+
|
|
131
|
+
return ClientConfig(
|
|
132
|
+
host=host,
|
|
133
|
+
subscribe=subscribe_list,
|
|
134
|
+
pubsub_ports=pubsub_ports,
|
|
135
|
+
reqrep_ports=reqrep_ports,
|
|
136
|
+
rest_ports=rest_ports,
|
|
137
|
+
callback_executor=callback_executor,
|
|
138
|
+
timeout_seconds=timeout_seconds,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def resolve_pubsub_port(config: ClientConfig, message_type: PubSubMessage) -> int:
|
|
143
|
+
"""Resolve the port for a pub-sub message type."""
|
|
144
|
+
|
|
145
|
+
return config.pubsub_ports[message_type]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def resolve_reqrep_port(config: ClientConfig, endpoint: ReqRepEndpoint) -> int:
|
|
149
|
+
"""Resolve the port for a request-reply endpoint."""
|
|
150
|
+
|
|
151
|
+
return config.reqrep_ports[endpoint]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def resolve_rest_port(config: ClientConfig, endpoint: RestEndpoint) -> int:
|
|
155
|
+
"""Resolve the port for a REST endpoint."""
|
|
156
|
+
|
|
157
|
+
return config.rest_ports[endpoint]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class GriddedBottomDetection:
|
|
9
|
+
timestamp_utc: float
|
|
10
|
+
latitude_degrees: float
|
|
11
|
+
longitude_degrees: float
|
|
12
|
+
grid_interval_meters: float
|
|
13
|
+
target_strength_db: float
|
|
14
|
+
is_tide_corrected: bool
|
|
15
|
+
uploaded_to_cloud: bool
|
|
16
|
+
number_of_points: int
|
|
17
|
+
depth_meters: float
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_dict(cls, data: dict[str, Any]) -> "GriddedBottomDetection":
|
|
21
|
+
return cls(
|
|
22
|
+
timestamp_utc=float(data["timestamp_utc"]),
|
|
23
|
+
latitude_degrees=float(data["latitude_degrees"]),
|
|
24
|
+
longitude_degrees=float(data["longitude_degrees"]),
|
|
25
|
+
grid_interval_meters=float(data["grid_interval_meters"]),
|
|
26
|
+
target_strength_db=float(data["target_strength_db"]),
|
|
27
|
+
is_tide_corrected=bool(data["is_tide_corrected"]),
|
|
28
|
+
uploaded_to_cloud=bool(data["uploaded_to_cloud"]),
|
|
29
|
+
number_of_points=int(data["number_of_points"]),
|
|
30
|
+
depth_meters=float(data["depth_meters"]),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class GriddedInwaterDetection:
|
|
36
|
+
timestamp_utc: float
|
|
37
|
+
latitude_degrees: float
|
|
38
|
+
longitude_degrees: float
|
|
39
|
+
grid_interval_meters: float
|
|
40
|
+
target_strength_db: float
|
|
41
|
+
is_tide_corrected: bool
|
|
42
|
+
uploaded_to_cloud: bool
|
|
43
|
+
number_of_points: int
|
|
44
|
+
deepest_depth_meters: float | None
|
|
45
|
+
shallowest_depth_meters: float | None
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_dict(cls, data: dict[str, Any]) -> "GriddedInwaterDetection":
|
|
49
|
+
return cls(
|
|
50
|
+
timestamp_utc=float(data["timestamp_utc"]),
|
|
51
|
+
latitude_degrees=float(data["latitude_degrees"]),
|
|
52
|
+
longitude_degrees=float(data["longitude_degrees"]),
|
|
53
|
+
grid_interval_meters=float(data["grid_interval_meters"]),
|
|
54
|
+
target_strength_db=float(data["target_strength_db"]),
|
|
55
|
+
is_tide_corrected=bool(data["is_tide_corrected"]),
|
|
56
|
+
uploaded_to_cloud=bool(data["uploaded_to_cloud"]),
|
|
57
|
+
number_of_points=int(data["number_of_points"]),
|
|
58
|
+
deepest_depth_meters=(
|
|
59
|
+
float(data["deepest_depth_meters"])
|
|
60
|
+
if data.get("deepest_depth_meters") is not None
|
|
61
|
+
else None
|
|
62
|
+
),
|
|
63
|
+
shallowest_depth_meters=(
|
|
64
|
+
float(data["shallowest_depth_meters"])
|
|
65
|
+
if data.get("shallowest_depth_meters") is not None
|
|
66
|
+
else None
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True)
|
|
72
|
+
class HistoryData:
|
|
73
|
+
gridded_bottom_detections: list[GriddedBottomDetection]
|
|
74
|
+
gridded_inwater_detections: list[GriddedInwaterDetection]
|
|
75
|
+
history_count: int | None = None
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_dict(
|
|
79
|
+
cls, data: dict[str, Any], *, history_count: int | None = None
|
|
80
|
+
) -> "HistoryData":
|
|
81
|
+
bottom_raw = data.get("gridded_bottom_detections", [])
|
|
82
|
+
inwater_raw = data.get("gridded_inwater_detections", [])
|
|
83
|
+
return cls(
|
|
84
|
+
gridded_bottom_detections=[
|
|
85
|
+
GriddedBottomDetection.from_dict(item) for item in bottom_raw
|
|
86
|
+
],
|
|
87
|
+
gridded_inwater_detections=[
|
|
88
|
+
GriddedInwaterDetection.from_dict(item) for item in inwater_raw
|
|
89
|
+
],
|
|
90
|
+
history_count=history_count,
|
|
91
|
+
)
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import zmq
|
|
8
|
+
import zmq.asyncio
|
|
9
|
+
from google.protobuf.message import Message
|
|
10
|
+
|
|
11
|
+
from farsounder.proto.proto import nav_api_pb2 as nav_api
|
|
12
|
+
from farsounder.client.config import (
|
|
13
|
+
ClientConfig,
|
|
14
|
+
ReqRepEndpoint,
|
|
15
|
+
resolve_reqrep_port,
|
|
16
|
+
resolve_rest_port,
|
|
17
|
+
)
|
|
18
|
+
from farsounder.client.exceptions import RequestTimeoutError
|
|
19
|
+
from farsounder.client.history_types import HistoryData
|
|
20
|
+
|
|
21
|
+
ResponseT = TypeVar("ResponseT", bound=Message)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def _request(
|
|
25
|
+
config: ClientConfig,
|
|
26
|
+
endpoint: ReqRepEndpoint,
|
|
27
|
+
request: Message,
|
|
28
|
+
response_cls: type[ResponseT],
|
|
29
|
+
) -> ResponseT:
|
|
30
|
+
ctx = zmq.asyncio.Context.instance()
|
|
31
|
+
socket = ctx.socket(zmq.REQ)
|
|
32
|
+
try:
|
|
33
|
+
port = resolve_reqrep_port(config, endpoint)
|
|
34
|
+
socket.connect(f"tcp://{config.host}:{port}")
|
|
35
|
+
await socket.send(request.SerializeToString())
|
|
36
|
+
try:
|
|
37
|
+
data = await asyncio.wait_for(socket.recv(), timeout=config.timeout_seconds)
|
|
38
|
+
except asyncio.TimeoutError as exc:
|
|
39
|
+
raise RequestTimeoutError(
|
|
40
|
+
f"Timeout waiting for {endpoint} after {config.timeout_seconds:.1f}s"
|
|
41
|
+
) from exc
|
|
42
|
+
finally:
|
|
43
|
+
socket.close(linger=0)
|
|
44
|
+
|
|
45
|
+
response = response_cls()
|
|
46
|
+
response.ParseFromString(data)
|
|
47
|
+
return response
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def get_processor_settings(
|
|
51
|
+
config: ClientConfig,
|
|
52
|
+
) -> nav_api.GetProcessorSettingsResponse:
|
|
53
|
+
"""Request the current processor settings.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
config
|
|
58
|
+
Client configuration.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
nav_api.GetProcessorSettingsResponse
|
|
63
|
+
Response containing the current processor settings.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
request = nav_api.GetProcessorSettingsRequest()
|
|
67
|
+
return await _request(
|
|
68
|
+
config,
|
|
69
|
+
"GetProcessorSettings",
|
|
70
|
+
request,
|
|
71
|
+
nav_api.GetProcessorSettingsResponse,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def set_field_of_view(
|
|
76
|
+
fov: nav_api.FieldOfView,
|
|
77
|
+
config: ClientConfig,
|
|
78
|
+
) -> nav_api.SetFieldOfViewResponse:
|
|
79
|
+
"""Request a change to the sonar FieldOfView.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
fov
|
|
84
|
+
Desired field of view value.
|
|
85
|
+
config
|
|
86
|
+
Client configuration.
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
nav_api.SetFieldOfViewResponse
|
|
91
|
+
Response describing the result of the request.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
request = nav_api.SetFieldOfViewRequest(fov=fov)
|
|
95
|
+
return await _request(
|
|
96
|
+
config,
|
|
97
|
+
"SetFieldOfView",
|
|
98
|
+
request,
|
|
99
|
+
nav_api.SetFieldOfViewResponse,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def set_bottom_detection(
|
|
104
|
+
enable: bool,
|
|
105
|
+
config: ClientConfig,
|
|
106
|
+
) -> nav_api.SetBottomDetectionResponse:
|
|
107
|
+
"""Enable or disable bottom detection.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
enable
|
|
112
|
+
Whether to enable bottom detection.
|
|
113
|
+
config
|
|
114
|
+
Client configuration.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
nav_api.SetBottomDetectionResponse
|
|
119
|
+
Response describing the result of the request.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
request = nav_api.SetBottomDetectionRequest(enable_bottom_detection=enable)
|
|
123
|
+
return await _request(
|
|
124
|
+
config,
|
|
125
|
+
"SetBottomDetection",
|
|
126
|
+
request,
|
|
127
|
+
nav_api.SetBottomDetectionResponse,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
async def set_inwater_squelch(
|
|
132
|
+
value: float,
|
|
133
|
+
config: ClientConfig,
|
|
134
|
+
) -> nav_api.SetInWaterSquelchResponse:
|
|
135
|
+
"""Set the in-water squelch value.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
value
|
|
140
|
+
Squelch value to set.
|
|
141
|
+
config
|
|
142
|
+
Client configuration.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
nav_api.SetInWaterSquelchResponse
|
|
147
|
+
Response describing the result of the request.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
request = nav_api.SetInWaterSquelchRequest(new_squelch_val=value)
|
|
151
|
+
return await _request(
|
|
152
|
+
config,
|
|
153
|
+
"SetInWaterSquelch",
|
|
154
|
+
request,
|
|
155
|
+
nav_api.SetInWaterSquelchResponse,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def set_squelchless_inwater_detector(
|
|
160
|
+
enable: bool,
|
|
161
|
+
config: ClientConfig,
|
|
162
|
+
) -> nav_api.SetSquelchlessInWaterDetectorResponse:
|
|
163
|
+
"""Enable or disable the squelchless in-water detector.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
enable
|
|
168
|
+
Whether to enable squelchless detection.
|
|
169
|
+
config
|
|
170
|
+
Client configuration.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
nav_api.SetSquelchlessInWaterDetectorResponse
|
|
175
|
+
Response describing the result of the request.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
request = nav_api.SetSquelchlessInWaterDetectorRequest(
|
|
179
|
+
enable_squelchless_detection=enable
|
|
180
|
+
)
|
|
181
|
+
return await _request(
|
|
182
|
+
config,
|
|
183
|
+
"SetSquelchlessInWaterDetector",
|
|
184
|
+
request,
|
|
185
|
+
nav_api.SetSquelchlessInWaterDetectorResponse,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
async def get_vessel_info(
|
|
190
|
+
config: ClientConfig,
|
|
191
|
+
) -> nav_api.GetVesselInfoResponse:
|
|
192
|
+
"""Request the current vessel info.
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
config
|
|
197
|
+
Client configuration.
|
|
198
|
+
|
|
199
|
+
Returns
|
|
200
|
+
-------
|
|
201
|
+
nav_api.GetVesselInfoResponse
|
|
202
|
+
Response containing the vessel info.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
request = nav_api.GetVesselInfoRequest()
|
|
206
|
+
return await _request(
|
|
207
|
+
config, "GetVesselInfo", request, nav_api.GetVesselInfoResponse
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _build_history_params(
|
|
212
|
+
*,
|
|
213
|
+
latitude: float,
|
|
214
|
+
longitude: float,
|
|
215
|
+
radius_meters: float,
|
|
216
|
+
since_timestamp_utc: float | None,
|
|
217
|
+
tide_corrected_only: bool,
|
|
218
|
+
skip: int,
|
|
219
|
+
limit: int,
|
|
220
|
+
include_count: bool,
|
|
221
|
+
) -> dict[str, str]:
|
|
222
|
+
params: dict[str, str] = {
|
|
223
|
+
"latitude": str(latitude),
|
|
224
|
+
"longitude": str(longitude),
|
|
225
|
+
"radius_meters": str(radius_meters),
|
|
226
|
+
"tide_corrected_only": str(tide_corrected_only).lower(),
|
|
227
|
+
"skip": str(skip),
|
|
228
|
+
"limit": str(limit),
|
|
229
|
+
"include_count": str(include_count).lower(),
|
|
230
|
+
}
|
|
231
|
+
if since_timestamp_utc is not None:
|
|
232
|
+
params["since_timestamp_utc"] = str(since_timestamp_utc)
|
|
233
|
+
return params
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
async def get_history_data(
|
|
237
|
+
config: ClientConfig,
|
|
238
|
+
*,
|
|
239
|
+
latitude: float,
|
|
240
|
+
longitude: float,
|
|
241
|
+
radius_meters: float = 500,
|
|
242
|
+
since_timestamp_utc: float | None = None,
|
|
243
|
+
tide_corrected_only: bool = False,
|
|
244
|
+
skip: int = 0,
|
|
245
|
+
limit: int = 50000,
|
|
246
|
+
include_count: bool = True,
|
|
247
|
+
) -> HistoryData:
|
|
248
|
+
"""Request history data from the REST API.
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
----------
|
|
252
|
+
config
|
|
253
|
+
Client configuration.
|
|
254
|
+
latitude
|
|
255
|
+
Latitude in decimal degrees.
|
|
256
|
+
longitude
|
|
257
|
+
Longitude in decimal degrees.
|
|
258
|
+
radius_meters
|
|
259
|
+
Query radius in meters.
|
|
260
|
+
since_timestamp_utc
|
|
261
|
+
Return data collected or updated since this UTC epoch time.
|
|
262
|
+
tide_corrected_only
|
|
263
|
+
Only return tide corrected seafloor detections.
|
|
264
|
+
skip
|
|
265
|
+
Number of records to skip.
|
|
266
|
+
limit
|
|
267
|
+
Maximum number of records to return.
|
|
268
|
+
include_count
|
|
269
|
+
Include the total count in the response header.
|
|
270
|
+
|
|
271
|
+
Returns
|
|
272
|
+
-------
|
|
273
|
+
HistoryData
|
|
274
|
+
Parsed history data response.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
port = resolve_rest_port(config, "GetHistoryData")
|
|
278
|
+
params = _build_history_params(
|
|
279
|
+
latitude=latitude,
|
|
280
|
+
longitude=longitude,
|
|
281
|
+
radius_meters=radius_meters,
|
|
282
|
+
since_timestamp_utc=since_timestamp_utc,
|
|
283
|
+
tide_corrected_only=tide_corrected_only,
|
|
284
|
+
skip=skip,
|
|
285
|
+
limit=limit,
|
|
286
|
+
include_count=include_count,
|
|
287
|
+
)
|
|
288
|
+
url = f"http://{config.host}:{port}/api/history_data"
|
|
289
|
+
try:
|
|
290
|
+
async with httpx.AsyncClient(timeout=config.timeout_seconds) as client:
|
|
291
|
+
response = await client.get(url, params=params)
|
|
292
|
+
response.raise_for_status()
|
|
293
|
+
except httpx.ConnectError as exc:
|
|
294
|
+
raise httpx.ConnectError(
|
|
295
|
+
f"Failed to connect to {url}, is the server running?"
|
|
296
|
+
) from exc
|
|
297
|
+
|
|
298
|
+
history_count = None
|
|
299
|
+
if include_count:
|
|
300
|
+
header_val = response.headers.get("X-Grids-Count")
|
|
301
|
+
if header_val is not None:
|
|
302
|
+
try:
|
|
303
|
+
history_count = int(header_val)
|
|
304
|
+
except ValueError:
|
|
305
|
+
history_count = None
|
|
306
|
+
payload = response.json()
|
|
307
|
+
return HistoryData.from_dict(payload, history_count=history_count)
|