plexus-python-common 1.0.31__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.
- plexus/common/__init__.py +6 -0
- plexus/common/carto/OSMFile.py +259 -0
- plexus/common/carto/OSMNode.py +25 -0
- plexus/common/carto/OSMTags.py +101 -0
- plexus/common/carto/OSMWay.py +24 -0
- plexus/common/carto/__init__.py +11 -0
- plexus/common/pose.py +107 -0
- plexus/common/proj.py +305 -0
- plexus/common/utils/__init__.py +0 -0
- plexus/common/utils/apiutils.py +31 -0
- plexus/common/utils/bagutils.py +215 -0
- plexus/common/utils/config.py +61 -0
- plexus/common/utils/datautils.py +200 -0
- plexus/common/utils/jsonutils.py +92 -0
- plexus/common/utils/ormutils.py +1428 -0
- plexus/common/utils/s3utils.py +799 -0
- plexus/common/utils/shutils.py +234 -0
- plexus/common/utils/sqlutils.py +9 -0
- plexus/common/utils/strutils.py +382 -0
- plexus/common/utils/testutils.py +49 -0
- plexus_python_common-1.0.31.dist-info/METADATA +38 -0
- plexus_python_common-1.0.31.dist-info/RECORD +24 -0
- plexus_python_common-1.0.31.dist-info/WHEEL +5 -0
- plexus_python_common-1.0.31.dist-info/top_level.txt +1 -0
plexus/common/proj.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from typing import Annotated, Self
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pydantic as pdt
|
|
7
|
+
import pyproj
|
|
8
|
+
from iker.common.utils.funcutils import singleton
|
|
9
|
+
from iker.common.utils.strutils import parse_params_string, repr_data, str_conv
|
|
10
|
+
|
|
11
|
+
from plexus.common.pose import Pose
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Proj(ABC):
|
|
15
|
+
@classmethod
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def typename(cls) -> str:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def __init__(self, spec):
|
|
21
|
+
self.spec = spec
|
|
22
|
+
|
|
23
|
+
def __str__(self):
|
|
24
|
+
return self.to_string()
|
|
25
|
+
|
|
26
|
+
def method(self) -> str:
|
|
27
|
+
return self.typename()
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def ellipsoid(self) -> str:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def from_latlon(self, lat: float, lon: float) -> tuple[float, float]:
|
|
35
|
+
"""
|
|
36
|
+
Performs forward projection from latitude/longitude to projection coordinate system
|
|
37
|
+
|
|
38
|
+
:param lat: latitude
|
|
39
|
+
:param lon: longitude
|
|
40
|
+
|
|
41
|
+
:return: tuple of x and y in projection coordinate system
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def to_latlon(self, x: float, y: float) -> tuple[float, float]:
|
|
47
|
+
"""
|
|
48
|
+
Performs inverse projection from projection coordinate system to latitude/longitude
|
|
49
|
+
|
|
50
|
+
:param x: x in projection coordinate system
|
|
51
|
+
:param y: y in projection coordinate system
|
|
52
|
+
|
|
53
|
+
:return: tuple of latitude and longitude
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def to_string(self) -> str:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class EQDCProj(Proj):
|
|
63
|
+
"""
|
|
64
|
+
Equidistant conic projection
|
|
65
|
+
Please refer to https://en.wikipedia.org/wiki/Equidistant_conic_projection
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
class Spec(pdt.BaseModel):
|
|
69
|
+
lat_1: Annotated[float, pdt.Strict(), pdt.AllowInfNan(False)]
|
|
70
|
+
lat_2: Annotated[float, pdt.Strict(), pdt.AllowInfNan(False)]
|
|
71
|
+
lon_0: Annotated[float, pdt.Strict(), pdt.AllowInfNan(False)] = 0.0
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def typename(cls) -> str:
|
|
75
|
+
return "eqdc"
|
|
76
|
+
|
|
77
|
+
def __init__(self, **kwargs):
|
|
78
|
+
super(EQDCProj, self).__init__(EQDCProj.Spec(**kwargs))
|
|
79
|
+
self.proj = self.make_proj()
|
|
80
|
+
|
|
81
|
+
def ellipsoid(self) -> str:
|
|
82
|
+
return "WGS84"
|
|
83
|
+
|
|
84
|
+
def from_latlon(self, lat: float, lon: float) -> tuple[float, float]:
|
|
85
|
+
x, y = self.proj(lon, lat)
|
|
86
|
+
return x, y
|
|
87
|
+
|
|
88
|
+
def to_latlon(self, x: float, y: float) -> tuple[float, float]:
|
|
89
|
+
lon, lat = self.proj(x, y, inverse=True)
|
|
90
|
+
return lat, lon
|
|
91
|
+
|
|
92
|
+
def to_string(self) -> str:
|
|
93
|
+
return "{method}:lat_1={},lat_2={},lon_0={}".format(
|
|
94
|
+
self.spec.lat_1,
|
|
95
|
+
self.spec.lat_2,
|
|
96
|
+
self.spec.lon_0,
|
|
97
|
+
method=self.method(),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def make_proj(self):
|
|
101
|
+
return pyproj.Proj(
|
|
102
|
+
"+proj={method} +ellps={ellipsoid} +datum={ellipsoid} +units=m +no_defs +lat_1={} +lat_2={} +lon_0={}".format(
|
|
103
|
+
self.spec.lat_1,
|
|
104
|
+
self.spec.lat_2,
|
|
105
|
+
self.spec.lon_0,
|
|
106
|
+
method=self.method(),
|
|
107
|
+
ellipsoid=self.ellipsoid(),
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class UTMProj(Proj):
|
|
113
|
+
"""
|
|
114
|
+
Universal Transverse Mercator coordinate system
|
|
115
|
+
Please refer to https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
class Spec(pdt.BaseModel):
|
|
119
|
+
zone: Annotated[int, pdt.Strict()]
|
|
120
|
+
south: Annotated[bool, pdt.Strict()] = False
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def typename(cls) -> str:
|
|
124
|
+
return "utm"
|
|
125
|
+
|
|
126
|
+
def __init__(self, **kwargs):
|
|
127
|
+
super(UTMProj, self).__init__(UTMProj.Spec(**kwargs))
|
|
128
|
+
self.proj = self.make_proj()
|
|
129
|
+
|
|
130
|
+
def ellipsoid(self) -> str:
|
|
131
|
+
return "WGS84"
|
|
132
|
+
|
|
133
|
+
def from_latlon(self, lat: float, lon: float) -> tuple[float, float]:
|
|
134
|
+
x, y = self.proj(lon, lat)
|
|
135
|
+
return x, y
|
|
136
|
+
|
|
137
|
+
def to_latlon(self, x: float, y: float) -> tuple[float, float]:
|
|
138
|
+
lon, lat = self.proj(x, y, inverse=True)
|
|
139
|
+
return lat, lon
|
|
140
|
+
|
|
141
|
+
def to_string(self) -> str:
|
|
142
|
+
return "{method}:zone={},south={}".format(self.spec.zone, self.spec.south, method=self.method())
|
|
143
|
+
|
|
144
|
+
def make_proj(self):
|
|
145
|
+
return pyproj.Proj(
|
|
146
|
+
"+proj={method} +ellps={ellipsoid} +datum={ellipsoid} +units=m +no_defs +zone={} {}".format(
|
|
147
|
+
self.spec.zone,
|
|
148
|
+
"+south" if self.spec.south else "",
|
|
149
|
+
method=self.method(),
|
|
150
|
+
ellipsoid=self.ellipsoid(),
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class WebMercProj(Proj):
|
|
156
|
+
"""
|
|
157
|
+
Web Mercator projection
|
|
158
|
+
Please refer to https://en.wikipedia.org/wiki/Web_Mercator_projection
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
class Spec(pdt.BaseModel):
|
|
162
|
+
lon_0: Annotated[float, pdt.Strict(), pdt.AllowInfNan(False)] = 0.0
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def typename(cls) -> str:
|
|
166
|
+
return "webmerc"
|
|
167
|
+
|
|
168
|
+
def __init__(self, **kwargs):
|
|
169
|
+
super(WebMercProj, self).__init__(WebMercProj.Spec(**kwargs))
|
|
170
|
+
self.proj = self.make_proj()
|
|
171
|
+
|
|
172
|
+
def ellipsoid(self) -> str:
|
|
173
|
+
return "WGS84"
|
|
174
|
+
|
|
175
|
+
def from_latlon(self, lat: float, lon: float) -> tuple[float, float]:
|
|
176
|
+
x, y = self.proj(lon, lat)
|
|
177
|
+
return x, y
|
|
178
|
+
|
|
179
|
+
def to_latlon(self, x: float, y: float) -> tuple[float, float]:
|
|
180
|
+
lon, lat = self.proj(x, y, inverse=True)
|
|
181
|
+
return lat, lon
|
|
182
|
+
|
|
183
|
+
def to_string(self) -> str:
|
|
184
|
+
return "{method}:lon_0={}".format(self.spec.lon_0, method=self.method())
|
|
185
|
+
|
|
186
|
+
def make_proj(self):
|
|
187
|
+
return pyproj.Proj(
|
|
188
|
+
"+proj={method} +ellps={ellipsoid} +datum={ellipsoid} +units=m +no_defs +lon_0={}".format(
|
|
189
|
+
self.spec.lon_0,
|
|
190
|
+
method=self.method(),
|
|
191
|
+
ellipsoid=self.ellipsoid(),
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@singleton
|
|
197
|
+
def default_proj() -> Proj:
|
|
198
|
+
return make_proj("eqdc:lat_1=30.0,lat_2=50.0")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def make_proj(spec_str: str) -> Proj | None:
|
|
202
|
+
"""
|
|
203
|
+
Constructs cartographic projection from the given name and arguments
|
|
204
|
+
|
|
205
|
+
:param spec_str: projection spec string
|
|
206
|
+
|
|
207
|
+
:return: projection
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
if spec_str is None:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
match spec_str.split(":", maxsplit=1):
|
|
214
|
+
case [name]:
|
|
215
|
+
kwargs = {}
|
|
216
|
+
case [name, args]:
|
|
217
|
+
kwargs = parse_params_string(args, str_parser=str_conv)
|
|
218
|
+
case _:
|
|
219
|
+
raise ValueError("bad spec string")
|
|
220
|
+
|
|
221
|
+
if name.lower() == UTMProj.typename():
|
|
222
|
+
return UTMProj(**kwargs)
|
|
223
|
+
if name.lower() == EQDCProj.typename():
|
|
224
|
+
return EQDCProj(**kwargs)
|
|
225
|
+
if name.lower() == WebMercProj.typename():
|
|
226
|
+
return WebMercProj(**kwargs)
|
|
227
|
+
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class Coord(object):
|
|
232
|
+
@classmethod
|
|
233
|
+
def from_pose(cls, pose: Pose, proj: Proj | None) -> Self:
|
|
234
|
+
"""
|
|
235
|
+
Creates a coord from given pose
|
|
236
|
+
|
|
237
|
+
:param pose: pose based on which the coord is created
|
|
238
|
+
:param proj: cartographic projection
|
|
239
|
+
|
|
240
|
+
:return: generated coord
|
|
241
|
+
"""
|
|
242
|
+
return cls.from_xy(pose.p[0], pose.p[1], pose.p[2], proj, pose.ts)
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def from_latlon(cls, lat: float, lon: float, ele: float, proj: Proj | None, ts: float = 0) -> Self:
|
|
246
|
+
"""
|
|
247
|
+
Creates a coord from latitude/longitude
|
|
248
|
+
|
|
249
|
+
:param lat: latitude
|
|
250
|
+
:param lon: longitude
|
|
251
|
+
:param ele: elevation
|
|
252
|
+
:param proj: cartographic projection
|
|
253
|
+
:param ts: timestamp
|
|
254
|
+
|
|
255
|
+
:return: generated coord
|
|
256
|
+
"""
|
|
257
|
+
if proj is None:
|
|
258
|
+
return Coord(ts, lat, lon, ele, 0.0, 0.0, pdt.BaseModel())
|
|
259
|
+
|
|
260
|
+
x, y = proj.from_latlon(lat, lon)
|
|
261
|
+
return Coord(ts, lat, lon, ele, x, y, proj.spec)
|
|
262
|
+
|
|
263
|
+
@classmethod
|
|
264
|
+
def from_xy(cls, x: float, y: float, ele: float, proj: Proj | None, ts: float = 0) -> Self:
|
|
265
|
+
"""
|
|
266
|
+
Creates a coord from coordinate in cartographic projection
|
|
267
|
+
|
|
268
|
+
:param x: x coordinate in cartographic projection
|
|
269
|
+
:param y: y coordinate in cartographic projection
|
|
270
|
+
:param ele: elevation
|
|
271
|
+
:param proj: cartographic projection
|
|
272
|
+
:param ts: timestamp
|
|
273
|
+
|
|
274
|
+
:return: a coord
|
|
275
|
+
"""
|
|
276
|
+
if proj is None:
|
|
277
|
+
return Coord(ts, 0.0, 0.0, ele, x, y, pdt.BaseModel())
|
|
278
|
+
|
|
279
|
+
lat, lon = proj.to_latlon(x, y)
|
|
280
|
+
return Coord(ts, lat, lon, ele, x, y, proj.spec)
|
|
281
|
+
|
|
282
|
+
def __init__(self, ts: float, lat: float, lon: float, ele: float, x: float, y: float, spec: pdt.BaseModel):
|
|
283
|
+
"""
|
|
284
|
+
Represents a coordinate
|
|
285
|
+
|
|
286
|
+
:param ts: timestamp
|
|
287
|
+
:param lat: latitude
|
|
288
|
+
:param lon: longitude
|
|
289
|
+
:param ele: elevation
|
|
290
|
+
:param x: x in projection coordinate system
|
|
291
|
+
:param y: y in projection coordinate system
|
|
292
|
+
:param spec: projection specification
|
|
293
|
+
"""
|
|
294
|
+
self.ts = ts
|
|
295
|
+
self.lat = lat
|
|
296
|
+
self.lon = lon
|
|
297
|
+
self.x = x
|
|
298
|
+
self.y = y
|
|
299
|
+
self.spec = spec
|
|
300
|
+
self.ele = ele
|
|
301
|
+
self.xy = np.array([x, y], dtype=np.float64)
|
|
302
|
+
self.xyz = np.array([x, y, ele], dtype=np.float64)
|
|
303
|
+
|
|
304
|
+
def __str__(self):
|
|
305
|
+
return repr_data(self)
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from contextlib import AbstractContextManager
|
|
2
|
+
|
|
3
|
+
import sqlalchemy.orm as sa_orm
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"managed_db_session",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DBSessionExceptionManager(AbstractContextManager):
|
|
12
|
+
def __init__(self, db: sa_orm.Session, commit_on_exit: bool):
|
|
13
|
+
self.db = db
|
|
14
|
+
self.commit_on_exit = commit_on_exit
|
|
15
|
+
|
|
16
|
+
def __enter__(self):
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
20
|
+
if isinstance(exc_type, HTTPException):
|
|
21
|
+
return False # Propagate HTTPException
|
|
22
|
+
if exc_type is not None:
|
|
23
|
+
self.db.rollback() # Raise a new HTTPException (or any other exception type)
|
|
24
|
+
raise HTTPException(status_code=500, detail=str(exc_value)) from exc_value
|
|
25
|
+
if self.commit_on_exit:
|
|
26
|
+
self.db.commit()
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def managed_db_session(db: sa_orm.Session, commit_on_exit: bool = True) -> DBSessionExceptionManager:
|
|
31
|
+
return DBSessionExceptionManager(db, commit_on_exit)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Generator
|
|
3
|
+
from contextlib import AbstractContextManager
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
import sqlalchemy as sa
|
|
7
|
+
import sqlalchemy.orm as sa_orm
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"SerializationFormat",
|
|
11
|
+
"BagTopic",
|
|
12
|
+
"BagMessage",
|
|
13
|
+
"BagSchema",
|
|
14
|
+
"BagMessageDefinition",
|
|
15
|
+
"BagMetadata",
|
|
16
|
+
"BagReader",
|
|
17
|
+
"BagWriter",
|
|
18
|
+
"bag_reader",
|
|
19
|
+
"bag_writer",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
SerializationFormat = Literal["cdr", "cdr2", "json", "yaml"]
|
|
23
|
+
|
|
24
|
+
default_bag_db_file = "bag.db"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BagBase(sa_orm.DeclarativeBase):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BagSchema(BagBase):
|
|
32
|
+
__tablename__ = "schema"
|
|
33
|
+
|
|
34
|
+
schema_version: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, primary_key=True)
|
|
35
|
+
ros_distro: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BagMetadata(BagBase):
|
|
39
|
+
__tablename__ = "metadata"
|
|
40
|
+
|
|
41
|
+
id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
42
|
+
metadata_version: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, nullable=False)
|
|
43
|
+
metadata_text: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class BagTopic(BagBase):
|
|
47
|
+
__tablename__ = "topics"
|
|
48
|
+
|
|
49
|
+
id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
50
|
+
name: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
51
|
+
type: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
52
|
+
serialization_format: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
53
|
+
type_description_hash: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
54
|
+
|
|
55
|
+
messages: sa_orm.Mapped[list["BagMessage"]] = sa_orm.relationship("BagMessage", back_populates="topic")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class BagMessageDefinition(BagBase):
|
|
59
|
+
__tablename__ = "message_definitions"
|
|
60
|
+
|
|
61
|
+
id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
62
|
+
topic_type: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
63
|
+
encoding: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
64
|
+
encoded_message_definition: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
65
|
+
type_description_hash: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BagMessage(BagBase):
|
|
69
|
+
__tablename__ = "messages"
|
|
70
|
+
|
|
71
|
+
id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
72
|
+
topic_id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, sa.ForeignKey("topics.id"), nullable=False)
|
|
73
|
+
timestamp: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, nullable=False)
|
|
74
|
+
data: sa_orm.Mapped[bytes] = sa_orm.mapped_column(sa.LargeBinary, nullable=False)
|
|
75
|
+
|
|
76
|
+
topic: sa_orm.Mapped["BagTopic"] = sa_orm.relationship("BagTopic", back_populates="messages")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class BagReader(AbstractContextManager):
|
|
80
|
+
def __init__(self, db_dir: str, db_file: str = default_bag_db_file):
|
|
81
|
+
self.db_path = os.path.join(db_dir, db_file)
|
|
82
|
+
if not os.path.exists(self.db_path):
|
|
83
|
+
raise FileNotFoundError(f"could not find SQLite DB at '{db_dir}'")
|
|
84
|
+
|
|
85
|
+
self.engine = sa.create_engine(f"sqlite:///{self.db_path}")
|
|
86
|
+
self.session = sa_orm.Session(self.engine)
|
|
87
|
+
self.topic_map = None
|
|
88
|
+
|
|
89
|
+
def __enter__(self):
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
93
|
+
self.close()
|
|
94
|
+
|
|
95
|
+
def close(self):
|
|
96
|
+
self.session.close()
|
|
97
|
+
self.engine.dispose()
|
|
98
|
+
|
|
99
|
+
def get_topics(self) -> dict[str, BagTopic]:
|
|
100
|
+
if self.topic_map is None:
|
|
101
|
+
stmt = sa.select(BagTopic)
|
|
102
|
+
self.topic_map = {db_topic.name: db_topic for db_topic in self.session.scalars(stmt).all()}
|
|
103
|
+
return self.topic_map
|
|
104
|
+
|
|
105
|
+
def iter_messages(
|
|
106
|
+
self,
|
|
107
|
+
topic_names: list[str] | None = None,
|
|
108
|
+
begin_timestamp: int | None = None,
|
|
109
|
+
end_timestamp: int | None = None,
|
|
110
|
+
) -> Generator[BagMessage, None, None]:
|
|
111
|
+
stmt = sa.select(BagMessage)
|
|
112
|
+
if topic_names is not None:
|
|
113
|
+
topic_ids = [db_topic.id for db_topic in self.get_topics().values() if db_topic.name in topic_names]
|
|
114
|
+
stmt = stmt.where(BagMessage.topic_id.in_(topic_ids))
|
|
115
|
+
if begin_timestamp is not None:
|
|
116
|
+
stmt = stmt.where(BagMessage.timestamp >= begin_timestamp)
|
|
117
|
+
if end_timestamp is not None:
|
|
118
|
+
stmt = stmt.where(BagMessage.timestamp <= end_timestamp)
|
|
119
|
+
for db_message in self.session.scalars(stmt.order_by(BagMessage.timestamp)):
|
|
120
|
+
yield db_message
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class BagWriter(AbstractContextManager):
|
|
124
|
+
def __init__(self, db_dir: str, db_file: str = default_bag_db_file):
|
|
125
|
+
self.db_path = os.path.join(db_dir, db_file)
|
|
126
|
+
if os.path.exists(self.db_path):
|
|
127
|
+
os.remove(self.db_path)
|
|
128
|
+
|
|
129
|
+
self.engine = sa.create_engine(f"sqlite:///{self.db_path}")
|
|
130
|
+
self.session = sa_orm.Session(self.engine)
|
|
131
|
+
self.topic_map = {}
|
|
132
|
+
|
|
133
|
+
BagBase.metadata.create_all(self.engine)
|
|
134
|
+
|
|
135
|
+
def __enter__(self):
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
139
|
+
self.close()
|
|
140
|
+
|
|
141
|
+
def close(self) -> None:
|
|
142
|
+
self.session.commit()
|
|
143
|
+
self.session.close()
|
|
144
|
+
self.engine.dispose()
|
|
145
|
+
|
|
146
|
+
def get_or_create_topic(
|
|
147
|
+
self,
|
|
148
|
+
topic_name: str,
|
|
149
|
+
topic_type: str,
|
|
150
|
+
*,
|
|
151
|
+
serialization_format: SerializationFormat = "cdr",
|
|
152
|
+
) -> BagTopic:
|
|
153
|
+
if topic_name in self.topic_map:
|
|
154
|
+
return self.topic_map[topic_name]
|
|
155
|
+
|
|
156
|
+
db_topic = self.session.query(BagTopic).filter(BagTopic.name == topic_name).first()
|
|
157
|
+
if db_topic is None:
|
|
158
|
+
db_topic = BagTopic(name=topic_name,
|
|
159
|
+
type=topic_type,
|
|
160
|
+
serialization_format=serialization_format,
|
|
161
|
+
type_description_hash="")
|
|
162
|
+
self.session.add(db_topic)
|
|
163
|
+
self.session.flush()
|
|
164
|
+
|
|
165
|
+
self.topic_map[topic_name] = db_topic
|
|
166
|
+
self.session.commit()
|
|
167
|
+
|
|
168
|
+
return db_topic
|
|
169
|
+
|
|
170
|
+
def write_message(
|
|
171
|
+
self,
|
|
172
|
+
topic_name: str,
|
|
173
|
+
timestamp: int,
|
|
174
|
+
data: bytes,
|
|
175
|
+
topic_type: str = None,
|
|
176
|
+
*,
|
|
177
|
+
serialization_format: SerializationFormat = "cdr",
|
|
178
|
+
) -> BagMessage:
|
|
179
|
+
if topic_name not in self.topic_map:
|
|
180
|
+
if topic_type is None:
|
|
181
|
+
raise ValueError(f"missing topic type to create topic '{topic_name}' dynamically")
|
|
182
|
+
|
|
183
|
+
self.get_or_create_topic(topic_name, topic_type, serialization_format=serialization_format)
|
|
184
|
+
|
|
185
|
+
db_topic = self.topic_map[topic_name]
|
|
186
|
+
db_message = BagMessage(topic_id=db_topic.id, timestamp=timestamp, data=data)
|
|
187
|
+
|
|
188
|
+
self.session.add(db_message)
|
|
189
|
+
self.session.flush()
|
|
190
|
+
|
|
191
|
+
return db_message
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def bag_reader(db_dir: str, db_file: str = default_bag_db_file) -> BagReader:
|
|
195
|
+
"""
|
|
196
|
+
Creates a BagReader instance to read messages from a ROS2 bag file.
|
|
197
|
+
|
|
198
|
+
:param db_dir: directory containing the SQLite DB file.
|
|
199
|
+
:param db_file: name of the SQLite DB file.
|
|
200
|
+
:return: BagReader instance.
|
|
201
|
+
"""
|
|
202
|
+
return BagReader(db_dir, db_file)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def bag_writer(db_dir: str, db_file: str = default_bag_db_file) -> BagWriter:
|
|
206
|
+
"""
|
|
207
|
+
Creates a BagWriter instance to write messages to a ROS2 bag file.
|
|
208
|
+
|
|
209
|
+
:param db_dir: directory to store the SQLite DB file.
|
|
210
|
+
:param db_file: name of the SQLite DB file.
|
|
211
|
+
:return: BagWriter instance.
|
|
212
|
+
"""
|
|
213
|
+
if not os.path.exists(db_dir):
|
|
214
|
+
os.makedirs(db_dir, exist_ok=True)
|
|
215
|
+
return BagWriter(db_dir, db_file)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from iker.common.utils import logger
|
|
2
|
+
from iker.common.utils.config import Config
|
|
3
|
+
from iker.common.utils.funcutils import memorized
|
|
4
|
+
from iker.common.utils.shutils import expanded_path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@memorized()
|
|
8
|
+
def config(config_path: str = "") -> Config:
|
|
9
|
+
default_items: list[tuple[str, str, str]] = [
|
|
10
|
+
("plexus", "logging.level", "INFO"),
|
|
11
|
+
("plexus", "logging.format", "%(asctime)s [%(levelname)s] %(name)s: %(message)s"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
instance = Config(config_path or expanded_path("~/.plexus.cfg"))
|
|
15
|
+
instance.restore()
|
|
16
|
+
instance.update(default_items, overwrite=False)
|
|
17
|
+
|
|
18
|
+
return instance
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def config_print_or_set(config: Config, section: str, key: str, value: str):
|
|
22
|
+
if value is not None:
|
|
23
|
+
if section is None or key is None:
|
|
24
|
+
raise ValueError("cannot specify value without section and key")
|
|
25
|
+
|
|
26
|
+
old_value = config.get(section, key)
|
|
27
|
+
config.set(section, key, value)
|
|
28
|
+
config.persist()
|
|
29
|
+
|
|
30
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
31
|
+
print(f"Section <{section}>")
|
|
32
|
+
print(f" {key} = {old_value} -> {value}")
|
|
33
|
+
|
|
34
|
+
else:
|
|
35
|
+
if section is None and key is None:
|
|
36
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
37
|
+
for section in config.config_parser.sections():
|
|
38
|
+
print(f"Section <{section}>")
|
|
39
|
+
for key, value in config.config_parser.items(section):
|
|
40
|
+
print(f" {key} = {value}")
|
|
41
|
+
|
|
42
|
+
elif section is not None and key is None:
|
|
43
|
+
if not config.has_section(section):
|
|
44
|
+
logger.warning("Configuration section <%s> not found", section)
|
|
45
|
+
return
|
|
46
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
47
|
+
print(f"Section <{section}>")
|
|
48
|
+
for key, value in config.config_parser.items(section):
|
|
49
|
+
print(f" {key} = {value}")
|
|
50
|
+
|
|
51
|
+
elif section is not None and key is not None:
|
|
52
|
+
value = config.get(section, key)
|
|
53
|
+
if value is None:
|
|
54
|
+
logger.warning("Configuration section <%s> key <%s> not found", section, key)
|
|
55
|
+
return
|
|
56
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
57
|
+
print(f"Section <{section}>")
|
|
58
|
+
print(f" {key} = {value}")
|
|
59
|
+
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError("cannot specify key without section")
|