plexus-python-common 1.0.8__tar.gz → 1.0.10__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.
Files changed (65) hide show
  1. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/PKG-INFO +1 -1
  2. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/utils/ormutils.py +62 -6
  3. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/utils/strutils.py +13 -2
  4. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus_python_common.egg-info/PKG-INFO +1 -1
  5. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_tests/common/utils/ormutils_test.py +6 -0
  6. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_tests/common/utils/strutils_test.py +39 -0
  7. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/.editorconfig +0 -0
  8. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/.github/workflows/pr.yml +0 -0
  9. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/.github/workflows/push.yml +0 -0
  10. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/.gitignore +0 -0
  11. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/MANIFEST.in +0 -0
  12. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/README.md +0 -0
  13. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/VERSION +0 -0
  14. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/pyproject.toml +0 -0
  15. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/jsonutils/dummy.0.jsonl +0 -0
  16. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/jsonutils/dummy.1.jsonl +0 -0
  17. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/jsonutils/dummy.2.jsonl +0 -0
  18. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/0-dummy +0 -0
  19. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/1-dummy +0 -0
  20. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/2-dummy +0 -0
  21. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.0.0.jsonl +0 -0
  22. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.0.0.vol-0.jsonl +0 -0
  23. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.0.jsonl +0 -0
  24. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.1.1.jsonl +0 -0
  25. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.1.1.vol-1.jsonl +0 -0
  26. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.1.jsonl +0 -0
  27. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.2.2.jsonl +0 -0
  28. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.2.2.vol-2.jsonl +0 -0
  29. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.2.jsonl +0 -0
  30. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.csv.part0 +0 -0
  31. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.csv.part1 +0 -0
  32. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.csv.part2 +0 -0
  33. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/resources/unittest/shutils/dummy.txt +0 -0
  34. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/setup.cfg +0 -0
  35. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/setup.py +0 -0
  36. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/__init__.py +0 -0
  37. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/carto/OSMFile.py +0 -0
  38. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/carto/OSMNode.py +0 -0
  39. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/carto/OSMTags.py +0 -0
  40. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/carto/OSMWay.py +0 -0
  41. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/carto/__init__.py +0 -0
  42. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/config.py +0 -0
  43. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/pose.py +0 -0
  44. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/proj.py +0 -0
  45. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/utils/__init__.py +0 -0
  46. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/utils/bagutils.py +0 -0
  47. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/utils/datautils.py +0 -0
  48. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/utils/jsonutils.py +0 -0
  49. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/utils/s3utils.py +0 -0
  50. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus/common/utils/shutils.py +0 -0
  51. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus_python_common.egg-info/SOURCES.txt +0 -0
  52. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus_python_common.egg-info/dependency_links.txt +0 -0
  53. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus_python_common.egg-info/not-zip-safe +0 -0
  54. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus_python_common.egg-info/requires.txt +0 -0
  55. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/src/plexus_python_common.egg-info/top_level.txt +0 -0
  56. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_test.py +0 -0
  57. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_tests/__init__.py +0 -0
  58. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_tests/common/carto/osm_file_test.py +0 -0
  59. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_tests/common/carto/osm_tags_test.py +0 -0
  60. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_tests/common/pose_test.py +0 -0
  61. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_tests/common/proj_test.py +0 -0
  62. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_tests/common/utils/bagutils_test.py +0 -0
  63. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_tests/common/utils/datautils_test.py +0 -0
  64. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_tests/common/utils/jsonutils_test.py +0 -0
  65. {plexus_python_common-1.0.8 → plexus_python_common-1.0.10}/test/plexus_tests/common/utils/shutils_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python-common
3
- Version: 1.0.8
3
+ Version: 1.0.10
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -1,5 +1,5 @@
1
1
  import datetime
2
- from typing import Self, TypeVar
2
+ from typing import Protocol, Self, TypeVar
3
3
 
4
4
  import pydantic as pdt
5
5
  import sqlalchemy as sa
@@ -127,7 +127,11 @@ def make_base_model() -> type[SQLModel]:
127
127
  return BaseModel
128
128
 
129
129
 
130
- def make_serial_model_mixin() -> type[SQLModel]:
130
+ class SerialModelMixinProto(Protocol):
131
+ sid: int | None
132
+
133
+
134
+ def make_serial_model_mixin() -> type[SerialModelMixinProto]:
131
135
  """
132
136
  Creates a mixin class for SQLModel models that adds a unique identifier field `sid`.
133
137
  Use this mixin to add an auto-incremented primary key to your models.
@@ -143,7 +147,16 @@ def make_serial_model_mixin() -> type[SQLModel]:
143
147
  return ModelMixin
144
148
 
145
149
 
146
- def make_record_model_mixin() -> type[SQLModel]:
150
+ class RecordModelMixinProto(Protocol):
151
+ sid: int | None
152
+ created_at: datetime.datetime | None
153
+ updated_at: datetime.datetime | None
154
+
155
+ @classmethod
156
+ def make_index_created_at(cls, index_name: str) -> sa.Index: ...
157
+
158
+
159
+ def make_record_model_mixin() -> type[RecordModelMixinProto]:
147
160
  """
148
161
  Creates a mixin class for SQLModel models that adds common fields and validation logic for updatable records.
149
162
  This mixin includes `sid`, `created_at`, and `updated_at` fields, along with validation for timestamps.
@@ -188,7 +201,7 @@ def make_record_model_mixin() -> type[SQLModel]:
188
201
  return m
189
202
 
190
203
  @classmethod
191
- def make_created_at_index(cls, index_name: str) -> sa.Index:
204
+ def make_index_created_at(cls, index_name: str) -> sa.Index:
192
205
  """
193
206
  Helper to create an index on the `created_at` field with the given index name.
194
207
  """
@@ -197,7 +210,23 @@ def make_record_model_mixin() -> type[SQLModel]:
197
210
  return ModelMixin
198
211
 
199
212
 
200
- def make_snapshot_model_mixin() -> type[SQLModel]:
213
+ class SnapshotModelMixinProto(Protocol):
214
+ sid: int | None
215
+ created_at: datetime.datetime | None
216
+ expired_at: datetime.datetime | None
217
+ record_sid: int | None
218
+
219
+ @classmethod
220
+ def make_index_created_at_expired_at(cls, index_name: str) -> sa.Index: ...
221
+
222
+ @classmethod
223
+ def make_active_unique_index_record_sid(cls, index_name: str) -> sa.Index: ...
224
+
225
+ @classmethod
226
+ def make_active_unique_index_for(cls, index_name: str, *fields: str) -> sa.Index: ...
227
+
228
+
229
+ def make_snapshot_model_mixin() -> type[SnapshotModelMixinProto]:
201
230
  """
202
231
  Provides a mixin class for SQLModel models that adds common fields and validation logic for record snapshots.
203
232
  A snapshot model tracks the full change history of an entity: when any field changes, the current record (with a
@@ -255,9 +284,36 @@ def make_snapshot_model_mixin() -> type[SQLModel]:
255
284
  return m
256
285
 
257
286
  @classmethod
258
- def make_created_at_expired_at_index(cls, index_name: str) -> sa.Index:
287
+ def make_index_created_at_expired_at(cls, index_name: str) -> sa.Index:
259
288
  return sa.Index(index_name, "created_at", "expired_at")
260
289
 
290
+ @classmethod
291
+ def make_active_unique_index_record_sid(cls, index_name: str) -> sa.Index:
292
+ """
293
+ Helper to create a unique index on the `record_sid` field for active records (where `expired_at` is NULL).
294
+ This ensures that there is only one active snapshot per record at any given time.
295
+ """
296
+ return sa.Index(
297
+ index_name,
298
+ "record_sid",
299
+ unique=True,
300
+ postgresql_where=sa.text('"expired_at" IS NULL'),
301
+ )
302
+
303
+ @classmethod
304
+ def make_active_unique_index_for(cls, index_name: str, *fields: str) -> sa.Index:
305
+ """
306
+ Helper to create a unique index on the specified fields for active records (where `expired_at` is NULL).
307
+ This ensures that there is only one active snapshot per combination of the specified fields at any given
308
+ time.
309
+ """
310
+ return sa.Index(
311
+ index_name,
312
+ *fields,
313
+ unique=True,
314
+ postgresql_where=sa.text('"expired_at" IS NULL'),
315
+ )
316
+
261
317
  return ModelMixin
262
318
 
263
319
 
@@ -16,12 +16,14 @@ __all__ = [
16
16
  "kebab_case_parser",
17
17
  "dot_case_pattern",
18
18
  "dot_case_parser",
19
+ "uuid_pattern",
20
+ "uuid_parser",
19
21
  "strict_relpath_pattern",
20
22
  "strict_relpath_parser",
21
23
  "strict_abspath_pattern",
22
24
  "strict_abspath_parser",
23
- "uuid_pattern",
24
- "uuid_parser",
25
+ "semver_pattern",
26
+ "semver_parser",
25
27
  "tag_pattern",
26
28
  "tag_parser",
27
29
  "topic_pattern",
@@ -138,6 +140,12 @@ strict_relpath_element: pp.ParserElement = pp.Combine(
138
140
  strict_abspath_element: pp.ParserElement = pp.Combine(
139
141
  slash_token + (strict_path_chars_element + slash_token)[...] + strict_path_chars_element[0, 1])
140
142
 
143
+ semver_regexp: re.Pattern[str] = re.compile(
144
+ rf"({number_regexp.pattern})\.({number_regexp.pattern})\.({number_regexp.pattern})"
145
+ rf"(?:-{alpha_digits_regexp.pattern}(?:\.{alpha_digits_regexp.pattern})*)?"
146
+ rf"(?:\+{alpha_digits_regexp.pattern}(?:\.{alpha_digits_regexp.pattern})*)?")
147
+ semver_element: pp.ParserElement = pp.Regex(semver_regexp.pattern)
148
+
141
149
  tag_regexp: re.Pattern[str] = re.compile(rf"{snake_case_regexp.pattern}(?:::{snake_case_regexp.pattern})*")
142
150
  tag_element: pp.ParserElement = pp.Combine(snake_case_element + (colon_token + colon_token + snake_case_element)[...])
143
151
 
@@ -167,6 +175,9 @@ strict_relpath_parser = make_string_parser(strict_relpath_element)
167
175
  strict_abspath_pattern = make_string_pattern(strict_abspath_regexp)
168
176
  strict_abspath_parser = make_string_parser(strict_abspath_element)
169
177
 
178
+ semver_pattern = make_string_pattern(semver_regexp)
179
+ semver_parser = make_string_parser(semver_element)
180
+
170
181
  tag_pattern = make_string_pattern(tag_regexp)
171
182
  tag_parser = make_string_parser(tag_element)
172
183
  topic_pattern = make_string_pattern(topic_regexp)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python-common
3
- Version: 1.0.8
3
+ Version: 1.0.10
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.11
6
6
  Classifier: Programming Language :: Python :: 3.12
@@ -8,6 +8,7 @@ from iker.common.utils.randutils import randomizer
8
8
  from sqlmodel import Field
9
9
 
10
10
  from plexus.common.utils.ormutils import make_base_model, make_snapshot_model_mixin, make_snapshot_model_trigger
11
+ from plexus.common.utils.ormutils import snapshot_model_mixin
11
12
 
12
13
  fixture_postgresql_test_proc = pytest_postgresql.factories.postgresql_proc(host="localhost", user="postgres")
13
14
  fixture_postgresql_test = pytest_postgresql.factories.postgresql("fixture_postgresql_test_proc", dbname="test")
@@ -23,6 +24,11 @@ class DummyModel(make_base_model(), make_snapshot_model_mixin(), table=True):
23
24
  dummy_array: list[str] = Field(sa_column=sa.Column(sa_pg.ARRAY(sa_pg.VARCHAR(64))))
24
25
  dummy_json: JsonType = Field(sa_column=sa.Column(sa_pg.JSONB))
25
26
 
27
+ __table_args__ = (
28
+ snapshot_model_mixin.make_index_created_at_expired_at("ix_dummy_model_created_at_expired_at"),
29
+ snapshot_model_mixin.make_active_unique_index_record_sid("aux_dummy_model_record_sid"),
30
+ )
31
+
26
32
 
27
33
  def test_make_snapshot_model_trigger(fixture_postgresql_test_proc, fixture_postgresql_test):
28
34
  scheme = make_scheme(Dialects.postgresql, Drivers.psycopg)
@@ -9,6 +9,7 @@ from plexus.common.utils.strutils import dot_case_parser, dot_case_pattern
9
9
  from plexus.common.utils.strutils import hex_string_parser, hex_string_pattern
10
10
  from plexus.common.utils.strutils import kebab_case_parser, kebab_case_pattern
11
11
  from plexus.common.utils.strutils import parse_bag_name, parse_user_name, parse_vehicle_name
12
+ from plexus.common.utils.strutils import semver_parser, semver_pattern
12
13
  from plexus.common.utils.strutils import snake_case_parser, snake_case_pattern
13
14
  from plexus.common.utils.strutils import strict_abspath_parser, strict_abspath_pattern
14
15
  from plexus.common.utils.strutils import strict_relpath_parser, strict_relpath_pattern
@@ -376,6 +377,44 @@ class StrUtilsTest(unittest.TestCase):
376
377
  with self.assertRaises(pp.ParseException):
377
378
  strict_abspath_parser.parse_string(data, parse_all=True)
378
379
 
380
+ data_semver_pattern = [
381
+ ("0.0.0",),
382
+ ("1.2.3",),
383
+ ("10.20.30",),
384
+ ("0.1.0-alpha",),
385
+ ("1.0.0+build.1",),
386
+ ("1.0.0-alpha+build.1",),
387
+ ("1.0.0-0.3.7",),
388
+ ("1.0.0-x.7.z.92",),
389
+ ]
390
+
391
+ @ddt.idata(data_semver_pattern)
392
+ @ddt.unpack
393
+ def test_semver_pattern(self, data):
394
+ self.assertIsNotNone(semver_pattern.match(data))
395
+ self.assertIsNotNone(semver_parser.parse_string(data, parse_all=True))
396
+
397
+ data_semver_pattern__bad_cases = [
398
+ ("",),
399
+ ("dummy",),
400
+ ("1",),
401
+ ("1.0",),
402
+ ("01.0.0",),
403
+ ("0.01.0",),
404
+ ("0.0.01",),
405
+ ("1.0.0-",),
406
+ ("1.0.0+",),
407
+ ("1.0.0-alpha+",),
408
+ ("1.0.0-alpha+beta-",),
409
+ ]
410
+
411
+ @ddt.idata(data_semver_pattern__bad_cases)
412
+ @ddt.unpack
413
+ def test_semver_pattern__bad_cases(self, data):
414
+ self.assertIsNone(semver_pattern.match(data))
415
+ with self.assertRaises(pp.ParseException):
416
+ semver_parser.parse_string(data, parse_all=True)
417
+
379
418
  data_tag_pattern = [
380
419
  ("dummy",),
381
420
  ("dummy::dummy",),