plexus-python-pulse-common 1.1.75__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/pulse/common/__init__.py +6 -0
- plexus/pulse/common/carto/OSMFile.py +259 -0
- plexus/pulse/common/carto/OSMNode.py +25 -0
- plexus/pulse/common/carto/OSMTags.py +101 -0
- plexus/pulse/common/carto/OSMWay.py +24 -0
- plexus/pulse/common/carto/__init__.py +11 -0
- plexus/pulse/common/resources/__init__.py +0 -0
- plexus/pulse/common/resources/tags/__init__.py +35 -0
- plexus/pulse/common/resources/tags/unittest-1.0.0.tagset.yaml +98 -0
- plexus/pulse/common/resources/tags/universal-1.0.0.tagset.yaml +1390 -0
- plexus/pulse/common/utils/__init__.py +0 -0
- plexus/pulse/common/utils/apiutils.py +31 -0
- plexus/pulse/common/utils/bagutils.py +331 -0
- plexus/pulse/common/utils/config.py +63 -0
- plexus/pulse/common/utils/datautils.py +233 -0
- plexus/pulse/common/utils/dockerutils.py +181 -0
- plexus/pulse/common/utils/gisutils.py +406 -0
- plexus/pulse/common/utils/jsonutils.py +96 -0
- plexus/pulse/common/utils/ormutils.py +1638 -0
- plexus/pulse/common/utils/pathutils.py +234 -0
- plexus/pulse/common/utils/s3utils.py +939 -0
- plexus/pulse/common/utils/sqlutils.py +9 -0
- plexus/pulse/common/utils/strutils.py +401 -0
- plexus/pulse/common/utils/tagutils.py +1476 -0
- plexus/pulse/common/utils/testutils.py +172 -0
- plexus_python_pulse_common-1.1.75.dist-info/METADATA +42 -0
- plexus_python_pulse_common-1.1.75.dist-info/RECORD +29 -0
- plexus_python_pulse_common-1.1.75.dist-info/WHEEL +5 -0
- plexus_python_pulse_common-1.1.75.dist-info/top_level.txt +1 -0
|
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,331 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import os
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
from contextlib import AbstractContextManager
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
import sqlalchemy as sa
|
|
8
|
+
import sqlalchemy.orm as sa_orm
|
|
9
|
+
from plexus.common.utils.pathutils import make_path
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"SerializationFormat",
|
|
13
|
+
"BagSchema",
|
|
14
|
+
"BagMetadata",
|
|
15
|
+
"BagMessageDefinition",
|
|
16
|
+
"BagTopic",
|
|
17
|
+
"BagMessage",
|
|
18
|
+
"BagReader",
|
|
19
|
+
"BagWriter",
|
|
20
|
+
"bag_reader",
|
|
21
|
+
"bag_writer",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
SerializationFormat = Literal["cdr", "cdr2", "json", "yaml"]
|
|
25
|
+
|
|
26
|
+
default_bag_db_file = "bag.db"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BagBase(sa_orm.DeclarativeBase):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BagSchema(BagBase):
|
|
34
|
+
__tablename__ = "schema"
|
|
35
|
+
|
|
36
|
+
schema_version: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, primary_key=True)
|
|
37
|
+
ros_distro: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BagMetadata(BagBase):
|
|
41
|
+
__tablename__ = "metadata"
|
|
42
|
+
|
|
43
|
+
id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
44
|
+
metadata_version: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, nullable=False, unique=True)
|
|
45
|
+
metadata_text: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BagMessageDefinition(BagBase):
|
|
49
|
+
__tablename__ = "message_definitions"
|
|
50
|
+
|
|
51
|
+
id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
52
|
+
message_type: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False, unique=True)
|
|
53
|
+
encoding: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
54
|
+
encoded_message_definition: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
55
|
+
type_description_hash: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
56
|
+
|
|
57
|
+
topics: sa_orm.Mapped[list["BagTopic"]] = sa_orm.relationship("BagTopic", back_populates="message_definition")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BagTopic(BagBase):
|
|
61
|
+
__tablename__ = "topics"
|
|
62
|
+
|
|
63
|
+
id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
64
|
+
name: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False, unique=True)
|
|
65
|
+
message_definition_id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer,
|
|
66
|
+
sa.ForeignKey("message_definitions.id"),
|
|
67
|
+
nullable=False)
|
|
68
|
+
serialization_format: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
69
|
+
type_description_hash: sa_orm.Mapped[str] = sa_orm.mapped_column(sa.String, nullable=False)
|
|
70
|
+
|
|
71
|
+
messages: sa_orm.Mapped[list["BagMessage"]] = sa_orm.relationship("BagMessage", back_populates="topic")
|
|
72
|
+
message_definition: sa_orm.Mapped["BagMessageDefinition"] = sa_orm.relationship("BagMessageDefinition",
|
|
73
|
+
back_populates="topics")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BagMessage(BagBase):
|
|
77
|
+
__tablename__ = "messages"
|
|
78
|
+
|
|
79
|
+
id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, primary_key=True, autoincrement=True)
|
|
80
|
+
topic_id: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, sa.ForeignKey("topics.id"), nullable=False)
|
|
81
|
+
timestamp: sa_orm.Mapped[int] = sa_orm.mapped_column(sa.Integer, nullable=False)
|
|
82
|
+
data: sa_orm.Mapped[bytes] = sa_orm.mapped_column(sa.LargeBinary, nullable=False)
|
|
83
|
+
|
|
84
|
+
topic: sa_orm.Mapped["BagTopic"] = sa_orm.relationship("BagTopic", back_populates="messages")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BagIOBase(object):
|
|
88
|
+
def __init__(self, bag_file_path: str | os.PathLike[str]):
|
|
89
|
+
self.bag_file_path = make_path(bag_file_path)
|
|
90
|
+
|
|
91
|
+
self.engine: sa.Engine | None = None
|
|
92
|
+
self.session: sa_orm.Session | None = None
|
|
93
|
+
|
|
94
|
+
self.message_definitions_lookup: dict[str, int] = {}
|
|
95
|
+
self.topics_lookup: dict[str, int] = {}
|
|
96
|
+
|
|
97
|
+
@contextlib.contextmanager
|
|
98
|
+
def setup(self):
|
|
99
|
+
self.engine = sa.create_engine(f"sqlite:///{str(self.bag_file_path.absolute())}")
|
|
100
|
+
self.session = sa_orm.Session(self.engine)
|
|
101
|
+
try:
|
|
102
|
+
yield
|
|
103
|
+
finally:
|
|
104
|
+
self.message_definitions_lookup = {db_message_definition.message_type: db_message_definition.id
|
|
105
|
+
for db_message_definition in self.message_definitions()}
|
|
106
|
+
self.topics_lookup = {db_topic.name: db_topic.id for db_topic in self.topics()}
|
|
107
|
+
|
|
108
|
+
@contextlib.contextmanager
|
|
109
|
+
def teardown(self):
|
|
110
|
+
try:
|
|
111
|
+
yield
|
|
112
|
+
finally:
|
|
113
|
+
self.session.close()
|
|
114
|
+
self.engine.dispose()
|
|
115
|
+
|
|
116
|
+
def get_metadata(self, metadata_version: int) -> BagMetadata | None:
|
|
117
|
+
return (
|
|
118
|
+
self
|
|
119
|
+
.session
|
|
120
|
+
.query(BagMetadata)
|
|
121
|
+
.filter(BagMetadata.metadata_version == metadata_version)
|
|
122
|
+
.one_or_none()
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def get_message_definition(self, message_type: str) -> BagMessageDefinition | None:
|
|
126
|
+
return (
|
|
127
|
+
self
|
|
128
|
+
.session
|
|
129
|
+
.query(BagMessageDefinition)
|
|
130
|
+
.filter(BagMessageDefinition.message_type == message_type)
|
|
131
|
+
.one_or_none()
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def get_topic(self, name: str) -> BagTopic | None:
|
|
135
|
+
return self.session.query(BagTopic).filter(BagTopic.name == name).one_or_none()
|
|
136
|
+
|
|
137
|
+
def metadata(self) -> list[BagMetadata]:
|
|
138
|
+
return self.session.query(BagMetadata).all()
|
|
139
|
+
|
|
140
|
+
def message_definitions(self) -> list[BagMessageDefinition]:
|
|
141
|
+
return self.session.query(BagMessageDefinition).all()
|
|
142
|
+
|
|
143
|
+
def topics(self) -> list[BagTopic]:
|
|
144
|
+
return self.session.query(BagTopic).all()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class BagReader(AbstractContextManager, BagIOBase):
|
|
148
|
+
@staticmethod
|
|
149
|
+
def open_ros2(db_dir: str | os.PathLike[str], db_filename: str = default_bag_db_file, **kwargs) -> "BagReader":
|
|
150
|
+
"""
|
|
151
|
+
Creates a ``BagReader`` instance to read messages from a ROS2 bag file in the specified directory.
|
|
152
|
+
|
|
153
|
+
:param db_dir: path to the directory where the SQLite DB file is located.
|
|
154
|
+
:param db_filename: name of the SQLite DB file to read. Default is "bag.db".
|
|
155
|
+
:param kwargs: additional keyword arguments to pass to the ``BagReader`` constructor.
|
|
156
|
+
:return: a ``BagReader`` instance.
|
|
157
|
+
"""
|
|
158
|
+
return BagReader(make_path(db_dir) / db_filename)
|
|
159
|
+
|
|
160
|
+
def __init__(self, bag_file_path: str | os.PathLike[str]):
|
|
161
|
+
"""
|
|
162
|
+
Creates a ``BagReader`` instance to read messages from a ROS2 bag file.
|
|
163
|
+
|
|
164
|
+
:param bag_file_path: path to the SQLite DB file to read.
|
|
165
|
+
"""
|
|
166
|
+
super().__init__(bag_file_path)
|
|
167
|
+
|
|
168
|
+
if not self.bag_file_path.exists():
|
|
169
|
+
raise FileNotFoundError(f"could not find SQLite DB at '{str(self.bag_file_path)}'")
|
|
170
|
+
|
|
171
|
+
def __enter__(self):
|
|
172
|
+
with self.setup():
|
|
173
|
+
pass
|
|
174
|
+
return self
|
|
175
|
+
|
|
176
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
177
|
+
with self.teardown():
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
def iter_messages(
|
|
181
|
+
self,
|
|
182
|
+
topic_names: list[str] | None = None,
|
|
183
|
+
begin_timestamp: int | None = None,
|
|
184
|
+
end_timestamp: int | None = None,
|
|
185
|
+
*,
|
|
186
|
+
batch_size: int = 1000,
|
|
187
|
+
) -> Generator[BagMessage, None, None]:
|
|
188
|
+
query = self.session.query(BagMessage)
|
|
189
|
+
if topic_names is not None:
|
|
190
|
+
topic_ids = [topic_id for topic_name, topic_id in self.topics_lookup.items() if topic_name in topic_names]
|
|
191
|
+
query = query.where(BagMessage.topic_id.in_(topic_ids))
|
|
192
|
+
if begin_timestamp is not None:
|
|
193
|
+
query = query.where(BagMessage.timestamp >= begin_timestamp)
|
|
194
|
+
if end_timestamp is not None:
|
|
195
|
+
query = query.where(BagMessage.timestamp <= end_timestamp)
|
|
196
|
+
|
|
197
|
+
for db_message in query.order_by(BagMessage.timestamp.asc()).yield_per(batch_size):
|
|
198
|
+
yield db_message
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class BagWriter(AbstractContextManager, BagIOBase):
|
|
202
|
+
@staticmethod
|
|
203
|
+
def open_ros2(db_dir: str | os.PathLike[str], db_filename: str = default_bag_db_file, **kwargs) -> "BagWriter":
|
|
204
|
+
"""
|
|
205
|
+
Creates a ``BagWriter`` instance to write messages to a ROS2 bag file in the specified directory.
|
|
206
|
+
|
|
207
|
+
:param db_dir: path to the directory where the SQLite DB file will be created.
|
|
208
|
+
:param db_filename: name of the SQLite DB file to create. Default is "bag.db".
|
|
209
|
+
:param kwargs: additional keyword arguments to pass to the ``BagWriter`` constructor.
|
|
210
|
+
:return: a ``BagWriter`` instance.
|
|
211
|
+
"""
|
|
212
|
+
return BagWriter(make_path(db_dir) / db_filename, **kwargs)
|
|
213
|
+
|
|
214
|
+
def __init__(self, bag_file_path: str | os.PathLike[str], *, overwrite: bool = True, exist_ok: bool = False):
|
|
215
|
+
"""
|
|
216
|
+
Creates a ``BagWriter`` instance to write messages to a ROS2 bag file.
|
|
217
|
+
|
|
218
|
+
:param bag_file_path: path to the SQLite DB file to create.
|
|
219
|
+
:param overwrite: whether to overwrite the SQLite DB file if it already exists. If ``False`` and the file
|
|
220
|
+
already exists, a ``FileExistsError`` is raised. If ``True``, the existing file is deleted
|
|
221
|
+
and a new one is created.
|
|
222
|
+
:param exist_ok: whether to ignore if the SQLite DB file already exists. If ``False`` and the file already
|
|
223
|
+
exists, a ``FileExistsError`` is raised. If ``True``, the existing file is used and no new
|
|
224
|
+
file is created.
|
|
225
|
+
"""
|
|
226
|
+
super().__init__(bag_file_path)
|
|
227
|
+
|
|
228
|
+
if not self.bag_file_path.parent.exists():
|
|
229
|
+
self.bag_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
230
|
+
if self.bag_file_path.exists():
|
|
231
|
+
if not overwrite and not exist_ok:
|
|
232
|
+
raise FileExistsError(f"SQLite DB file already exists at '{str(self.bag_file_path)}'")
|
|
233
|
+
if overwrite:
|
|
234
|
+
self.bag_file_path.unlink()
|
|
235
|
+
|
|
236
|
+
def __enter__(self):
|
|
237
|
+
with self.setup():
|
|
238
|
+
BagBase.metadata.create_all(self.engine)
|
|
239
|
+
return self
|
|
240
|
+
|
|
241
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
242
|
+
with self.teardown():
|
|
243
|
+
if exc_type is None:
|
|
244
|
+
self.session.commit()
|
|
245
|
+
else:
|
|
246
|
+
self.session.rollback()
|
|
247
|
+
|
|
248
|
+
def add_metadata(self, metadata_version: int, metadata_text: str, *, return_existing: bool = False) -> BagMetadata:
|
|
249
|
+
if (db_metadata := self.get_metadata(metadata_version)) is not None:
|
|
250
|
+
if return_existing:
|
|
251
|
+
return db_metadata
|
|
252
|
+
raise ValueError(f"metadata with metadata_version '{metadata_version}' already exists in the bag file")
|
|
253
|
+
|
|
254
|
+
db_metadata = BagMetadata(metadata_version=metadata_version, metadata_text=metadata_text)
|
|
255
|
+
self.session.add(db_metadata)
|
|
256
|
+
self.session.flush()
|
|
257
|
+
|
|
258
|
+
return db_metadata
|
|
259
|
+
|
|
260
|
+
def add_message_definition(
|
|
261
|
+
self,
|
|
262
|
+
message_type: str,
|
|
263
|
+
encoding: str = "",
|
|
264
|
+
encoded_message_definition: str = "",
|
|
265
|
+
type_description_hash: str = "",
|
|
266
|
+
*,
|
|
267
|
+
return_existing: bool = False,
|
|
268
|
+
) -> BagMessageDefinition:
|
|
269
|
+
if (db_message_definition := self.get_message_definition(message_type)) is not None:
|
|
270
|
+
if return_existing:
|
|
271
|
+
return db_message_definition
|
|
272
|
+
raise ValueError(f"message definition with message_type '{message_type}' already exists in the bag file")
|
|
273
|
+
|
|
274
|
+
db_message_definition = BagMessageDefinition(
|
|
275
|
+
message_type=message_type,
|
|
276
|
+
encoding=encoding,
|
|
277
|
+
encoded_message_definition=encoded_message_definition,
|
|
278
|
+
type_description_hash=type_description_hash,
|
|
279
|
+
)
|
|
280
|
+
self.session.add(db_message_definition)
|
|
281
|
+
self.session.flush()
|
|
282
|
+
|
|
283
|
+
self.message_definitions_lookup[db_message_definition.message_type] = db_message_definition.id
|
|
284
|
+
|
|
285
|
+
return db_message_definition
|
|
286
|
+
|
|
287
|
+
def add_topic(
|
|
288
|
+
self,
|
|
289
|
+
name: str,
|
|
290
|
+
message_type: str,
|
|
291
|
+
serialization_format: SerializationFormat = "cdr",
|
|
292
|
+
type_description_hash: str = "",
|
|
293
|
+
*,
|
|
294
|
+
return_existing: bool = False,
|
|
295
|
+
) -> BagTopic:
|
|
296
|
+
message_definition_id = self.message_definitions_lookup.get(message_type)
|
|
297
|
+
if message_definition_id is None:
|
|
298
|
+
raise ValueError(f"message definition with message_type '{message_type}' does not exist in the bag file")
|
|
299
|
+
|
|
300
|
+
if (db_topic := self.get_topic(name)) is not None:
|
|
301
|
+
if return_existing:
|
|
302
|
+
return db_topic
|
|
303
|
+
raise ValueError(f"topic with name '{name}' already exists in the bag file")
|
|
304
|
+
|
|
305
|
+
db_topic = BagTopic(
|
|
306
|
+
name=name,
|
|
307
|
+
message_definition_id=message_definition_id,
|
|
308
|
+
serialization_format=serialization_format,
|
|
309
|
+
type_description_hash=type_description_hash,
|
|
310
|
+
)
|
|
311
|
+
self.session.add(db_topic)
|
|
312
|
+
self.session.flush()
|
|
313
|
+
|
|
314
|
+
self.topics_lookup[db_topic.name] = db_topic.id
|
|
315
|
+
|
|
316
|
+
return db_topic
|
|
317
|
+
|
|
318
|
+
def write_message(self, name: str, timestamp: int, data: bytes) -> BagMessage:
|
|
319
|
+
topic_id = self.topics_lookup.get(name)
|
|
320
|
+
if topic_id is None:
|
|
321
|
+
raise ValueError(f"topic with name '{name}' does not exist in the bag file")
|
|
322
|
+
|
|
323
|
+
db_message = BagMessage(topic_id=topic_id, timestamp=timestamp, data=data)
|
|
324
|
+
self.session.add(db_message)
|
|
325
|
+
self.session.flush()
|
|
326
|
+
|
|
327
|
+
return db_message
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
bag_reader = BagReader
|
|
331
|
+
bag_writer = BagWriter
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from plexus.common.utils import logger
|
|
4
|
+
from plexus.common.utils.config import Config
|
|
5
|
+
from plexus.common.utils.funcutils import memorized
|
|
6
|
+
from plexus.common.utils.pathutils import make_path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@memorized()
|
|
10
|
+
def config(config_path: str | os.PathLike[str] = "") -> Config:
|
|
11
|
+
default_items: list[tuple[str, str, str]] = [
|
|
12
|
+
("plexus", "logging.level", "INFO"),
|
|
13
|
+
("plexus", "logging.format", "%(asctime)s [%(levelname)s] %(name)s: %(message)s"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
instance = Config(config_path or make_path("~/.plexus.cfg", expand=True, normalize=True, absolute=True))
|
|
17
|
+
instance.restore()
|
|
18
|
+
instance.update(default_items, overwrite=False)
|
|
19
|
+
|
|
20
|
+
return instance
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def config_print_or_set(config: Config, section: str, key: str, value: str):
|
|
24
|
+
if value is not None:
|
|
25
|
+
if section is None or key is None:
|
|
26
|
+
raise ValueError("cannot specify value without section and key")
|
|
27
|
+
|
|
28
|
+
old_value = config.get(section, key)
|
|
29
|
+
config.set(section, key, value)
|
|
30
|
+
config.persist()
|
|
31
|
+
|
|
32
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
33
|
+
print(f"Section <{section}>")
|
|
34
|
+
print(f" {key} = {old_value} -> {value}")
|
|
35
|
+
|
|
36
|
+
else:
|
|
37
|
+
if section is None and key is None:
|
|
38
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
39
|
+
for section in config.config_parser.sections():
|
|
40
|
+
print(f"Section <{section}>")
|
|
41
|
+
for key, value in config.config_parser.items(section):
|
|
42
|
+
print(f" {key} = {value}")
|
|
43
|
+
|
|
44
|
+
elif section is not None and key is None:
|
|
45
|
+
if not config.has_section(section):
|
|
46
|
+
logger.warning("Configuration section '%s' not found", section)
|
|
47
|
+
return
|
|
48
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
49
|
+
print(f"Section <{section}>")
|
|
50
|
+
for key, value in config.config_parser.items(section):
|
|
51
|
+
print(f" {key} = {value}")
|
|
52
|
+
|
|
53
|
+
elif section is not None and key is not None:
|
|
54
|
+
value = config.get(section, key)
|
|
55
|
+
if value is None:
|
|
56
|
+
logger.warning("Configuration section '%s' key '%s' not found", section, key)
|
|
57
|
+
return
|
|
58
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
59
|
+
print(f"Section <{section}>")
|
|
60
|
+
print(f" {key} = {value}")
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError("cannot specify key without section")
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from collections.abc import Callable, Generator
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pyparsing as pp
|
|
6
|
+
import ujson as json
|
|
7
|
+
from plexus.common.utils.funcutils import singleton
|
|
8
|
+
from plexus.common.utils.jsonutils import JsonType
|
|
9
|
+
from plexus.common.utils.randutils import randomizer
|
|
10
|
+
|
|
11
|
+
from plexus.pulse.common.utils.strutils import BagName, UserName, VehicleName
|
|
12
|
+
from plexus.pulse.common.utils.strutils import colon_tag_parser, slash_tag_parser
|
|
13
|
+
from plexus.pulse.common.utils.strutils import dot_case_parser, kebab_case_parser, snake_case_parser
|
|
14
|
+
from plexus.pulse.common.utils.strutils import hex_string_parser
|
|
15
|
+
from plexus.pulse.common.utils.strutils import parse_bag_name, parse_user_name, parse_vehicle_name
|
|
16
|
+
from plexus.pulse.common.utils.strutils import semver_parser, uuid_parser
|
|
17
|
+
from plexus.pulse.common.utils.strutils import strict_abspath_parser, strict_relpath_parser
|
|
18
|
+
from plexus.pulse.common.utils.strutils import strict_fragmented_abspath_parser, strict_fragmented_relpath_parser
|
|
19
|
+
from plexus.pulse.common.utils.strutils import topic_parser, vin_code_chars, vin_code_parser
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"compute_vin_code_check_digit",
|
|
23
|
+
"validate_hex_string",
|
|
24
|
+
"validate_snake_case",
|
|
25
|
+
"validate_kebab_case",
|
|
26
|
+
"validate_dot_case",
|
|
27
|
+
"validate_uuid",
|
|
28
|
+
"validate_strict_relpath",
|
|
29
|
+
"validate_strict_abspath",
|
|
30
|
+
"validate_strict_fragmented_relpath",
|
|
31
|
+
"validate_strict_fragmented_abspath",
|
|
32
|
+
"validate_semver",
|
|
33
|
+
"validate_colon_tag",
|
|
34
|
+
"validate_slash_tag",
|
|
35
|
+
"validate_topic",
|
|
36
|
+
"validate_vin_code",
|
|
37
|
+
"validate_user_name",
|
|
38
|
+
"validate_vehicle_name",
|
|
39
|
+
"validate_bag_name",
|
|
40
|
+
"validate_dt_timezone",
|
|
41
|
+
"validate_json_type_dump_size",
|
|
42
|
+
"random_vin_code",
|
|
43
|
+
"known_topics",
|
|
44
|
+
"known_user_names",
|
|
45
|
+
"known_vehicle_names",
|
|
46
|
+
"random_bag_names_sequence",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def make_compute_vin_code_check_digit() -> Callable[[str], str]:
|
|
51
|
+
trans_nums = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 7, 9, 2, 3, 4, 5, 6, 7, 8, 9]
|
|
52
|
+
weights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]
|
|
53
|
+
|
|
54
|
+
trans_dict = {vin_code_char: trans_num for vin_code_char, trans_num in zip(vin_code_chars, trans_nums)}
|
|
55
|
+
|
|
56
|
+
def func(vin_code: str) -> str:
|
|
57
|
+
remainder = sum(trans_dict[vin_code_char] * weight for vin_code_char, weight in zip(vin_code, weights)) % 11
|
|
58
|
+
return "X" if remainder == 10 else str(remainder)
|
|
59
|
+
|
|
60
|
+
return func
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
compute_vin_code_check_digit = make_compute_vin_code_check_digit()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def make_validate_string(element: pp.ParserElement) -> Callable[[str], None]:
|
|
67
|
+
def func(s: str) -> None:
|
|
68
|
+
try:
|
|
69
|
+
if not element.parse_string(s, parse_all=True):
|
|
70
|
+
raise ValueError(f"failed to parse '{s}'")
|
|
71
|
+
except Exception as e:
|
|
72
|
+
raise ValueError(f"encountered error while parsing '{s}'") from e
|
|
73
|
+
|
|
74
|
+
return func
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
validate_hex_string = make_validate_string(hex_string_parser)
|
|
78
|
+
|
|
79
|
+
validate_snake_case = make_validate_string(snake_case_parser)
|
|
80
|
+
validate_kebab_case = make_validate_string(kebab_case_parser)
|
|
81
|
+
validate_dot_case = make_validate_string(dot_case_parser)
|
|
82
|
+
|
|
83
|
+
validate_uuid = make_validate_string(uuid_parser)
|
|
84
|
+
|
|
85
|
+
validate_strict_relpath = make_validate_string(strict_relpath_parser)
|
|
86
|
+
validate_strict_abspath = make_validate_string(strict_abspath_parser)
|
|
87
|
+
validate_strict_fragmented_relpath = make_validate_string(strict_fragmented_relpath_parser)
|
|
88
|
+
validate_strict_fragmented_abspath = make_validate_string(strict_fragmented_abspath_parser)
|
|
89
|
+
|
|
90
|
+
validate_semver = make_validate_string(semver_parser)
|
|
91
|
+
|
|
92
|
+
validate_colon_tag = make_validate_string(colon_tag_parser)
|
|
93
|
+
validate_slash_tag = make_validate_string(slash_tag_parser)
|
|
94
|
+
|
|
95
|
+
validate_topic = make_validate_string(topic_parser)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def validate_vin_code(vin_code: str):
|
|
99
|
+
make_validate_string(vin_code_parser)(vin_code)
|
|
100
|
+
check_digit = compute_vin_code_check_digit(vin_code)
|
|
101
|
+
if check_digit != vin_code[8]:
|
|
102
|
+
raise ValueError(f"get wrong VIN code check digit from '{vin_code}', expected '{check_digit}'")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def make_validate_parse_string(parse: Callable[[str], Any]) -> Callable[[str], None]:
|
|
106
|
+
def func(s: str) -> None:
|
|
107
|
+
try:
|
|
108
|
+
if not parse(s):
|
|
109
|
+
raise ValueError(f"failed to parse '{s}'")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
raise ValueError(f"encountered error while parsing '{s}'") from e
|
|
112
|
+
|
|
113
|
+
return func
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
validate_user_name = make_validate_parse_string(parse_user_name)
|
|
117
|
+
validate_vehicle_name = make_validate_parse_string(parse_vehicle_name)
|
|
118
|
+
validate_bag_name = make_validate_parse_string(parse_bag_name)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def validate_dt_timezone(dt: datetime.datetime, *, allow_naive: bool = False):
|
|
122
|
+
if allow_naive and dt.tzinfo is None:
|
|
123
|
+
return
|
|
124
|
+
if dt.tzinfo != datetime.timezone.utc:
|
|
125
|
+
raise ValueError(f"dt '{dt}' is not in UTC")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def validate_json_type_dump_size(json_type: JsonType, dump_size_limit: int = 10000):
|
|
129
|
+
dump_string = json.dumps(json_type, ensure_ascii=False)
|
|
130
|
+
if len(dump_string) > dump_size_limit:
|
|
131
|
+
raise ValueError(f"dump size exceeds the maximum length '{dump_size_limit}'")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def random_vin_code() -> str:
|
|
135
|
+
vin_code = randomizer().random_string(vin_code_chars, 17)
|
|
136
|
+
check_digit = compute_vin_code_check_digit(vin_code)
|
|
137
|
+
return vin_code[:8] + check_digit + vin_code[9:]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@singleton
|
|
141
|
+
def known_topics() -> list[str]:
|
|
142
|
+
return [
|
|
143
|
+
"/sensor/camera/front_center",
|
|
144
|
+
"/sensor/camera/front_left",
|
|
145
|
+
"/sensor/camera/front_right",
|
|
146
|
+
"/sensor/camera/side_left",
|
|
147
|
+
"/sensor/camera/side_right",
|
|
148
|
+
"/sensor/camera/rear_left",
|
|
149
|
+
"/sensor/camera/rear_right",
|
|
150
|
+
"/sensor/lidar/front_center",
|
|
151
|
+
"/sensor/lidar/front_left_corner",
|
|
152
|
+
"/sensor/lidar/front_right_corner",
|
|
153
|
+
"/sensor/lidar/side_left",
|
|
154
|
+
"/sensor/lidar/side_right",
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@singleton
|
|
159
|
+
def known_user_names() -> list[UserName]:
|
|
160
|
+
return [
|
|
161
|
+
UserName("adam", "anderson"),
|
|
162
|
+
UserName("ben", "bennett"),
|
|
163
|
+
UserName("charlie", "clark"),
|
|
164
|
+
UserName("david", "dixon"),
|
|
165
|
+
UserName("evan", "edwards"),
|
|
166
|
+
UserName("frank", "fisher"),
|
|
167
|
+
UserName("george", "graham"),
|
|
168
|
+
UserName("henry", "harrison"),
|
|
169
|
+
UserName("isaac", "irving"),
|
|
170
|
+
UserName("jack", "jacobs"),
|
|
171
|
+
UserName("kevin", "kennedy"),
|
|
172
|
+
UserName("luke", "lawson"),
|
|
173
|
+
UserName("michael", "mitchell"),
|
|
174
|
+
UserName("nathan", "newton"),
|
|
175
|
+
UserName("oscar", "owens"),
|
|
176
|
+
UserName("paul", "peterson"),
|
|
177
|
+
UserName("quincy", "quinn"),
|
|
178
|
+
UserName("ryan", "robinson"),
|
|
179
|
+
UserName("sam", "stevens"),
|
|
180
|
+
UserName("tom", "thomas"),
|
|
181
|
+
UserName("umar", "underwood"),
|
|
182
|
+
UserName("victor", "vaughan"),
|
|
183
|
+
UserName("william", "walker"),
|
|
184
|
+
UserName("xander", "xavier"),
|
|
185
|
+
UserName("yale", "young"),
|
|
186
|
+
UserName("zane", "zimmerman"),
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@singleton
|
|
191
|
+
def known_vehicle_names() -> list[VehicleName]:
|
|
192
|
+
return [
|
|
193
|
+
VehicleName("cascadia", "antelope", "00000", "3AKJGLD5XLS000000"),
|
|
194
|
+
VehicleName("cascadia", "bear", "00001", "3AKJGLD51LS000001"),
|
|
195
|
+
VehicleName("cascadia", "cheetah", "00002", "3AKJGLD53LS000002"),
|
|
196
|
+
VehicleName("cascadia", "dolphin", "00003", "3AKJGLD55LS000003"),
|
|
197
|
+
VehicleName("cascadia", "eagle", "00004", "3AKJGLD57LS000004"),
|
|
198
|
+
VehicleName("cascadia", "falcon", "00005", "3AKJGLD59LS000005"),
|
|
199
|
+
VehicleName("cascadia", "gorilla", "00006", "3AKJGLD50LS000006"),
|
|
200
|
+
VehicleName("cascadia", "hawk", "00007", "3AKJGLD52LS000007"),
|
|
201
|
+
VehicleName("cascadia", "iguana", "00008", "3AKJGLD54LS000008"),
|
|
202
|
+
VehicleName("cascadia", "jaguar", "00009", "3AKJGLD56LS000009"),
|
|
203
|
+
VehicleName("cascadia", "koala", "00010", "3AKJGLD52LS000010"),
|
|
204
|
+
VehicleName("cascadia", "leopard", "00011", "3AKJGLD54LS000011"),
|
|
205
|
+
VehicleName("cascadia", "mongoose", "00012", "3AKJGLD56LS000012"),
|
|
206
|
+
VehicleName("cascadia", "narwhal", "00013", "3AKJGLD58LS000013"),
|
|
207
|
+
VehicleName("cascadia", "otter", "00014", "3AKJGLD5XLS000014"),
|
|
208
|
+
VehicleName("cascadia", "panther", "00015", "3AKJGLD51LS000015"),
|
|
209
|
+
VehicleName("cascadia", "quail", "00016", "3AKJGLD53LS000016"),
|
|
210
|
+
VehicleName("cascadia", "rhino", "00017", "3AKJGLD55LS000017"),
|
|
211
|
+
VehicleName("cascadia", "snake", "00018", "3AKJGLD57LS000018"),
|
|
212
|
+
VehicleName("cascadia", "tiger", "00019", "3AKJGLD59LS000019"),
|
|
213
|
+
VehicleName("cascadia", "urial", "00020", "3AKJGLD55LS000020"),
|
|
214
|
+
VehicleName("cascadia", "vulture", "00021", "3AKJGLD57LS000021"),
|
|
215
|
+
VehicleName("cascadia", "wolf", "00022", "3AKJGLD59LS000022"),
|
|
216
|
+
VehicleName("cascadia", "xerus", "00023", "3AKJGLD50LS000023"),
|
|
217
|
+
VehicleName("cascadia", "yak", "00024", "3AKJGLD52LS000024"),
|
|
218
|
+
VehicleName("cascadia", "zebra", "00025", "3AKJGLD54LS000025"),
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def random_bag_names_sequence(
|
|
223
|
+
min_record_dt: datetime.datetime,
|
|
224
|
+
max_record_dt: datetime.datetime,
|
|
225
|
+
min_sequence_length: int,
|
|
226
|
+
max_sequence_length: int,
|
|
227
|
+
) -> Generator[BagName, None, None]:
|
|
228
|
+
vehicle_name = randomizer().choose(known_vehicle_names())
|
|
229
|
+
record_dt = randomizer().random_datetime(begin=min_record_dt, end=max_record_dt)
|
|
230
|
+
bags_count = randomizer().next_int(min_sequence_length, max_sequence_length)
|
|
231
|
+
|
|
232
|
+
for record_sn in range(bags_count):
|
|
233
|
+
yield BagName(vehicle_name=vehicle_name, record_dt=record_dt, record_sn=record_sn)
|