plexus-python-pulse-demeter 1.1.126__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.
Files changed (31) hide show
  1. plexus/pulse/demeter/__init__.py +6 -0
  2. plexus/pulse/demeter/app/__init__.py +0 -0
  3. plexus/pulse/demeter/app/api/__init__.py +44 -0
  4. plexus/pulse/demeter/app/api/archive_internal_endpoints.py +316 -0
  5. plexus/pulse/demeter/app/api/archive_public_endpoints.py +255 -0
  6. plexus/pulse/demeter/app/api/dataset_internal_endpoints.py +1043 -0
  7. plexus/pulse/demeter/app/api/dataset_public_endpoints.py +2161 -0
  8. plexus/pulse/demeter/app/api/dataset_workflow_endpoints.py +532 -0
  9. plexus/pulse/demeter/app/api/tagging_endpoints.py +323 -0
  10. plexus/pulse/demeter/app/api/user_endpoints.py +78 -0
  11. plexus/pulse/demeter/app/core/__init__.py +0 -0
  12. plexus/pulse/demeter/app/core/database.py +43 -0
  13. plexus/pulse/demeter/app/core/vault.py +29 -0
  14. plexus/pulse/demeter/app/main.py +16 -0
  15. plexus/pulse/demeter/app/models/__init__.py +33 -0
  16. plexus/pulse/demeter/app/models/local.py +249 -0
  17. plexus/pulse/demeter/app/models/registry/__init__.py +112 -0
  18. plexus/pulse/demeter/app/models/registry/archive.py +529 -0
  19. plexus/pulse/demeter/app/models/registry/dataset.py +1531 -0
  20. plexus/pulse/demeter/app/models/registry/tagging.py +478 -0
  21. plexus/pulse/demeter/app/models/registry/user.py +79 -0
  22. plexus/pulse/demeter/tools/__init__.py +0 -0
  23. plexus/pulse/demeter/tools/main.py +306 -0
  24. plexus/pulse/demeter/utils/__init__.py +0 -0
  25. plexus/pulse/demeter/utils/config.py +25 -0
  26. plexus/pulse/demeter/utils/datautils.py +436 -0
  27. plexus_python_pulse_demeter-1.1.126.dist-info/METADATA +36 -0
  28. plexus_python_pulse_demeter-1.1.126.dist-info/RECORD +31 -0
  29. plexus_python_pulse_demeter-1.1.126.dist-info/WHEEL +5 -0
  30. plexus_python_pulse_demeter-1.1.126.dist-info/entry_points.txt +2 -0
  31. plexus_python_pulse_demeter-1.1.126.dist-info/top_level.txt +1 -0
@@ -0,0 +1,478 @@
1
+ import datetime
2
+ import typing
3
+ import uuid as py_uuid
4
+ from typing import Self
5
+
6
+ import jsonschema
7
+ import pydantic as pdt
8
+ import sqlalchemy as sa
9
+ import sqlalchemy.dialects.postgresql as sa_pg
10
+ import sqlalchemy.orm as sa_orm
11
+ from plexus.common.utils.dtutils import dt_to_ts_us
12
+ from plexus.common.utils.jsonutils import JsonObject
13
+ from plexus.pulse.common.utils.datautils import (
14
+ validate_colon_tag,
15
+ validate_dt_timezone,
16
+ validate_semver,
17
+ validate_slash_tag,
18
+ validate_snake_case,
19
+ validate_vehicle_name,
20
+ )
21
+ from plexus.pulse.common.utils.jsonutils import json_datetime_encoder
22
+ from plexus.pulse.common.utils.ormutils import ChangingModelMixinProtocol, SequenceModelMixinProtocol
23
+ from plexus.pulse.common.utils.ormutils import (
24
+ changing_model_mixin,
25
+ make_changing_model_mixin,
26
+ make_sequence_model_mixin,
27
+ )
28
+ from sqlmodel import Field, SQLModel
29
+
30
+ from plexus.pulse.demeter.app.models.registry import RegistryBase
31
+
32
+ __all__ = [
33
+ "DataTag",
34
+ "DataTagTarget",
35
+ "DataTagRecord",
36
+ "DataTagClip",
37
+ "DataTagTable",
38
+ "DataTagTargetTable",
39
+ "DataTagRecordTable",
40
+ "DataTagClipTable",
41
+ ]
42
+
43
+
44
+ class DataTag(RegistryBase):
45
+ namespace: str = Field(
46
+ sa_column=sa.Column(sa_pg.VARCHAR(64), nullable=False),
47
+ description="Namespace of the tagset that this tag belongs to",
48
+ schema_extra=dict(examples=["namespace_name"]),
49
+ )
50
+ version: str = Field(
51
+ sa_column=sa.Column(sa_pg.VARCHAR(32), nullable=False),
52
+ description="Version of the tagset that this tag belongs to",
53
+ schema_extra=dict(examples=["1.2.3-alpha+post0"]),
54
+ )
55
+ name: str = Field(
56
+ sa_column=sa.Column(sa_pg.VARCHAR(256), nullable=False),
57
+ description="Name of the tag",
58
+ schema_extra=dict(examples=["tag_name:sub_tag_name"]),
59
+ )
60
+ parent_name: str | None = Field(
61
+ sa_column=sa.Column(sa_pg.VARCHAR(256)),
62
+ default=None,
63
+ description="Name of the parent tag that this tag extends",
64
+ schema_extra=dict(examples=["tag_name:sub_tag_name"]),
65
+ )
66
+ props_schema: JsonObject | None = Field(
67
+ sa_column=sa.Column(sa_pg.JSONB),
68
+ default=None,
69
+ description="JSON schema defining the properties format for this tag",
70
+ schema_extra=dict(
71
+ examples=[
72
+ {
73
+ "type": "object",
74
+ "required": ["weather", "obstacle"],
75
+ "properties": {
76
+ "weather": {"type": "string", "enum": ["sunny", "rainy", "foggy"]},
77
+ "obstacle": {
78
+ "type": "string",
79
+ "enum": ["car", "truck", "bus", "motorcycle", "pedestrian", "bicycle"],
80
+ },
81
+ },
82
+ },
83
+ ]
84
+ ),
85
+ )
86
+
87
+ @pdt.field_validator("namespace", mode="after")
88
+ @classmethod
89
+ def validate_namespace(cls, v: str) -> str:
90
+ validate_snake_case(v)
91
+ return v
92
+
93
+ @pdt.field_validator("version", mode="after")
94
+ @classmethod
95
+ def validate_version(cls, v: str) -> str:
96
+ validate_semver(v)
97
+ return v
98
+
99
+ @pdt.field_validator("name", mode="after")
100
+ @classmethod
101
+ def validate_name(cls, v: str) -> str | None:
102
+ validate_colon_tag(v)
103
+ return v
104
+
105
+ @pdt.field_validator("parent_name", mode="after")
106
+ @classmethod
107
+ def validate_parent_name(cls, v: str | None) -> str | None:
108
+ if v is not None:
109
+ validate_colon_tag(v)
110
+ return v
111
+
112
+ @pdt.field_validator("props_schema", mode="after")
113
+ @classmethod
114
+ def validate_props_schema(cls, v: JsonObject | None) -> JsonObject | None:
115
+ if v is not None:
116
+ try:
117
+ jsonschema.Draft7Validator.check_schema(v)
118
+ except Exception as e:
119
+ raise ValueError("not a valid JSON schema") from e
120
+ return v
121
+
122
+
123
+ class DataTagTarget(RegistryBase):
124
+ uuid: py_uuid.UUID = Field(
125
+ sa_column=sa.Column(sa_pg.UUID(as_uuid=True), nullable=False),
126
+ default_factory=py_uuid.uuid4,
127
+ description="UUID of the tag target",
128
+ schema_extra=dict(examples=["abcd1234-5678-90ab-cdef-1234567890ab"]),
129
+ )
130
+ tagger_name: str = Field(
131
+ sa_column=sa.Column(sa_pg.VARCHAR(128), nullable=False),
132
+ description="Name of the tagger that generates the tag records for the target",
133
+ schema_extra=dict(examples=["tagger_name"]),
134
+ )
135
+ tagger_version: str = Field(
136
+ sa_column=sa.Column(sa_pg.VARCHAR(32), nullable=False),
137
+ description="Version of the tagger that generates the tag records for the target",
138
+ schema_extra=dict(examples=["1.2.3-alpha+post0"]),
139
+ )
140
+ vehicle_name: str = Field(
141
+ sa_column=sa.Column(sa_pg.VARCHAR(64), nullable=False),
142
+ description="Name of the tagger's applicability vehicle",
143
+ schema_extra=dict(examples=["vehicle_name"]),
144
+ )
145
+ begin_dt: datetime.datetime = Field(
146
+ sa_column=sa.Column(sa_pg.TIMESTAMP(timezone=True), nullable=False),
147
+ description="Beginning of the tagger's applicability time range",
148
+ schema_extra=dict(examples=["2024-01-01T00:00:00.000000+00:00"]),
149
+ )
150
+ end_dt: datetime.datetime = Field(
151
+ sa_column=sa.Column(sa_pg.TIMESTAMP(timezone=True), nullable=False),
152
+ description="End of the tagger's applicability time range",
153
+ schema_extra=dict(examples=["2024-12-31T23:59:59.999999+00:00"]),
154
+ )
155
+ props: JsonObject | None = Field(
156
+ sa_column=sa.Column(sa_pg.JSONB),
157
+ default=None,
158
+ description="Properties of the tag record stored as JSON",
159
+ schema_extra=dict(examples=[{"example_prop_key": "example_prop_value"}]),
160
+ )
161
+
162
+ @pdt.field_validator("tagger_name", mode="after")
163
+ @classmethod
164
+ def validate_tagger_name(cls, v: str) -> str:
165
+ validate_slash_tag(v)
166
+ return v
167
+
168
+ @pdt.field_validator("tagger_version", mode="after")
169
+ @classmethod
170
+ def validate_tagger_version(cls, v: str) -> str:
171
+ validate_semver(v)
172
+ return v
173
+
174
+ @pdt.field_validator("vehicle_name", mode="after")
175
+ @classmethod
176
+ def validate_vehicle_name(cls, v: str) -> str:
177
+ validate_vehicle_name(v)
178
+ return v
179
+
180
+ @pdt.field_validator("begin_dt", mode="after")
181
+ @classmethod
182
+ def validate_begin_dt(cls, v: datetime.datetime) -> datetime.datetime:
183
+ validate_dt_timezone(v)
184
+ return v
185
+
186
+ @pdt.field_validator("end_dt", mode="after")
187
+ @classmethod
188
+ def validate_end_dt(cls, v: datetime.datetime) -> datetime.datetime:
189
+ validate_dt_timezone(v)
190
+ return v
191
+
192
+ @pdt.model_validator(mode="after")
193
+ def validate_begin_dt_end_dt(self) -> Self:
194
+ if self.begin_dt > self.end_dt:
195
+ raise ValueError(f"begin_dt '{self.begin_dt}' is greater than end_dt '{self.end_dt}'")
196
+ return self
197
+
198
+ @pdt.field_serializer("begin_dt", mode="plain")
199
+ def serialize_begin_dt(self, v: datetime.datetime) -> str:
200
+ return json_datetime_encoder(v)
201
+
202
+ @pdt.field_serializer("end_dt", mode="plain")
203
+ def serialize_end_dt(self, v: datetime.datetime) -> str:
204
+ return json_datetime_encoder(v)
205
+
206
+
207
+ class DataTagRecord(RegistryBase):
208
+ target_uuid: py_uuid.UUID = Field(
209
+ sa_column=sa.Column(sa_pg.UUID(as_uuid=True), nullable=False),
210
+ description="UUID of the tag target that this record is associated with",
211
+ schema_extra=dict(examples=["abcd1234-5678-90ab-cdef-1234567890ab"]),
212
+ )
213
+ vehicle_name: str = Field(
214
+ sa_column=sa.Column(sa_pg.VARCHAR(64), nullable=False),
215
+ description="Name of the vehicle associated with this tag record",
216
+ schema_extra=dict(examples=["vehicle_name"]),
217
+ )
218
+ begin_dt: datetime.datetime = Field(
219
+ sa_column=sa.Column(sa_pg.TIMESTAMP(timezone=True), nullable=False),
220
+ description="Beginning of the tag record time range",
221
+ schema_extra=dict(examples=["2024-01-01T00:00:00.000000+00:00"]),
222
+ )
223
+ end_dt: datetime.datetime = Field(
224
+ sa_column=sa.Column(sa_pg.TIMESTAMP(timezone=True), nullable=False),
225
+ description="End of the tag record time range",
226
+ schema_extra=dict(examples=["2024-01-01T01:00:00.000000+00:00"]),
227
+ )
228
+ tagset_namespace: str = Field(
229
+ sa_column=sa.Column(sa_pg.VARCHAR(64), nullable=False),
230
+ description="Namespace of the tagset that the tag in this record belongs to",
231
+ schema_extra=dict(examples=["namespace_name"]),
232
+ )
233
+ tagset_version: str = Field(
234
+ sa_column=sa.Column(sa_pg.VARCHAR(32), nullable=False),
235
+ description="Version of the tagset that the tag in this record belongs to",
236
+ schema_extra=dict(examples=["1.2.3-alpha+post0"]),
237
+ )
238
+ tag: str = Field(
239
+ sa_column=sa.Column(sa_pg.VARCHAR(256), nullable=False),
240
+ description="Tag name",
241
+ schema_extra=dict(examples=["tag_name:sub_tag_name"]),
242
+ )
243
+ features: JsonObject | None = Field(
244
+ sa_column=sa.Column(sa_pg.JSONB),
245
+ default=None,
246
+ description="Extracted features associated with the tag record, stored as JSON",
247
+ schema_extra=dict(examples=[{"example_feature_key": "example_feature_value"}]),
248
+ )
249
+ props: JsonObject | None = Field(
250
+ sa_column=sa.Column(sa_pg.JSONB),
251
+ default=None,
252
+ description="Properties of the tag record stored as JSON",
253
+ schema_extra=dict(examples=[{"example_prop_key": "example_prop_value"}]),
254
+ )
255
+
256
+ @pdt.field_validator("vehicle_name", mode="after")
257
+ @classmethod
258
+ def validate_vehicle_name(cls, v: str) -> str:
259
+ validate_vehicle_name(v)
260
+ return v
261
+
262
+ @pdt.field_validator("begin_dt", mode="after")
263
+ @classmethod
264
+ def validate_begin_dt(cls, v: datetime.datetime) -> datetime.datetime:
265
+ validate_dt_timezone(v)
266
+ return v
267
+
268
+ @pdt.field_validator("end_dt", mode="after")
269
+ @classmethod
270
+ def validate_end_dt(cls, v: datetime.datetime) -> datetime.datetime:
271
+ validate_dt_timezone(v)
272
+ return v
273
+
274
+ @pdt.model_validator(mode="after")
275
+ def validate_begin_dt_end_dt(self) -> Self:
276
+ if self.begin_dt > self.end_dt:
277
+ raise ValueError(f"begin_dt '{self.begin_dt}' is greater than end_dt '{self.end_dt}'")
278
+ return self
279
+
280
+ @pdt.field_validator("tag", mode="after")
281
+ @classmethod
282
+ def validate_tag(cls, v: str) -> str:
283
+ validate_colon_tag(v)
284
+ return v
285
+
286
+ @pdt.field_serializer("begin_dt", mode="plain")
287
+ def serialize_begin_dt(self, v: datetime.datetime) -> str:
288
+ return json_datetime_encoder(v)
289
+
290
+ @pdt.field_serializer("end_dt", mode="plain")
291
+ def serialize_end_dt(self, v: datetime.datetime) -> str:
292
+ return json_datetime_encoder(v)
293
+
294
+
295
+ class DataTagClip(RegistryBase):
296
+ source_sqn: int = Field(
297
+ sa_column=sa.Column(sa_pg.BIGINT, nullable=False),
298
+ description="Source sequence number for the tag clip, used for ordering and lineage tracking",
299
+ )
300
+ vehicle_name: str = Field(
301
+ sa_column=sa.Column(sa_pg.VARCHAR(64), nullable=False),
302
+ description="Name of the vehicle associated with this tag record",
303
+ schema_extra=dict(examples=["vehicle_name"]),
304
+ )
305
+ clip_dt: datetime.datetime = Field(
306
+ sa_column=sa.Column(sa_pg.TIMESTAMP(timezone=True), nullable=False),
307
+ description="Timestamp representing the time of the tag clip",
308
+ schema_extra=dict(examples=["2024-01-01T00:00:00.000000+00:00"]),
309
+ )
310
+ clip_duration_us: int = Field(
311
+ sa_column=sa.Column(sa_pg.BIGINT, nullable=False),
312
+ description="Duration of the tag clip in microseconds",
313
+ schema_extra=dict(examples=[1_000_000]),
314
+ )
315
+ begin_offset_us: int = Field(
316
+ sa_column=sa.Column(sa_pg.BIGINT, nullable=False),
317
+ description="Offset in microseconds from the beginning of the tag clip to the beginning of the tag record time range",
318
+ schema_extra=dict(examples=[0]),
319
+ )
320
+ end_offset_us: int = Field(
321
+ sa_column=sa.Column(sa_pg.BIGINT, nullable=False),
322
+ description="Offset in microseconds from the beginning of the tag clip to the end of the tag record time range",
323
+ schema_extra=dict(examples=[1_000_000]),
324
+ )
325
+ tagset_namespace: str = Field(
326
+ sa_column=sa.Column(sa_pg.VARCHAR(64), nullable=False),
327
+ description="Namespace of the tagset that the tag in this record belongs to",
328
+ schema_extra=dict(examples=["namespace_name"]),
329
+ )
330
+ tagset_version: str = Field(
331
+ sa_column=sa.Column(sa_pg.VARCHAR(32), nullable=False),
332
+ description="Version of the tagset that the tag in this record belongs to",
333
+ schema_extra=dict(examples=["1.2.3-alpha+post0"]),
334
+ )
335
+ tag: str = Field(
336
+ sa_column=sa.Column(sa_pg.VARCHAR(256), nullable=False),
337
+ description="Tag name",
338
+ schema_extra=dict(examples=["tag_name:sub_tag_name"]),
339
+ )
340
+
341
+ @pdt.field_validator("vehicle_name", mode="after")
342
+ @classmethod
343
+ def validate_vehicle_name(cls, v: str) -> str:
344
+ validate_vehicle_name(v)
345
+ return v
346
+
347
+ @pdt.field_validator("clip_dt", mode="after")
348
+ @classmethod
349
+ def validate_clip_dt(cls, v: datetime.datetime) -> datetime.datetime:
350
+ validate_dt_timezone(v)
351
+ return v
352
+
353
+ @pdt.field_validator("clip_duration_us", mode="after")
354
+ @classmethod
355
+ def validate_clip_duration_us(cls, v: int) -> int:
356
+ if v <= 0:
357
+ raise ValueError(f"clip_duration_us '{v}' must be a positive integer")
358
+ return v
359
+
360
+ @pdt.field_validator("begin_offset_us", mode="after")
361
+ @classmethod
362
+ def validate_begin_offset_us(cls, v: int) -> int:
363
+ if v < 0:
364
+ raise ValueError(f"begin_offset_us '{v}' must be a non-negative integer")
365
+ return v
366
+
367
+ @pdt.field_validator("end_offset_us", mode="after")
368
+ @classmethod
369
+ def validate_end_offset_us(cls, v: int) -> int:
370
+ if v < 0:
371
+ raise ValueError(f"end_offset_us '{v}' must be a non-negative integer")
372
+ return v
373
+
374
+ @pdt.model_validator(mode="after")
375
+ def validate_clip_dt_clip_duration_us(self) -> Self:
376
+ if dt_to_ts_us(self.clip_dt) % self.clip_duration_us > 0:
377
+ raise ValueError(
378
+ f"clip_dt '{self.clip_dt}' must be aligned with clip_duration_us '{self.clip_duration_us}'")
379
+ return self
380
+
381
+ @pdt.model_validator(mode="after")
382
+ def validate_clip_duration_us_begin_offset_us_end_offset_us(self) -> Self:
383
+ if self.begin_offset_us > self.end_offset_us:
384
+ raise ValueError(
385
+ f"begin_offset_us '{self.begin_offset_us}' is greater than end_offset_us '{self.end_offset_us}'")
386
+ if self.end_offset_us > self.clip_duration_us:
387
+ raise ValueError(
388
+ f"end_offset_us '{self.end_offset_us}' is greater than clip_duration_us '{self.clip_duration_us}'")
389
+ return self
390
+
391
+ @pdt.field_validator("tag", mode="after")
392
+ @classmethod
393
+ def validate_tag(cls, v: str) -> str:
394
+ validate_colon_tag(v)
395
+ return v
396
+
397
+ @pdt.field_serializer("clip_dt", mode="plain")
398
+ def serialize_clip_dt(self, v: datetime.datetime) -> str:
399
+ return json_datetime_encoder(v)
400
+
401
+
402
+ class DataTagTable(DataTag, make_changing_model_mixin(), table=True):
403
+ __tablename__ = "data_tag"
404
+ __table_args__ = (
405
+ changing_model_mixin.make_index_created_at("ix_data_tag_created_at"),
406
+ sa.Index("ix_data_tag_namespace_version_name", "namespace", "version", "name"),
407
+ )
408
+
409
+
410
+ class DataTagTargetTable(DataTagTarget, make_changing_model_mixin(), table=True):
411
+ __tablename__ = "data_tag_target"
412
+ __table_args__ = (
413
+ changing_model_mixin.make_index_created_at("ix_data_tag_target_created_at"),
414
+ sa.Index("ix_data_tag_target_uuid", "uuid", unique=True),
415
+ sa.Index("ix_data_tag_target_tagger_name_tagger_version", "tagger_name", "tagger_version"),
416
+ sa.Index("ix_data_tag_target_vehicle_name_begin_dt_end_dt", "vehicle_name", "begin_dt", "end_dt"),
417
+ )
418
+
419
+
420
+ class DataTagRecordTable(DataTagRecord, make_changing_model_mixin(), table=True):
421
+ __tablename__ = "data_tag_record"
422
+ __table_args__ = (
423
+ changing_model_mixin.make_index_created_at("ix_data_tag_record_created_at"),
424
+ sa.Index("ix_data_tag_record_vehicle_name_begin_dt_end_dt", "vehicle_name", "begin_dt", "end_dt"),
425
+ )
426
+
427
+
428
+ class DataTagClipTable(DataTagClip, make_sequence_model_mixin(), table=True):
429
+ __tablename__ = "data_tag_clip"
430
+ __table_args__ = (
431
+ sa.Index("ix_data_tag_clip_source_sqn", "source_sqn"),
432
+ sa.Index("ix_data_tag_clip_vehicle_name_clip_dt", "vehicle_name", "clip_dt"),
433
+ )
434
+
435
+
436
+ if typing.TYPE_CHECKING:
437
+ RegistryBase = SQLModel
438
+
439
+
440
+ class DataTagTable(RegistryBase, ChangingModelMixinProtocol):
441
+ namespace: sa_orm.Mapped[str] = ...
442
+ version: sa_orm.Mapped[str] = ...
443
+ name: sa_orm.Mapped[str] = ...
444
+ parent_name: sa_orm.Mapped[str | None] = ...
445
+ props_schema: sa_orm.Mapped[JsonObject | None] = ...
446
+
447
+
448
+ class DataTagTargetTable(RegistryBase, ChangingModelMixinProtocol):
449
+ uuid: sa_orm.Mapped[py_uuid.UUID] = ...
450
+ tagger_name: sa_orm.Mapped[str] = ...
451
+ tagger_version: sa_orm.Mapped[str] = ...
452
+ vehicle_name: sa_orm.Mapped[str] = ...
453
+ begin_dt: sa_orm.Mapped[datetime.datetime] = ...
454
+ end_dt: sa_orm.Mapped[datetime.datetime] = ...
455
+
456
+
457
+ class DataTagRecordTable(RegistryBase, ChangingModelMixinProtocol):
458
+ target_uuid: sa_orm.Mapped[py_uuid.UUID] = ...
459
+ vehicle_name: sa_orm.Mapped[str] = ...
460
+ begin_dt: sa_orm.Mapped[datetime.datetime] = ...
461
+ end_dt: sa_orm.Mapped[datetime.datetime] = ...
462
+ tagset_namespace: sa_orm.Mapped[str] = ...
463
+ tagset_version: sa_orm.Mapped[str] = ...
464
+ tag: sa_orm.Mapped[str] = ...
465
+ features: sa_orm.Mapped[JsonObject | None] = ...
466
+ props: sa_orm.Mapped[JsonObject | None] = ...
467
+
468
+
469
+ class DataTagClipTable(RegistryBase, SequenceModelMixinProtocol):
470
+ source_sqn: sa_orm.Mapped[int] = ...
471
+ vehicle_name: sa_orm.Mapped[str] = ...
472
+ clip_dt: sa_orm.Mapped[datetime.datetime] = ...
473
+ clip_duration_us: sa_orm.Mapped[int] = ...
474
+ begin_offset_us: sa_orm.Mapped[int] = ...
475
+ end_offset_us: sa_orm.Mapped[int] = ...
476
+ tagset_namespace: sa_orm.Mapped[str] = ...
477
+ tagset_version: sa_orm.Mapped[str] = ...
478
+ tag: sa_orm.Mapped[str] = ...
@@ -0,0 +1,79 @@
1
+ import enum
2
+ import typing
3
+
4
+ import pydantic as pdt
5
+ import sqlalchemy as sa
6
+ import sqlalchemy.dialects.postgresql as sa_pg
7
+ import sqlalchemy.orm as sa_orm
8
+ from plexus.pulse.common.utils.datautils import validate_user_name
9
+ from plexus.pulse.common.utils.ormutils import RevisionModelMixinProtocol
10
+ from plexus.pulse.common.utils.ormutils import make_revision_model_mixin, revision_model_mixin
11
+ from sqlmodel import Field, SQLModel
12
+
13
+ from plexus.pulse.demeter.app.models.registry import RegistryBase
14
+
15
+ __all__ = [
16
+ "UserRole",
17
+ "User",
18
+ "UserTable",
19
+ ]
20
+
21
+
22
+ class UserRole(enum.Enum):
23
+ Admin = "admin"
24
+ Contributor = "contributor"
25
+ Reviewer = "reviewer"
26
+ Viewer = "viewer"
27
+
28
+
29
+ class User(RegistryBase):
30
+ name: str = Field(
31
+ sa_column=sa.Column(sa_pg.VARCHAR(64), nullable=False),
32
+ description="Name of the user",
33
+ schema_extra=dict(examples=["user.name"]),
34
+ )
35
+ sso_sid: int | None = Field(
36
+ sa_column=sa.Column(sa_pg.BIGINT),
37
+ default=None,
38
+ description="SSO SID of the user",
39
+ schema_extra=dict(examples=[1234567890]),
40
+ )
41
+ role: str = Field(
42
+ sa_column=sa.Column(sa_pg.VARCHAR(32), nullable=False),
43
+ description="Role of the account",
44
+ schema_extra=dict(examples=["contributor"]),
45
+ )
46
+
47
+ @pdt.field_validator("name", mode="after")
48
+ @classmethod
49
+ def validate_name(cls, v: str) -> str:
50
+ validate_user_name(v)
51
+ return v
52
+
53
+ @pdt.field_validator("role", mode="after")
54
+ @classmethod
55
+ def validate_role(cls, v: str) -> str:
56
+ if not any(v == item.value for item in UserRole):
57
+ raise ValueError(f"invalid user role '{v}'")
58
+ return v
59
+
60
+
61
+ class UserTable(User, make_revision_model_mixin(), table=True):
62
+ __tablename__ = "user"
63
+ __table_args__ = (
64
+ revision_model_mixin.make_index_created_at_updated_at_expired_at("ix_user_created_at_updated_at_expired_at"),
65
+ revision_model_mixin.make_unique_index_record_sqn_revision("ix_u_user_record_sqn_revision"),
66
+ revision_model_mixin.make_active_unique_index_record_sqn("ix_ua_user_record_sqn"),
67
+ revision_model_mixin.make_active_unique_index_for("ix_ua_user_name", "name"),
68
+ revision_model_mixin.make_active_unique_index_for("ix_ua_user_sso_sid", "sso_sid"),
69
+ )
70
+
71
+
72
+ if typing.TYPE_CHECKING:
73
+ RegistryBase = SQLModel
74
+
75
+
76
+ class UserTable(RegistryBase, RevisionModelMixinProtocol):
77
+ name: sa_orm.Mapped[str] = ...
78
+ sso_sid: sa_orm.Mapped[int | None] = ...
79
+ role: sa_orm.Mapped[str] = ...
File without changes