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.
- plexus/pulse/demeter/__init__.py +6 -0
- plexus/pulse/demeter/app/__init__.py +0 -0
- plexus/pulse/demeter/app/api/__init__.py +44 -0
- plexus/pulse/demeter/app/api/archive_internal_endpoints.py +316 -0
- plexus/pulse/demeter/app/api/archive_public_endpoints.py +255 -0
- plexus/pulse/demeter/app/api/dataset_internal_endpoints.py +1043 -0
- plexus/pulse/demeter/app/api/dataset_public_endpoints.py +2161 -0
- plexus/pulse/demeter/app/api/dataset_workflow_endpoints.py +532 -0
- plexus/pulse/demeter/app/api/tagging_endpoints.py +323 -0
- plexus/pulse/demeter/app/api/user_endpoints.py +78 -0
- plexus/pulse/demeter/app/core/__init__.py +0 -0
- plexus/pulse/demeter/app/core/database.py +43 -0
- plexus/pulse/demeter/app/core/vault.py +29 -0
- plexus/pulse/demeter/app/main.py +16 -0
- plexus/pulse/demeter/app/models/__init__.py +33 -0
- plexus/pulse/demeter/app/models/local.py +249 -0
- plexus/pulse/demeter/app/models/registry/__init__.py +112 -0
- plexus/pulse/demeter/app/models/registry/archive.py +529 -0
- plexus/pulse/demeter/app/models/registry/dataset.py +1531 -0
- plexus/pulse/demeter/app/models/registry/tagging.py +478 -0
- plexus/pulse/demeter/app/models/registry/user.py +79 -0
- plexus/pulse/demeter/tools/__init__.py +0 -0
- plexus/pulse/demeter/tools/main.py +306 -0
- plexus/pulse/demeter/utils/__init__.py +0 -0
- plexus/pulse/demeter/utils/config.py +25 -0
- plexus/pulse/demeter/utils/datautils.py +436 -0
- plexus_python_pulse_demeter-1.1.126.dist-info/METADATA +36 -0
- plexus_python_pulse_demeter-1.1.126.dist-info/RECORD +31 -0
- plexus_python_pulse_demeter-1.1.126.dist-info/WHEEL +5 -0
- plexus_python_pulse_demeter-1.1.126.dist-info/entry_points.txt +2 -0
- 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
|