plexus-python-common 1.0.44__tar.gz → 1.0.46__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.
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/PKG-INFO +1 -1
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/config.py +2 -2
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/datautils.py +2 -218
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/dockerutils.py +2 -2
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/ormutils.py +109 -18
- plexus_python_common-1.0.46/src/plexus/common/utils/tagutils.py +416 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus_python_common.egg-info/PKG-INFO +1 -1
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus_python_common.egg-info/SOURCES.txt +2 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/utils/datautils_test.py +0 -18
- plexus_python_common-1.0.46/test/plexus_tests/common/utils/tagutils_test.py +95 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/.editorconfig +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/.github/workflows/pr.yml +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/.github/workflows/push.yml +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/.gitignore +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/MANIFEST.in +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/README.md +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/VERSION +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/pyproject.toml +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/jsonutils/dummy.0.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/jsonutils/dummy.1.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/jsonutils/dummy.2.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils_archive/archive.compressed.zip +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/s3utils_archive/archive.uncompressed.zip +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/0-dummy +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/1-dummy +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/2-dummy +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.0.0.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.0.0.vol-0.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.0.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.1.1.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.1.1.vol-1.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.1.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.2.2.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.2.2.vol-2.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.2.jsonl +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.csv.part0 +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.csv.part1 +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.csv.part2 +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/resources/unittest/shutils/dummy.txt +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/setup.cfg +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/setup.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/__init__.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/carto/OSMFile.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/carto/OSMNode.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/carto/OSMTags.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/carto/OSMWay.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/carto/__init__.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/pose.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/proj.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/resources/__init__.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/resources/tags/__init__.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/resources/tags/universal.tagset.yaml +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/__init__.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/apiutils.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/bagutils.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/jsonutils.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/s3utils.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/shutils.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/sqlutils.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/strutils.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/testutils.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus_python_common.egg-info/dependency_links.txt +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus_python_common.egg-info/not-zip-safe +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus_python_common.egg-info/requires.txt +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus_python_common.egg-info/top_level.txt +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/__init__.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/carto/osm_file_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/carto/osm_tags_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/pose_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/proj_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/utils/bagutils_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/utils/dockerutils_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/utils/jsonutils_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/utils/ormutils_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/utils/s3utils_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/utils/shutils_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/utils/strutils_test.py +0 -0
- {plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/test/plexus_tests/common/utils/testutils_test.py +0 -0
{plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/config.py
RENAMED
|
@@ -41,7 +41,7 @@ def config_print_or_set(config: Config, section: str, key: str, value: str):
|
|
|
41
41
|
|
|
42
42
|
elif section is not None and key is None:
|
|
43
43
|
if not config.has_section(section):
|
|
44
|
-
logger.warning("Configuration section
|
|
44
|
+
logger.warning("Configuration section '%s' not found", section)
|
|
45
45
|
return
|
|
46
46
|
print(f"Configuration file '{config.config_path}'", )
|
|
47
47
|
print(f"Section <{section}>")
|
|
@@ -51,7 +51,7 @@ def config_print_or_set(config: Config, section: str, key: str, value: str):
|
|
|
51
51
|
elif section is not None and key is not None:
|
|
52
52
|
value = config.get(section, key)
|
|
53
53
|
if value is None:
|
|
54
|
-
logger.warning("Configuration section
|
|
54
|
+
logger.warning("Configuration section '%s' key '%s' not found", section, key)
|
|
55
55
|
return
|
|
56
56
|
print(f"Configuration file '{config.config_path}'", )
|
|
57
57
|
print(f"Section <{section}>")
|
{plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/datautils.py
RENAMED
|
@@ -1,22 +1,13 @@
|
|
|
1
|
-
import dataclasses
|
|
2
1
|
import datetime
|
|
3
|
-
import
|
|
4
|
-
from collections.abc import Callable, Generator, Mapping, Sequence
|
|
2
|
+
from collections.abc import Callable, Generator
|
|
5
3
|
from typing import Any
|
|
6
4
|
|
|
7
|
-
import jinja2
|
|
8
5
|
import pyparsing as pp
|
|
9
6
|
import ujson as json
|
|
10
7
|
from iker.common.utils.funcutils import singleton
|
|
11
|
-
from iker.common.utils.
|
|
12
|
-
from iker.common.utils.iterutils import dicttree_add, dicttree_remove
|
|
13
|
-
from iker.common.utils.iterutils import dicttree_children, dicttree_lineage, dicttree_subtree
|
|
14
|
-
from iker.common.utils.iterutils import head_or_none
|
|
15
|
-
from iker.common.utils.jsonutils import JsonObject, JsonType
|
|
8
|
+
from iker.common.utils.jsonutils import JsonType
|
|
16
9
|
from iker.common.utils.randutils import randomizer
|
|
17
|
-
from iker.common.utils.strutils import is_blank
|
|
18
10
|
|
|
19
|
-
from plexus.common.resources.tags import predefined_tagset_specs
|
|
20
11
|
from plexus.common.utils.strutils import BagName, UserName, VehicleName
|
|
21
12
|
from plexus.common.utils.strutils import colon_tag_parser, slash_tag_parser
|
|
22
13
|
from plexus.common.utils.strutils import dot_case_parser, kebab_case_parser, snake_case_parser
|
|
@@ -53,12 +44,6 @@ __all__ = [
|
|
|
53
44
|
"known_user_names",
|
|
54
45
|
"known_vehicle_names",
|
|
55
46
|
"random_bag_names_sequence",
|
|
56
|
-
"RichDesc",
|
|
57
|
-
"Tag",
|
|
58
|
-
"Tagset",
|
|
59
|
-
"populate_tagset",
|
|
60
|
-
"predefined_tagsets",
|
|
61
|
-
"render_tagset_markdown_readme",
|
|
62
47
|
]
|
|
63
48
|
|
|
64
49
|
|
|
@@ -244,204 +229,3 @@ def random_bag_names_sequence(
|
|
|
244
229
|
|
|
245
230
|
for record_sn in range(bags_count):
|
|
246
231
|
yield BagName(vehicle_name=vehicle_name, record_dt=record_dt, record_sn=record_sn)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
@dataclasses.dataclass(frozen=True, eq=True, order=True)
|
|
250
|
-
class RichDesc(object):
|
|
251
|
-
type: str
|
|
252
|
-
text: str
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
@dataclasses.dataclass(frozen=True, eq=True, order=True)
|
|
256
|
-
class Tag(object):
|
|
257
|
-
name: str
|
|
258
|
-
desc: str | RichDesc | None
|
|
259
|
-
|
|
260
|
-
@property
|
|
261
|
-
def tag_parts(self) -> list[str]:
|
|
262
|
-
return self.name.rsplit(":")
|
|
263
|
-
|
|
264
|
-
@property
|
|
265
|
-
def parent_tag_name(self) -> str | None:
|
|
266
|
-
return head_or_none(self.name.rsplit(":", 1))
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
class Tagset(Sequence[Tag], Mapping[str, Tag]):
|
|
270
|
-
def __init__(self, namespace: str, desc: str | RichDesc) -> None:
|
|
271
|
-
super().__init__()
|
|
272
|
-
self.namespace = namespace
|
|
273
|
-
self.desc = desc
|
|
274
|
-
self.tags: list[Tag] = []
|
|
275
|
-
self.tags_dict: dict[str, Tag] = {}
|
|
276
|
-
self.tags_tree: dicttree[str, Tag] = {}
|
|
277
|
-
|
|
278
|
-
def __contains__(self, item: str | Tag) -> bool:
|
|
279
|
-
tag_name = item.name if isinstance(item, Tag) else item
|
|
280
|
-
return tag_name in self.tags_dict
|
|
281
|
-
|
|
282
|
-
def __len__(self) -> int:
|
|
283
|
-
return len(self.tags)
|
|
284
|
-
|
|
285
|
-
def __getitem__(self, index: int) -> Tag:
|
|
286
|
-
return self.tags[index]
|
|
287
|
-
|
|
288
|
-
def keys(self):
|
|
289
|
-
return self.tags_dict.keys()
|
|
290
|
-
|
|
291
|
-
def values(self):
|
|
292
|
-
return self.tags_dict.values()
|
|
293
|
-
|
|
294
|
-
def items(self):
|
|
295
|
-
return self.tags_dict.items()
|
|
296
|
-
|
|
297
|
-
def get(self, item: str | Tag) -> Tag | None:
|
|
298
|
-
tag_name = item.name if isinstance(item, Tag) else item
|
|
299
|
-
return self.tags_dict.get(tag_name)
|
|
300
|
-
|
|
301
|
-
@property
|
|
302
|
-
def tag_names(self) -> list[str]:
|
|
303
|
-
return list(self.tags_dict.keys())
|
|
304
|
-
|
|
305
|
-
def add(self, tag: Tag) -> None:
|
|
306
|
-
if tag.name in self.tags_dict:
|
|
307
|
-
raise ValueError(f"duplicate tag name '{tag.name}'")
|
|
308
|
-
|
|
309
|
-
self.tags.append(tag)
|
|
310
|
-
self.tags_dict[tag.name] = tag
|
|
311
|
-
|
|
312
|
-
dicttree_add(self.tags_tree, tag.tag_parts, tag, create_prefix=True)
|
|
313
|
-
|
|
314
|
-
def remove(self, tag_name: str) -> None:
|
|
315
|
-
tag = self.get(tag_name)
|
|
316
|
-
if tag is None:
|
|
317
|
-
raise ValueError(f"tag '{tag_name}' not found")
|
|
318
|
-
|
|
319
|
-
self.tags.remove(tag)
|
|
320
|
-
del self.tags_dict[tag_name]
|
|
321
|
-
|
|
322
|
-
dicttree_remove(self.tags_tree, tag.tag_parts, recursive=True)
|
|
323
|
-
|
|
324
|
-
def child_tags(self, parent: str | Tag) -> list[Tag]:
|
|
325
|
-
if parent is None:
|
|
326
|
-
return []
|
|
327
|
-
if isinstance(parent, str):
|
|
328
|
-
return self.child_tags(self.get(parent))
|
|
329
|
-
|
|
330
|
-
subtree = dicttree_subtree(self.tags_tree, parent.tag_parts)
|
|
331
|
-
return list(dicttree_children(subtree)) if subtree else []
|
|
332
|
-
|
|
333
|
-
def parent_tags(self, child: str | Tag) -> list[Tag]:
|
|
334
|
-
if child is None:
|
|
335
|
-
return []
|
|
336
|
-
if isinstance(child, str):
|
|
337
|
-
return self.parent_tags(self.get(child))
|
|
338
|
-
|
|
339
|
-
return list(dicttree_lineage(self.tags_tree, child.tag_parts[:-1]))
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
def populate_tagset(tagset_spec: JsonObject) -> Tagset:
|
|
343
|
-
"""
|
|
344
|
-
Collect tags from tagset spec JSON object, validate the format along the way.
|
|
345
|
-
|
|
346
|
-
:param tagset_spec: JSON object of tagset spec
|
|
347
|
-
:return: Populated Tagset instance
|
|
348
|
-
"""
|
|
349
|
-
|
|
350
|
-
def validate_and_collect(name: str, tag_def: JsonObject) -> Generator[Tag, None, None]:
|
|
351
|
-
if not isinstance(tag_def, dict):
|
|
352
|
-
raise ValueError(f"tag '{name}' definition is not a dict")
|
|
353
|
-
|
|
354
|
-
if not is_blank(name):
|
|
355
|
-
desc = tag_def.get("$desc")
|
|
356
|
-
if isinstance(desc, dict):
|
|
357
|
-
desc = RichDesc(**desc)
|
|
358
|
-
|
|
359
|
-
yield Tag(name=name, desc=desc)
|
|
360
|
-
|
|
361
|
-
for child_name, child_tag_def in tag_def.items():
|
|
362
|
-
if child_name == "$desc":
|
|
363
|
-
continue
|
|
364
|
-
|
|
365
|
-
if not isinstance(child_name, str):
|
|
366
|
-
raise ValueError(f"child '{child_name}' of tag '{name}' is not a string")
|
|
367
|
-
try:
|
|
368
|
-
validate_snake_case(child_name)
|
|
369
|
-
except ValueError as e:
|
|
370
|
-
raise ValueError(f"child '{child_name}' of tag '{name}' is not in snake case") from e
|
|
371
|
-
|
|
372
|
-
child_name = name + ":" + child_name if name else child_name
|
|
373
|
-
|
|
374
|
-
yield from validate_and_collect(child_name, child_tag_def)
|
|
375
|
-
|
|
376
|
-
namespace = tagset_spec.get("$namespace")
|
|
377
|
-
if namespace is None:
|
|
378
|
-
raise ValueError("missing '$namespace' in tagset spec")
|
|
379
|
-
try:
|
|
380
|
-
validate_snake_case(namespace)
|
|
381
|
-
except ValueError as e:
|
|
382
|
-
raise ValueError(f"tagset namespace '{namespace}' is not in snake case") from e
|
|
383
|
-
|
|
384
|
-
desc = tagset_spec.get("$desc")
|
|
385
|
-
if desc is None:
|
|
386
|
-
raise ValueError("missing '$desc' in tagset spec")
|
|
387
|
-
if isinstance(desc, dict):
|
|
388
|
-
desc = RichDesc(**desc)
|
|
389
|
-
|
|
390
|
-
tags = tagset_spec.get("$tags")
|
|
391
|
-
if tags is None:
|
|
392
|
-
raise ValueError("missing '$tags' in tagset spec")
|
|
393
|
-
|
|
394
|
-
tagset = Tagset(namespace=namespace, desc=desc)
|
|
395
|
-
|
|
396
|
-
for tag in validate_and_collect("", tags):
|
|
397
|
-
tagset.add(tag)
|
|
398
|
-
|
|
399
|
-
return tagset
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
@singleton
|
|
403
|
-
def predefined_tagsets() -> dict[str, Tagset]:
|
|
404
|
-
tagsets: dict[str, Tagset] = {}
|
|
405
|
-
for _, tagset_spec in predefined_tagset_specs():
|
|
406
|
-
tagset = populate_tagset(tagset_spec)
|
|
407
|
-
tagsets[tagset.namespace] = tagset
|
|
408
|
-
return tagsets
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
def render_tagset_markdown_readme(tagset: Tagset) -> str:
|
|
412
|
-
def render_desc(desc: str | RichDesc | None) -> str:
|
|
413
|
-
if desc is None:
|
|
414
|
-
return ""
|
|
415
|
-
if isinstance(desc, str):
|
|
416
|
-
return desc
|
|
417
|
-
if isinstance(desc, RichDesc):
|
|
418
|
-
return desc.text
|
|
419
|
-
raise ValueError(f"unsupported desc type '{type(desc)}'")
|
|
420
|
-
|
|
421
|
-
template_str = textwrap.dedent(
|
|
422
|
-
"""
|
|
423
|
-
# Tagset {{ tagset.namespace }}
|
|
424
|
-
|
|
425
|
-
{{ tagset.desc | render_desc }}
|
|
426
|
-
|
|
427
|
-
## Contents
|
|
428
|
-
|
|
429
|
-
{% for tag in tagset.tags %}
|
|
430
|
-
- {{ tag.name }}
|
|
431
|
-
{% endfor %}
|
|
432
|
-
|
|
433
|
-
## Tags
|
|
434
|
-
|
|
435
|
-
{% for tag in tagset.tags %}
|
|
436
|
-
### {{ tag.name }}
|
|
437
|
-
|
|
438
|
-
{{ tag.desc | render_desc }}
|
|
439
|
-
|
|
440
|
-
{% endfor %}
|
|
441
|
-
"""
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
env = jinja2.Environment(trim_blocks=True, lstrip_blocks=True)
|
|
445
|
-
env.filters["render_desc"] = render_desc
|
|
446
|
-
|
|
447
|
-
return env.from_string(template_str).render(tagset=tagset)
|
{plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/dockerutils.py
RENAMED
|
@@ -110,11 +110,11 @@ def docker_pull_image(
|
|
|
110
110
|
except docker.errors.ImageNotFound:
|
|
111
111
|
if not fallback_local:
|
|
112
112
|
raise
|
|
113
|
-
logger.warning("Image
|
|
113
|
+
logger.warning("Image '%s' is not found from remote repository, try local repository instead", image)
|
|
114
114
|
except docker.errors.APIError:
|
|
115
115
|
if not fallback_local:
|
|
116
116
|
raise
|
|
117
|
-
logger.warning("Docker server returns an error while pulling image
|
|
117
|
+
logger.warning("Docker server returns an error while pulling image '%s', try local repository instead", image)
|
|
118
118
|
|
|
119
119
|
return docker_get_image(client, image)
|
|
120
120
|
|
{plexus_python_common-1.0.44 → plexus_python_common-1.0.46}/src/plexus/common/utils/ormutils.py
RENAMED
|
@@ -4,8 +4,10 @@ from typing import Protocol, Self
|
|
|
4
4
|
import pydantic as pdt
|
|
5
5
|
import sqlalchemy as sa
|
|
6
6
|
import sqlalchemy.dialects.postgresql as sa_pg
|
|
7
|
+
import sqlalchemy.dialects.sqlite as sa_sqlite
|
|
7
8
|
import sqlalchemy.exc as sa_exc
|
|
8
9
|
import sqlalchemy.orm as sa_orm
|
|
10
|
+
from iker.common.utils.dbutils import dialects
|
|
9
11
|
from sqlmodel import Field, SQLModel
|
|
10
12
|
|
|
11
13
|
from plexus.common.utils.datautils import validate_dt_timezone
|
|
@@ -13,6 +15,7 @@ from plexus.common.utils.jsonutils import json_datetime_encoder
|
|
|
13
15
|
|
|
14
16
|
__all__ = [
|
|
15
17
|
"compare_postgresql_types",
|
|
18
|
+
"compare_sqlite_types",
|
|
16
19
|
"model_name_of",
|
|
17
20
|
"validate_model_extended",
|
|
18
21
|
"collect_model_tables",
|
|
@@ -110,6 +113,30 @@ def compare_postgresql_types(type_a, type_b) -> bool:
|
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
|
|
116
|
+
def compare_sqlite_types(type_a, type_b) -> bool:
|
|
117
|
+
"""
|
|
118
|
+
Compares two SQLite-specific column types to determine if they are equivalent.
|
|
119
|
+
This includes types from sqlalchemy.dialects.sqlite like JSON, etc.
|
|
120
|
+
"""
|
|
121
|
+
if not isinstance(type_a, type(type_b)):
|
|
122
|
+
return False
|
|
123
|
+
if isinstance(type_a, (sa_sqlite.VARCHAR, sa_sqlite.CHAR, sa_sqlite.TEXT)):
|
|
124
|
+
return type_a.length == type_b.length
|
|
125
|
+
if isinstance(type_a, (sa_sqlite.TIMESTAMP, sa_sqlite.TIME)):
|
|
126
|
+
return type_a.timezone == type_b.timezone
|
|
127
|
+
if isinstance(type_a, (sa_sqlite.NUMERIC, sa_sqlite.DECIMAL)):
|
|
128
|
+
return type_a.precision == type_b.precision and type_a.scale == type_b.scale
|
|
129
|
+
return type(type_a) in {
|
|
130
|
+
sa_sqlite.BOOLEAN,
|
|
131
|
+
sa_sqlite.INTEGER,
|
|
132
|
+
sa_sqlite.SMALLINT,
|
|
133
|
+
sa_sqlite.FLOAT,
|
|
134
|
+
sa_sqlite.REAL,
|
|
135
|
+
sa_sqlite.DATE,
|
|
136
|
+
sa_sqlite.JSON,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
113
140
|
def model_name_of(model: type[SQLModel], fallback_classname: bool = True) -> str | None:
|
|
114
141
|
table_name = getattr(model, "__tablename__")
|
|
115
142
|
if not table_name:
|
|
@@ -317,15 +344,67 @@ SnapshotModelMixin = SnapshotModelMixinProtocol | SQLModel
|
|
|
317
344
|
RevisionModelMixin = RevisionModelMixinProtocol | SQLModel
|
|
318
345
|
|
|
319
346
|
|
|
320
|
-
def
|
|
347
|
+
def model_sqn_type(dialect: str | None = None) -> sa.types.TypeEngine[int]:
|
|
348
|
+
"""
|
|
349
|
+
Returns the appropriate SQLAlchemy column type for a sequence number (sqn) based on the specified database dialect.
|
|
350
|
+
|
|
351
|
+
:param dialect: The database dialect to determine the column type for. If None, defaults to PostgreSQL.
|
|
352
|
+
:return: The SQLAlchemy column type to use for the sqn field.
|
|
353
|
+
"""
|
|
354
|
+
if dialect is None:
|
|
355
|
+
dialect = dialects.postgresql
|
|
356
|
+
if dialect == dialects.postgresql:
|
|
357
|
+
return sa_pg.BIGINT()
|
|
358
|
+
if dialect == dialects.sqlite:
|
|
359
|
+
return sa_sqlite.INTEGER()
|
|
360
|
+
raise ValueError(f"unsupported database dialect '{dialect}'")
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def model_datetime_tz_type(dialect: str | None = None) -> sa.types.TypeEngine[datetime.datetime]:
|
|
364
|
+
"""
|
|
365
|
+
Returns the appropriate SQLAlchemy column type for a timezone-aware datetime field based on the specified database
|
|
366
|
+
dialect.
|
|
367
|
+
|
|
368
|
+
:param dialect: The database dialect to determine the column type for. If None, defaults to PostgreSQL.
|
|
369
|
+
:return: The SQLAlchemy column type to use for timezone-aware datetime fields.
|
|
370
|
+
"""
|
|
371
|
+
if dialect is None:
|
|
372
|
+
dialect = dialects.postgresql
|
|
373
|
+
if dialect == dialects.postgresql:
|
|
374
|
+
return sa_pg.TIMESTAMP(timezone=True)
|
|
375
|
+
if dialect == dialects.sqlite:
|
|
376
|
+
return sa_sqlite.TIMESTAMP(timezone=True)
|
|
377
|
+
raise ValueError(f"unsupported database dialect '{dialect}'")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def model_revision_type(dialect: str | None = None) -> sa.types.TypeEngine[int]:
|
|
381
|
+
"""
|
|
382
|
+
Returns the appropriate SQLAlchemy column type for a revision number field based on the specified database dialect.
|
|
383
|
+
|
|
384
|
+
:param dialect: The database dialect to determine the column type for. If None, defaults to PostgreSQL.
|
|
385
|
+
:return: The SQLAlchemy column type to use for revision number fields.
|
|
386
|
+
"""
|
|
387
|
+
if dialect is None:
|
|
388
|
+
dialect = dialects.postgresql
|
|
389
|
+
if dialect == dialects.postgresql:
|
|
390
|
+
return sa_pg.INTEGER()
|
|
391
|
+
if dialect == dialects.sqlite:
|
|
392
|
+
return sa_sqlite.INTEGER()
|
|
393
|
+
raise ValueError(f"unsupported database dialect '{dialect}'")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def make_sequence_model_mixin(dialect: str | None = None) -> type[SequenceModelMixin]:
|
|
321
397
|
"""
|
|
322
398
|
Creates a mixin class for SQLModel models that adds a unique identifier field `sqn`.
|
|
323
399
|
Use this mixin to add an auto-incremented primary key to your models.
|
|
400
|
+
|
|
401
|
+
:param dialect: The database dialect to determine the column type for the `sqn` field.
|
|
402
|
+
:return: A mixin class that can be used with SQLModel models to add the `sqn` field.
|
|
324
403
|
"""
|
|
325
404
|
|
|
326
405
|
class ModelMixin(SQLModel):
|
|
327
406
|
sqn: int | None = Field(
|
|
328
|
-
sa_column=sa.Column(
|
|
407
|
+
sa_column=sa.Column(model_sqn_type(dialect), primary_key=True, autoincrement=True),
|
|
329
408
|
default=None,
|
|
330
409
|
description="Unique auto-incremented primary key for the record",
|
|
331
410
|
)
|
|
@@ -333,25 +412,29 @@ def make_sequence_model_mixin() -> type[SequenceModelMixin]:
|
|
|
333
412
|
return ModelMixin
|
|
334
413
|
|
|
335
414
|
|
|
336
|
-
def make_changing_model_mixin() -> type[ChangingModelMixin]:
|
|
415
|
+
def make_changing_model_mixin(dialect: str | None = None) -> type[ChangingModelMixin]:
|
|
337
416
|
"""
|
|
338
417
|
Creates a mixin class for SQLModel models that adds common fields and validation logic for updatable records.
|
|
339
418
|
This mixin includes ``sqn``, ``created_at``, and ``updated_at`` fields, along with validation for timestamps.
|
|
419
|
+
|
|
420
|
+
:param dialect: The database dialect to determine the column types for the fields.
|
|
421
|
+
:return: A mixin class that can be used with SQLModel models to add the common fields and validation logic for
|
|
422
|
+
updatable records.
|
|
340
423
|
"""
|
|
341
424
|
|
|
342
425
|
class ModelMixin(SQLModel):
|
|
343
426
|
sqn: int | None = Field(
|
|
344
|
-
sa_column=sa.Column(
|
|
427
|
+
sa_column=sa.Column(model_sqn_type(dialect), primary_key=True, autoincrement=True),
|
|
345
428
|
default=None,
|
|
346
429
|
description="Unique auto-incremented primary key for the record",
|
|
347
430
|
)
|
|
348
431
|
created_at: datetime.datetime | None = Field(
|
|
349
|
-
sa_column=sa.Column(
|
|
432
|
+
sa_column=sa.Column(model_datetime_tz_type(dialect)),
|
|
350
433
|
default=None,
|
|
351
434
|
description="Timestamp (with timezone) when the record was created",
|
|
352
435
|
)
|
|
353
436
|
updated_at: datetime.datetime | None = Field(
|
|
354
|
-
sa_column=sa.Column(
|
|
437
|
+
sa_column=sa.Column(model_datetime_tz_type(dialect)),
|
|
355
438
|
default=None,
|
|
356
439
|
description="Timestamp (with timezone) when the record was last updated",
|
|
357
440
|
)
|
|
@@ -383,7 +466,7 @@ def make_changing_model_mixin() -> type[ChangingModelMixin]:
|
|
|
383
466
|
return ModelMixin
|
|
384
467
|
|
|
385
468
|
|
|
386
|
-
def make_snapshot_model_mixin() -> type[SnapshotModelMixin]:
|
|
469
|
+
def make_snapshot_model_mixin(dialect: str | None = None) -> type[SnapshotModelMixin]:
|
|
387
470
|
"""
|
|
388
471
|
Provides a mixin class for SQLModel models that adds common fields and validation logic for record snapshots.
|
|
389
472
|
A snapshot model tracks the full change history of an entity: when any field changes, the current record (with a
|
|
@@ -395,26 +478,30 @@ def make_snapshot_model_mixin() -> type[SnapshotModelMixin]:
|
|
|
395
478
|
- ``expired_at``: Time (with timezone) when this snapshot of the record was superseded or became inactive;
|
|
396
479
|
``None`` if still active.
|
|
397
480
|
- ``record_sqn``: Foreign key to the record this snapshot belongs to; used to link snapshots together.
|
|
481
|
+
|
|
482
|
+
:param dialect: The database dialect to determine the column types for the fields.
|
|
483
|
+
:return: A mixin class that can be used with SQLModel models to add the common fields and validation logic for
|
|
484
|
+
record snapshots.
|
|
398
485
|
"""
|
|
399
486
|
|
|
400
487
|
class ModelMixin(SQLModel):
|
|
401
488
|
sqn: int | None = Field(
|
|
402
|
-
sa_column=sa.Column(
|
|
489
|
+
sa_column=sa.Column(model_sqn_type(dialect), primary_key=True, autoincrement=True),
|
|
403
490
|
default=None,
|
|
404
491
|
description="Unique auto-incremented primary key for each record snapshot",
|
|
405
492
|
)
|
|
406
493
|
created_at: datetime.datetime | None = Field(
|
|
407
|
-
sa_column=sa.Column(
|
|
494
|
+
sa_column=sa.Column(model_datetime_tz_type(dialect)),
|
|
408
495
|
default=None,
|
|
409
496
|
description="Timestamp (with timezone) when this record snapshot became active",
|
|
410
497
|
)
|
|
411
498
|
expired_at: datetime.datetime | None = Field(
|
|
412
|
-
sa_column=sa.Column(
|
|
499
|
+
sa_column=sa.Column(model_datetime_tz_type(dialect)),
|
|
413
500
|
default=None,
|
|
414
501
|
description="Timestamp (with timezone) when this record snapshot became inactive; None if still active",
|
|
415
502
|
)
|
|
416
503
|
record_sqn: int | None = Field(
|
|
417
|
-
sa_column=sa.Column(
|
|
504
|
+
sa_column=sa.Column(model_sqn_type(dialect), nullable=True),
|
|
418
505
|
default=None,
|
|
419
506
|
description="Foreign key to the record this snapshot belongs to",
|
|
420
507
|
)
|
|
@@ -472,7 +559,7 @@ def make_snapshot_model_mixin() -> type[SnapshotModelMixin]:
|
|
|
472
559
|
return ModelMixin
|
|
473
560
|
|
|
474
561
|
|
|
475
|
-
def make_revision_model_mixin() -> type[RevisionModelMixin]:
|
|
562
|
+
def make_revision_model_mixin(dialect: str | None = None) -> type[RevisionModelMixin]:
|
|
476
563
|
"""
|
|
477
564
|
Provides a mixin class for SQLModel models that adds common fields and validation logic for record revisions.
|
|
478
565
|
A revision model tracks the full change history of an entity: when any field changes, the current record (with a
|
|
@@ -486,36 +573,40 @@ def make_revision_model_mixin() -> type[RevisionModelMixin]:
|
|
|
486
573
|
``None`` if still active.
|
|
487
574
|
- ``record_sqn``: Auto-incremented key of the record this revision belongs to; used to link revisions together.
|
|
488
575
|
- ``revision``: Revision number for the record, used to track changes over time.
|
|
576
|
+
|
|
577
|
+
:param dialect: The database dialect to determine the column types for the fields.
|
|
578
|
+
:return: A mixin class that can be used with SQLModel models to add the common fields and validation logic for
|
|
579
|
+
record revisions.
|
|
489
580
|
"""
|
|
490
581
|
|
|
491
582
|
class ModelMixin(SQLModel):
|
|
492
583
|
sqn: int | None = Field(
|
|
493
|
-
sa_column=sa.Column(
|
|
584
|
+
sa_column=sa.Column(model_sqn_type(dialect), primary_key=True, autoincrement=True),
|
|
494
585
|
default=None,
|
|
495
586
|
description="Unique auto-incremented primary key for each record revision",
|
|
496
587
|
)
|
|
497
588
|
created_at: datetime.datetime | None = Field(
|
|
498
|
-
sa_column=sa.Column(
|
|
589
|
+
sa_column=sa.Column(model_datetime_tz_type(dialect)),
|
|
499
590
|
default=None,
|
|
500
591
|
description="Timestamp (with timezone) when this record is first created (preserved across revisions)",
|
|
501
592
|
)
|
|
502
593
|
updated_at: datetime.datetime | None = Field(
|
|
503
|
-
sa_column=sa.Column(
|
|
594
|
+
sa_column=sa.Column(model_datetime_tz_type(dialect)),
|
|
504
595
|
default=None,
|
|
505
596
|
description="Timestamp (with timezone) when this record is updated and this record revision became active",
|
|
506
597
|
)
|
|
507
598
|
expired_at: datetime.datetime | None = Field(
|
|
508
|
-
sa_column=sa.Column(
|
|
599
|
+
sa_column=sa.Column(model_datetime_tz_type(dialect)),
|
|
509
600
|
default=None,
|
|
510
601
|
description="Timestamp (with timezone) when this record revision became inactive; None if still active",
|
|
511
602
|
)
|
|
512
603
|
record_sqn: int | None = Field(
|
|
513
|
-
sa_column=sa.Column(
|
|
604
|
+
sa_column=sa.Column(model_sqn_type(dialect), nullable=True),
|
|
514
605
|
default=None,
|
|
515
606
|
description="Auto-incremented key of the record this revision belongs to",
|
|
516
607
|
)
|
|
517
608
|
revision: int | None = Field(
|
|
518
|
-
sa_column=sa.Column(
|
|
609
|
+
sa_column=sa.Column(model_revision_type(dialect), nullable=True),
|
|
519
610
|
default=None,
|
|
520
611
|
description="Revision number for the record",
|
|
521
612
|
)
|