farsounder 0.1.0__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.
- farsounder-0.1.0/PKG-INFO +105 -0
- farsounder-0.1.0/README.md +94 -0
- farsounder-0.1.0/pyproject.toml +33 -0
- farsounder-0.1.0/setup.cfg +4 -0
- farsounder-0.1.0/src/farsounder/__init__.py +5 -0
- farsounder-0.1.0/src/farsounder/client/__init__.py +0 -0
- farsounder-0.1.0/src/farsounder/client/config.py +157 -0
- farsounder-0.1.0/src/farsounder/client/exceptions.py +2 -0
- farsounder-0.1.0/src/farsounder/client/history_types.py +91 -0
- farsounder-0.1.0/src/farsounder/client/requests.py +307 -0
- farsounder-0.1.0/src/farsounder/client/subscriber.py +190 -0
- farsounder-0.1.0/src/farsounder/proto/__init__.py +28 -0
- farsounder-0.1.0/src/farsounder/proto/proto/__init__.py +1 -0
- farsounder-0.1.0/src/farsounder/proto/proto/array_pb2.py +41 -0
- farsounder-0.1.0/src/farsounder/proto/proto/array_pb2.pyi +52 -0
- farsounder-0.1.0/src/farsounder/proto/proto/grid_description_pb2.py +41 -0
- farsounder-0.1.0/src/farsounder/proto/proto/grid_description_pb2.pyi +26 -0
- farsounder-0.1.0/src/farsounder/proto/proto/nav_api_pb2.py +103 -0
- farsounder-0.1.0/src/farsounder/proto/proto/nav_api_pb2.pyi +263 -0
- farsounder-0.1.0/src/farsounder/proto/proto/nav_info_pb2.py +63 -0
- farsounder-0.1.0/src/farsounder/proto/proto/nav_info_pb2.pyi +129 -0
- farsounder-0.1.0/src/farsounder/proto/proto/nmea_pb2.py +38 -0
- farsounder-0.1.0/src/farsounder/proto/proto/nmea_pb2.pyi +17 -0
- farsounder-0.1.0/src/farsounder/proto/proto/time_pb2.py +37 -0
- farsounder-0.1.0/src/farsounder/proto/proto/time_pb2.pyi +23 -0
- farsounder-0.1.0/src/farsounder.egg-info/PKG-INFO +105 -0
- farsounder-0.1.0/src/farsounder.egg-info/SOURCES.txt +32 -0
- farsounder-0.1.0/src/farsounder.egg-info/dependency_links.txt +1 -0
- farsounder-0.1.0/src/farsounder.egg-info/entry_points.txt +2 -0
- farsounder-0.1.0/src/farsounder.egg-info/requires.txt +3 -0
- farsounder-0.1.0/src/farsounder.egg-info/top_level.txt +1 -0
- farsounder-0.1.0/tests/test_config.py +34 -0
- farsounder-0.1.0/tests/test_exports.py +21 -0
- farsounder-0.1.0/tests/test_history_data.py +59 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: farsounder
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client to communicate with FarSounder's ARGOS sensors
|
|
5
|
+
Author-email: FarSounder Software Team <sw@farsounder.com>
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: httpx
|
|
9
|
+
Requires-Dist: protobuf
|
|
10
|
+
Requires-Dist: pyzmq
|
|
11
|
+
|
|
12
|
+
# SDK Client for live FarSounder data
|
|
13
|
+
|
|
14
|
+
Python client to communicate with SonaSoft via API.
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
Clone locally and install (uv add .) or install from pypi like:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
uv add farsounder
|
|
22
|
+
```
|
|
23
|
+
(or use pip, etc)
|
|
24
|
+
|
|
25
|
+
Then - for example to subscribe to `TargetData` messages:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import asyncio
|
|
29
|
+
|
|
30
|
+
from farsounder import config, requests, subscriber
|
|
31
|
+
from farsounder.proto import nav_api_pb2
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def main() -> None:
|
|
35
|
+
cfg = config.build_config(
|
|
36
|
+
host="127.0.0.1",
|
|
37
|
+
subscribe=["TargetData"],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
sub = subscriber.subscribe(config)
|
|
41
|
+
|
|
42
|
+
def on_targets(message: nav_api_pb2.TargetData) -> None:
|
|
43
|
+
print("Got a TargetData!")
|
|
44
|
+
print("Targets:", len(message.groups))
|
|
45
|
+
|
|
46
|
+
# Pub/Sub
|
|
47
|
+
sub.on("TargetData", on_targets)
|
|
48
|
+
await sub.start()
|
|
49
|
+
|
|
50
|
+
# Req/rep
|
|
51
|
+
settings = await requests.get_processor_settings(config)
|
|
52
|
+
print(settings)
|
|
53
|
+
|
|
54
|
+
# History data
|
|
55
|
+
history = await requests.get_history_data(
|
|
56
|
+
config,
|
|
57
|
+
latitude=41.7223,
|
|
58
|
+
longitude=-71.35,
|
|
59
|
+
radius_meters=500,
|
|
60
|
+
)
|
|
61
|
+
print(history.gridded_bottom_detections)
|
|
62
|
+
|
|
63
|
+
await asyncio.sleep(1.0)
|
|
64
|
+
await sub.stop()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
asyncio.run(main())
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Simulated backend
|
|
72
|
+
|
|
73
|
+
You can run a local simulated backend that publishes dummy messages and responds
|
|
74
|
+
to request/reply calls:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
uv run examples/simulated_backend.py
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Note: the `examples/` directory is not installed with `uv add farsounder`.
|
|
81
|
+
Clone the repo to run the examples locally.
|
|
82
|
+
|
|
83
|
+
# Development
|
|
84
|
+
|
|
85
|
+
## Re-generate Protobuf stubs
|
|
86
|
+
|
|
87
|
+
Protobuf sources live in `proto/`. Generate Python stubs with:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
uv run --group dev gen-protos
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
uv run tools/gen_protos.py
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Tests
|
|
100
|
+
|
|
101
|
+
Run tests with:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
uv run --group dev pytest
|
|
105
|
+
```
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# SDK Client for live FarSounder data
|
|
2
|
+
|
|
3
|
+
Python client to communicate with SonaSoft via API.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Clone locally and install (uv add .) or install from pypi like:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
uv add farsounder
|
|
11
|
+
```
|
|
12
|
+
(or use pip, etc)
|
|
13
|
+
|
|
14
|
+
Then - for example to subscribe to `TargetData` messages:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import asyncio
|
|
18
|
+
|
|
19
|
+
from farsounder import config, requests, subscriber
|
|
20
|
+
from farsounder.proto import nav_api_pb2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def main() -> None:
|
|
24
|
+
cfg = config.build_config(
|
|
25
|
+
host="127.0.0.1",
|
|
26
|
+
subscribe=["TargetData"],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
sub = subscriber.subscribe(config)
|
|
30
|
+
|
|
31
|
+
def on_targets(message: nav_api_pb2.TargetData) -> None:
|
|
32
|
+
print("Got a TargetData!")
|
|
33
|
+
print("Targets:", len(message.groups))
|
|
34
|
+
|
|
35
|
+
# Pub/Sub
|
|
36
|
+
sub.on("TargetData", on_targets)
|
|
37
|
+
await sub.start()
|
|
38
|
+
|
|
39
|
+
# Req/rep
|
|
40
|
+
settings = await requests.get_processor_settings(config)
|
|
41
|
+
print(settings)
|
|
42
|
+
|
|
43
|
+
# History data
|
|
44
|
+
history = await requests.get_history_data(
|
|
45
|
+
config,
|
|
46
|
+
latitude=41.7223,
|
|
47
|
+
longitude=-71.35,
|
|
48
|
+
radius_meters=500,
|
|
49
|
+
)
|
|
50
|
+
print(history.gridded_bottom_detections)
|
|
51
|
+
|
|
52
|
+
await asyncio.sleep(1.0)
|
|
53
|
+
await sub.stop()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
asyncio.run(main())
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Simulated backend
|
|
61
|
+
|
|
62
|
+
You can run a local simulated backend that publishes dummy messages and responds
|
|
63
|
+
to request/reply calls:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
uv run examples/simulated_backend.py
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Note: the `examples/` directory is not installed with `uv add farsounder`.
|
|
70
|
+
Clone the repo to run the examples locally.
|
|
71
|
+
|
|
72
|
+
# Development
|
|
73
|
+
|
|
74
|
+
## Re-generate Protobuf stubs
|
|
75
|
+
|
|
76
|
+
Protobuf sources live in `proto/`. Generate Python stubs with:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
uv run --group dev gen-protos
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Or:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
uv run tools/gen_protos.py
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Tests
|
|
89
|
+
|
|
90
|
+
Run tests with:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
uv run --group dev pytest
|
|
94
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "farsounder"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python client to communicate with FarSounder's ARGOS sensors"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "FarSounder Software Team", email = "sw@farsounder.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx",
|
|
12
|
+
"protobuf",
|
|
13
|
+
"pyzmq",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
gen-protos = "farsounder.tools.gen_protos:main"
|
|
18
|
+
|
|
19
|
+
[dependency-groups]
|
|
20
|
+
dev = [
|
|
21
|
+
"grpcio-tools",
|
|
22
|
+
"pytest",
|
|
23
|
+
"ruff>=0.14.14",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["setuptools>=68", "wheel"]
|
|
28
|
+
build-backend = "setuptools.build_meta"
|
|
29
|
+
|
|
30
|
+
[tool.ruff]
|
|
31
|
+
exclude = [
|
|
32
|
+
"src/farsounder/proto/proto/**",
|
|
33
|
+
]
|
|
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
|
+
)
|