plexus-python-common 1.0.7__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.

Potentially problematic release.


This version of plexus-python-common might be problematic. Click here for more details.

@@ -0,0 +1,6 @@
1
+ import importlib.metadata
2
+
3
+ try:
4
+ __version__ = importlib.metadata.version("plexus-python-common")
5
+ except importlib.metadata.PackageNotFoundError:
6
+ __version__ = "unknown"
@@ -0,0 +1,259 @@
1
+ from typing import Any, Self
2
+
3
+ import lxml.etree
4
+
5
+ from plexus.common.carto.OSMNode import OSMNode
6
+ from plexus.common.carto.OSMTags import OSMTags
7
+ from plexus.common.carto.OSMWay import OSMWay
8
+ from plexus.common.proj import Coord, Proj
9
+
10
+
11
+ class OSMFile(object):
12
+ @classmethod
13
+ def load_file(cls, path: str, proj: Proj | None) -> Self:
14
+ """
15
+ Loads OSM from file given the path
16
+
17
+ :param path: path of OSM file
18
+ :param proj: cartographic projection
19
+
20
+ :return: an OSM file instance
21
+ """
22
+ root = lxml.etree.parse(path).getroot()
23
+
24
+ nodes = {}
25
+ ways = {}
26
+ for elem in root:
27
+ if elem.attrib.get("action") == "delete":
28
+ continue
29
+
30
+ if elem.tag == "node":
31
+ # Parse an OSM node
32
+ node_id = int(elem.attrib["id"])
33
+ tags = {}
34
+ for child in elem:
35
+ if child.tag == "tag":
36
+ k = child.attrib["k"]
37
+ v = child.attrib["v"]
38
+ tags[k] = v
39
+
40
+ tags = OSMTags.from_any_tags(tags)
41
+ coord = Coord.from_latlon(
42
+ float(elem.attrib["lat"]),
43
+ float(elem.attrib["lon"]),
44
+ tags.getfloat("z", 0.0),
45
+ proj,
46
+ )
47
+
48
+ nodes[node_id] = OSMNode(node_id, coord, tags)
49
+
50
+ elif elem.tag == "way":
51
+ # Parse an OSM way
52
+ way_id = int(elem.attrib["id"])
53
+
54
+ tags = {}
55
+ node_ids = []
56
+ for child in elem:
57
+ if child.tag == "tag":
58
+ k = child.attrib["k"]
59
+ v = child.attrib["v"]
60
+ tags[k] = v
61
+ elif child.tag == "nd":
62
+ node_id = int(child.attrib.get("ref"))
63
+ node_ids.append(node_id)
64
+
65
+ tags = OSMTags.from_any_tags(tags)
66
+
67
+ ways[way_id] = OSMWay(way_id, node_ids, tags)
68
+
69
+ return OSMFile(nodes, ways)
70
+
71
+ @classmethod
72
+ def from_coords(cls, coords: list[Coord], way_tags: dict[str, Any] = None) -> Self:
73
+ """
74
+ Constructs a single-way OSM file from the given coords
75
+
76
+ :param coords: coords on a single way
77
+ :param way_tags: tags of the corresponding way
78
+
79
+ :return: OSM file contains one way
80
+ """
81
+ osm = OSMFile()
82
+ osm.add_way_from_coords(coords, way_tags or {})
83
+
84
+ return osm
85
+
86
+ @classmethod
87
+ def from_coords_list(cls, coords_list: list[list[Coord]], way_tags_list: list[dict[str, Any]] = None) -> Self:
88
+ """
89
+ Constructs a multi-way OSM file from the given coords
90
+
91
+ :param coords_list: coords of each way
92
+ :param way_tags_list: tags of each corresponding way
93
+
94
+ :return: OSM file contains multiple ways
95
+ """
96
+ osm = OSMFile()
97
+
98
+ if not way_tags_list or len(way_tags_list) != len(coords_list):
99
+ for coords in coords_list:
100
+ osm.add_way_from_coords(coords)
101
+ else:
102
+ for coords, way_tags in zip(coords_list, way_tags_list):
103
+ osm.add_way_from_coords(coords, way_tags or {})
104
+
105
+ return osm
106
+
107
+ def __init__(self, nodes: dict[int, OSMNode] = None, ways: dict[int, OSMWay] = None):
108
+ """
109
+ Represents an editable general purpose OSM file
110
+
111
+ :param nodes: nodes
112
+ :param ways: ways
113
+ """
114
+ self.nodes: dict[int, OSMNode] = {}
115
+ self.ways: dict[int, OSMWay] = {}
116
+
117
+ self.next_node_id = 1
118
+ self.next_way_id = 1
119
+
120
+ if nodes:
121
+ self.nodes = nodes
122
+ self.next_node_id = max(nodes.keys()) + 1
123
+ if ways:
124
+ self.ways = ways
125
+ self.next_way_id = max(ways.keys()) + 1
126
+
127
+ def add_node(self, node: OSMNode) -> OSMNode:
128
+ """
129
+ Adds the given node
130
+
131
+ :param node: the node to be added
132
+
133
+ :return: added node
134
+ """
135
+ self.nodes[node.node_id] = node
136
+ if node.node_id >= self.next_node_id:
137
+ self.next_node_id = node.node_id + 1
138
+ return node
139
+
140
+ def add_node_from_coord(self, coord: Coord, tags: dict[str, Any] = None) -> OSMNode:
141
+ """
142
+ Adds node given the coord and tags
143
+
144
+ :param coord: coord of the node
145
+ :param tags: tags of the node
146
+
147
+ :return: added node
148
+ """
149
+ return self.add_node(OSMNode(self.next_node_id, coord, OSMTags.from_any_tags(tags)))
150
+
151
+ def add_way(self, way: OSMWay) -> OSMWay:
152
+ """
153
+ Adds the given way
154
+
155
+ :param way: the way to be added
156
+
157
+ :return: added way
158
+ """
159
+ self.ways[way.way_id] = way
160
+ if way.way_id >= self.next_way_id:
161
+ self.next_way_id = way.way_id + 1
162
+ return way
163
+
164
+ def add_way_from_coords(self, coords: list[Coord], tags: dict[str, Any] = None) -> OSMWay:
165
+ """
166
+ Adds way given the coords of the nodes and tags
167
+
168
+ :param coords: coords of the nodes on the way
169
+ :param tags: tags of the way
170
+
171
+ :return: added way
172
+ """
173
+ return self.add_way_from_nodes(coords, tags)
174
+
175
+ def add_way_from_node_ids(self, node_ids: list[int], tags: dict[str, Any] = None) -> OSMWay:
176
+ """
177
+ Adds way given the ids of the nodes and tags
178
+
179
+ :param node_ids: ids of the nodes on the way
180
+ :param tags: tags of the way
181
+
182
+ :return: added way
183
+ """
184
+ return self.add_way_from_nodes(node_ids, tags)
185
+
186
+ def add_way_from_nodes(self, elements: list[OSMNode | Coord | int], tags: dict[str, Any] = None) -> OSMWay:
187
+ """
188
+ Adds way given list of the nodes, the coords, or the ids of the nodes and tags
189
+
190
+ :param elements: a list of the nodes, the coords, or the ids of the nodes on the way
191
+ :param tags: tags of the way
192
+
193
+ :return: added way
194
+ """
195
+ node_ids = []
196
+ for elem in elements:
197
+ if isinstance(elem, OSMNode):
198
+ node_ids.append(self.add_node_from_coord(elem.coord, elem.tags.tags).node_id)
199
+ elif isinstance(elem, Coord):
200
+ node_ids.append(self.add_node_from_coord(elem, {}).node_id)
201
+ else:
202
+ node_ids.append(elem)
203
+ return self.add_way(OSMWay(self.next_way_id, node_ids, OSMTags.from_any_tags(tags)))
204
+
205
+ def write(self, filename: str):
206
+ """
207
+ Writes this OSM file instance to the given file name
208
+
209
+ :param filename: file path to write into
210
+ """
211
+ root = lxml.etree.Element("osm")
212
+ root.attrib.update({
213
+ "version": "0.6",
214
+ "generator": "Plexus",
215
+ })
216
+
217
+ for _, node in self.nodes.items():
218
+ node_elem = lxml.etree.Element("node")
219
+ node_elem.attrib.update(
220
+ {
221
+ "id": str(node.node_id),
222
+ "lat": str(node.coord.lat),
223
+ "lon": str(node.coord.lon),
224
+ "version": "1",
225
+ }
226
+ )
227
+
228
+ for k, v in node.tags.items():
229
+ tag_elem = lxml.etree.Element("tag")
230
+ tag_elem.attrib.update({"k": str(k), "v": str(v)})
231
+ node_elem.append(tag_elem)
232
+
233
+ root.append(node_elem)
234
+
235
+ for _, way in self.ways.items():
236
+ way_elem = lxml.etree.Element("way")
237
+ way_elem.attrib.update(
238
+ {
239
+ "id": str(way.way_id),
240
+ "version": "1",
241
+ }
242
+ )
243
+
244
+ for k, v in way.tags.items():
245
+ tag_elem = lxml.etree.Element("tag")
246
+ tag_elem.attrib.update({"k": str(k), "v": str(v)})
247
+ way_elem.append(tag_elem)
248
+
249
+ for node_id in way.node_ids:
250
+ nd_elem = lxml.etree.Element("nd")
251
+ nd_elem.attrib.update({"ref": str(node_id)})
252
+ way_elem.append(nd_elem)
253
+
254
+ root.append(way_elem)
255
+
256
+ s = lxml.etree.tostring(root, pretty_print=True, encoding="unicode")
257
+
258
+ with open(filename, "w") as fh:
259
+ fh.write(s)
@@ -0,0 +1,25 @@
1
+ from iker.common.utils.strutils import repr_data
2
+
3
+ from plexus.common.carto.OSMTags import OSMTags
4
+ from plexus.common.proj import Coord
5
+
6
+
7
+ class OSMNode(object):
8
+ """
9
+ Represents node in OSM
10
+ """
11
+
12
+ def __init__(self, node_id: int, coord: Coord, tags: OSMTags):
13
+ """
14
+ Creates an instance from the given node id, coord, and tags
15
+
16
+ :param node_id: id
17
+ :param coord: node coordinate
18
+ :param tags: tags
19
+ """
20
+ self.node_id = node_id
21
+ self.coord = coord
22
+ self.tags = tags
23
+
24
+ def __str__(self):
25
+ return repr_data(self)
@@ -0,0 +1,101 @@
1
+ import datetime
2
+ from typing import Any, Self
3
+
4
+ from iker.common.utils.strutils import repr_data, str_conv
5
+
6
+
7
+ class OSMTags(object):
8
+ """
9
+ Represents tag in OSM
10
+ """
11
+
12
+ @classmethod
13
+ def from_any_tags(cls, any_tags: dict[str, Any] = None) -> Self:
14
+ """
15
+ Creates an instance from any-typed tags by automatically inferring data types
16
+
17
+ :param any_tags: tags with any-typed values
18
+
19
+ :return: OSM tag instance
20
+ """
21
+ if any_tags is None:
22
+ return OSMTags({})
23
+ return OSMTags({k: str_conv(v) for k, v in any_tags.items()})
24
+
25
+ def __init__(self, tags: dict[str, Any]):
26
+ """
27
+ Creates an instance from the given tags
28
+
29
+ :param tags: given tags
30
+ """
31
+ self.tags = tags
32
+
33
+ def __str__(self):
34
+ return repr_data(self)
35
+
36
+ def __iter__(self):
37
+ return self.tags.__iter__()
38
+
39
+ def __getitem__(self, k: str):
40
+ return self.tags.__getitem__(k)
41
+
42
+ def items(self):
43
+ return self.tags.items()
44
+
45
+ def set(self, key: str, value: Any) -> Any:
46
+ old_value = self.tags.get(key)
47
+ self.tags[key] = str_conv(value)
48
+ return old_value
49
+
50
+ def get(self, key: str, default: Any | None = None) -> Any | None:
51
+ return self.tags.get(key, default)
52
+
53
+ def getint(self, key: str, default: int | None = None) -> int | None:
54
+ value = self.tags.get(key, default)
55
+ if isinstance(value, int) and not isinstance(value, bool):
56
+ return value
57
+ if default is None:
58
+ return None
59
+ if isinstance(default, int) and not isinstance(default, bool):
60
+ return default
61
+ raise TypeError("type of either the value or the given default is incompatible, should be 'int'")
62
+
63
+ def getfloat(self, key: str, default: float | None = None) -> float | None:
64
+ value = self.tags.get(key, default)
65
+ if isinstance(value, float):
66
+ return value
67
+ if default is None:
68
+ return None
69
+ if isinstance(default, float):
70
+ return default
71
+ raise TypeError("type of either the value or the given default is incompatible, should be 'float'")
72
+
73
+ def getboolean(self, key: str, default: bool | None = None) -> bool | None:
74
+ value = self.tags.get(key, default)
75
+ if isinstance(value, bool):
76
+ return value
77
+ if default is None:
78
+ return None
79
+ if isinstance(default, bool):
80
+ return default
81
+ raise TypeError("type of either the value or the given default is incompatible, should be 'bool'")
82
+
83
+ def getdatetime(self, key: str, default: datetime.datetime | None = None) -> datetime.datetime | None:
84
+ value = self.tags.get(key, default)
85
+ if isinstance(value, datetime.datetime):
86
+ return value
87
+ if default is None:
88
+ return None
89
+ if isinstance(default, datetime.datetime):
90
+ return default
91
+ raise TypeError("type of either the value or the given default is incompatible, should be 'datetime.datetime'")
92
+
93
+ def getstring(self, key: str, default: str | None = None) -> str | None:
94
+ value = self.tags.get(key, default)
95
+ if isinstance(value, str):
96
+ return value
97
+ if default is None:
98
+ return None
99
+ if isinstance(default, str):
100
+ return default
101
+ raise TypeError("type of either the value or the given default is incompatible, should be 'str'")
@@ -0,0 +1,24 @@
1
+ from iker.common.utils.strutils import repr_data
2
+
3
+ from plexus.common.carto.OSMTags import OSMTags
4
+
5
+
6
+ class OSMWay(object):
7
+ """
8
+ Represents way in OSM
9
+ """
10
+
11
+ def __init__(self, way_id: int, node_ids: list[int], tags: OSMTags):
12
+ """
13
+ Creates an instance from the given way id, node ids, and tags
14
+
15
+ :param way_id: id
16
+ :param node_ids: nodes ids
17
+ :param tags: tags
18
+ """
19
+ self.way_id = way_id
20
+ self.node_ids = node_ids
21
+ self.tags = tags
22
+
23
+ def __str__(self):
24
+ return repr_data(self)
@@ -0,0 +1,11 @@
1
+ __all__ = [
2
+ "OSMFile",
3
+ "OSMNode",
4
+ "OSMTags",
5
+ "OSMWay",
6
+ ]
7
+
8
+ from plexus.common.carto.OSMFile import OSMFile
9
+ from plexus.common.carto.OSMNode import OSMNode
10
+ from plexus.common.carto.OSMTags import OSMTags
11
+ from plexus.common.carto.OSMWay import OSMWay
@@ -0,0 +1,84 @@
1
+ import hashlib
2
+
3
+ from iker.common.utils import logger
4
+ from iker.common.utils.config import Config
5
+ from iker.common.utils.funcutils import singleton
6
+ from iker.common.utils.shutils import expanded_path
7
+
8
+
9
+ @singleton
10
+ def config() -> Config:
11
+ default_items: list[tuple[str, str, str]] = [
12
+ ("pulse.commons", "logging.level", "INFO"),
13
+ ("pulse.commons", "logging.format", "%(asctime)s [%(levelname)s] %(name)s: %(message)s"),
14
+ ]
15
+
16
+ config = Config(expanded_path("~/.iker.pulse.cfg"))
17
+ config.restore()
18
+ config.update(default_items, overwrite=False)
19
+
20
+ return config
21
+
22
+
23
+ def validate_pulse_super_token(token: str) -> bool:
24
+ """
25
+ Validate the pulse super token.
26
+ :param token: The token to validate.
27
+ :return: True if the token is valid, False otherwise.
28
+ """
29
+ return hashlib.sha256(token.encode()).hexdigest() == pulse_super_token_sha256()
30
+
31
+
32
+ def ask_pulse_super_token():
33
+ import getpass
34
+ super_token = getpass.getpass("Pulse super token required: ")
35
+ if not validate_pulse_super_token(super_token):
36
+ raise ValueError("wrong Pulse super token")
37
+
38
+
39
+ @singleton
40
+ def pulse_super_token_sha256() -> str:
41
+ return "d7f97abe12bb00a7d19cb5957350509c14b3b085cf84524b80d389c6c2086f1b"
42
+
43
+
44
+ def config_print_or_set(config: Config, section: str, key: str, value: str):
45
+ if value is not None:
46
+ if section is None or key is None:
47
+ raise ValueError("cannot specify value without section and key")
48
+
49
+ old_value = config.get(section, key)
50
+ config.set(section, key, value)
51
+ config.persist()
52
+
53
+ print(f"Configuration file '{config.config_path}'", )
54
+ print(f"Section <{section}>")
55
+ print(f" {key} = {old_value} -> {value}")
56
+
57
+ else:
58
+ if section is None and key is None:
59
+ print(f"Configuration file '{config.config_path}'", )
60
+ for section in config.config_parser.sections():
61
+ print(f"Section <{section}>")
62
+ for key, value in config.config_parser.items(section):
63
+ print(f" {key} = {value}")
64
+
65
+ elif section is not None and key is None:
66
+ if not config.has_section(section):
67
+ logger.warning("Configuration section <%s> not found", section)
68
+ return
69
+ print(f"Configuration file '{config.config_path}'", )
70
+ print(f"Section <{section}>")
71
+ for key, value in config.config_parser.items(section):
72
+ print(f" {key} = {value}")
73
+
74
+ elif section is not None and key is not None:
75
+ value = config.get(section, key)
76
+ if value is None:
77
+ logger.warning("Configuration section <%s> key <%s> not found", section, key)
78
+ return
79
+ print(f"Configuration file '{config.config_path}'", )
80
+ print(f"Section <{section}>")
81
+ print(f" {key} = {value}")
82
+
83
+ else:
84
+ raise ValueError("cannot specify key without section")
plexus/common/pose.py ADDED
@@ -0,0 +1,107 @@
1
+ from typing import Self
2
+
3
+ import numpy as np
4
+ import pyquaternion as pyquat
5
+ from iker.common.utils.strutils import repr_data
6
+
7
+
8
+ class Pose(object):
9
+ @classmethod
10
+ def from_numbers(
11
+ cls,
12
+ px: float,
13
+ py: float,
14
+ pz: float,
15
+ qx: float,
16
+ qy: float,
17
+ qz: float,
18
+ qw: float,
19
+ ts: float = 0,
20
+ ) -> Self:
21
+ """
22
+ Constructs a pose from numbers representing position and orientation
23
+ """
24
+ return Pose(ts, np.array([px, py, pz], dtype=np.float64), np.array([qw, qx, qy, qz], dtype=np.float64))
25
+
26
+ @classmethod
27
+ def add(cls, x: Self, d: Self) -> Self:
28
+ """
29
+ Performs pose SE3 addition, as x + d = y
30
+ """
31
+ xq = pyquat.Quaternion(x.q)
32
+ dq = pyquat.Quaternion(d.q)
33
+
34
+ yp = x.p + xq.rotate(d.p)
35
+ yq = xq * dq
36
+
37
+ return Pose(0, yp, yq.normalised.elements)
38
+
39
+ @classmethod
40
+ def sub(cls, y: Self, x: Self) -> Self:
41
+ """
42
+ Performs pose SE3 subtraction, as x + d = y => d = y - x
43
+ """
44
+ xq = pyquat.Quaternion(x.q)
45
+ yq = pyquat.Quaternion(y.q)
46
+
47
+ dp = xq.inverse.rotate(y.p - x.p)
48
+ dq = xq.inverse * yq
49
+
50
+ return Pose(0, dp, dq.normalised.elements)
51
+
52
+ @classmethod
53
+ def interpolate(cls, a: Self, b: Self, t: float) -> Self:
54
+ """
55
+ Interpolates between two given poses, as a * t + b * (1 - t)
56
+
57
+ :return: interpolated pose
58
+ """
59
+ ts = a.ts + (b.ts - a.ts) * t
60
+ p = a.p + (b.p - a.p) * t
61
+ q = pyquat.Quaternion.slerp(pyquat.Quaternion(a.q), pyquat.Quaternion(b.q), t)
62
+ return Pose(ts, p, q.normalised.elements)
63
+
64
+ def __init__(self, ts: float, p: np.ndarray, q: np.ndarray):
65
+ """
66
+ Represents a pose
67
+
68
+ :param ts: timestamp
69
+ :param p: position vector
70
+ :param q: orientation quaternion
71
+ """
72
+ self.ts = ts
73
+ self.p = p
74
+ self.q = q
75
+
76
+ def matrix(self) -> np.ndarray:
77
+ """
78
+ Returns the transformation matrix of this pose
79
+
80
+ :return: transformation matrix
81
+ """
82
+ r = pyquat.Quaternion(self.q).rotation_matrix
83
+ t = self.p[:, None]
84
+ return np.block([[r, t], [np.zeros((1, 3), dtype=np.float64), np.ones((1, 1), dtype=np.float64)]])
85
+
86
+ def translate(self, v: np.ndarray) -> np.ndarray:
87
+ """
88
+ Translate the given vector with the pose position
89
+
90
+ :param v: the vector to be translated
91
+
92
+ :return: translated vector
93
+ """
94
+ return self.p + v
95
+
96
+ def rotate(self, v: np.ndarray) -> np.ndarray:
97
+ """
98
+ Rotates the given vector with the pose orientation
99
+
100
+ :param v: the vector to be rotated
101
+
102
+ :return: rotated vector
103
+ """
104
+ return pyquat.Quaternion(self.q).rotate(v)
105
+
106
+ def __str__(self):
107
+ return repr_data(self)