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,323 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import datetime
|
|
3
|
+
import tempfile
|
|
4
|
+
import uuid as py_uuid
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
import pydantic as pdt
|
|
8
|
+
import pyparsing as pp
|
|
9
|
+
import sqlalchemy as sa
|
|
10
|
+
import sqlalchemy.orm as sa_orm
|
|
11
|
+
from fastapi import APIRouter, Depends, File, UploadFile
|
|
12
|
+
from fastapi.params import Query
|
|
13
|
+
from plexus.common.utils.dtutils import dt_utc_now, td_to_us
|
|
14
|
+
from plexus.common.utils.funcutils import singleton
|
|
15
|
+
from plexus.pulse.common.utils.apiutils import managed_db_session
|
|
16
|
+
from plexus.pulse.common.utils.ormutils import (
|
|
17
|
+
db_create_changing_model,
|
|
18
|
+
db_create_changing_models,
|
|
19
|
+
db_create_sequence_models,
|
|
20
|
+
)
|
|
21
|
+
from plexus.pulse.common.utils.tagutils import TagCache
|
|
22
|
+
from plexus.pulse.common.utils.tagutils import populate_clip_ranges, predefined_tagsets, standard_clip_duration_us
|
|
23
|
+
from plexus.pulse.common.utils.tagutils import versioned_identifier
|
|
24
|
+
|
|
25
|
+
from plexus.pulse.demeter.app.core.database import make_session
|
|
26
|
+
from plexus.pulse.demeter.app.models import Pagination
|
|
27
|
+
from plexus.pulse.demeter.app.models.registry.tagging import (
|
|
28
|
+
DataTagClip,
|
|
29
|
+
DataTagClipTable,
|
|
30
|
+
DataTagRecord,
|
|
31
|
+
DataTagRecordTable,
|
|
32
|
+
DataTagTarget,
|
|
33
|
+
DataTagTargetTable,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
router = APIRouter(prefix="/pulse_demeter/tagging", tags=["tagging"])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@singleton
|
|
40
|
+
def tag_query_interpreter() -> Callable[[str], sa.ColumnElement[bool]]:
|
|
41
|
+
# AST node classes
|
|
42
|
+
@dataclasses.dataclass(frozen=True)
|
|
43
|
+
class TagQueryExpr:
|
|
44
|
+
tag_name: str
|
|
45
|
+
features_query: str | None = None
|
|
46
|
+
|
|
47
|
+
@dataclasses.dataclass(frozen=True)
|
|
48
|
+
class NotExpr:
|
|
49
|
+
operand: "Expr"
|
|
50
|
+
|
|
51
|
+
@dataclasses.dataclass(frozen=True)
|
|
52
|
+
class AndExpr:
|
|
53
|
+
left: "Expr"
|
|
54
|
+
right: "Expr"
|
|
55
|
+
|
|
56
|
+
@dataclasses.dataclass(frozen=True)
|
|
57
|
+
class OrExpr:
|
|
58
|
+
left: "Expr"
|
|
59
|
+
right: "Expr"
|
|
60
|
+
|
|
61
|
+
Expr = TagQueryExpr | NotExpr | AndExpr | OrExpr
|
|
62
|
+
|
|
63
|
+
# Parse actions
|
|
64
|
+
def make_tag_query_expr(tokens):
|
|
65
|
+
if len(tokens) == 1:
|
|
66
|
+
tag_name, = tokens
|
|
67
|
+
return TagQueryExpr(tag_name=tag_name, features_query=None)
|
|
68
|
+
elif len(tokens) == 2:
|
|
69
|
+
tag_name, features_query = tokens
|
|
70
|
+
return TagQueryExpr(tag_name=tag_name, features_query=features_query)
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError(f"invalid tag query expression: {tokens}")
|
|
73
|
+
|
|
74
|
+
def make_not(tokens):
|
|
75
|
+
if len(tokens) == 1:
|
|
76
|
+
operand, *_ = tokens
|
|
77
|
+
return NotExpr(operand=operand)
|
|
78
|
+
raise ValueError(f"invalid NOT expression: {tokens}")
|
|
79
|
+
|
|
80
|
+
def make_and(tokens):
|
|
81
|
+
left, *operands = tokens[0][::2]
|
|
82
|
+
if len(operands) == 0:
|
|
83
|
+
raise ValueError(f"invalid AND expression: {tokens}")
|
|
84
|
+
for right in operands:
|
|
85
|
+
left = AndExpr(left=left, right=right)
|
|
86
|
+
return left
|
|
87
|
+
|
|
88
|
+
def make_or(tokens):
|
|
89
|
+
left, *operands = tokens[0][::2]
|
|
90
|
+
if len(operands) == 0:
|
|
91
|
+
raise ValueError(f"invalid OR expression: {tokens}")
|
|
92
|
+
for right in operands:
|
|
93
|
+
left = OrExpr(left=left, right=right)
|
|
94
|
+
return left
|
|
95
|
+
|
|
96
|
+
# Grammar
|
|
97
|
+
ident_token = pp.Regex(r"[a-zA-Z0-9]+(_[a-zA-Z0-9]+)*")
|
|
98
|
+
colon_token = pp.Literal(":")
|
|
99
|
+
|
|
100
|
+
tag_name_expr = (ident_token + (pp.Suppress(colon_token) + ident_token)[...])
|
|
101
|
+
tag_name_expr.set_parse_action(lambda tokens: ":".join(tokens))
|
|
102
|
+
|
|
103
|
+
features_query_expr = pp.nested_expr("{", "}", )
|
|
104
|
+
features_query_expr.set_parse_action(lambda tokens: "".join(map(str, tokens[0])))
|
|
105
|
+
|
|
106
|
+
tag_query_expr = (tag_name_expr("tag_name") + pp.Optional(features_query_expr("features_query")))
|
|
107
|
+
tag_query_expr.set_parse_action(make_tag_query_expr)
|
|
108
|
+
|
|
109
|
+
expr = pp.Forward()
|
|
110
|
+
expr <<= pp.infix_notation(
|
|
111
|
+
tag_query_expr,
|
|
112
|
+
[
|
|
113
|
+
(pp.Keyword("not"), 1, pp.OpAssoc.RIGHT, make_not),
|
|
114
|
+
(pp.Keyword("and"), 2, pp.OpAssoc.LEFT, make_and),
|
|
115
|
+
(pp.Keyword("or"), 2, pp.OpAssoc.LEFT, make_or),
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def ast_to_sa_filter(ast: Expr) -> sa.ColumnElement[bool]:
|
|
120
|
+
"""
|
|
121
|
+
Converts the AST into an SQLAlchemy filter.
|
|
122
|
+
"""
|
|
123
|
+
if isinstance(ast, TagQueryExpr):
|
|
124
|
+
if ast.features_query is None:
|
|
125
|
+
return DataTagRecordTable.tag == ast.tag_name
|
|
126
|
+
return sa.and_(
|
|
127
|
+
DataTagRecordTable.tag == ast.tag_name,
|
|
128
|
+
sa.text(f"jsonb_path_exists({DataTagRecordTable.__table__.c.features.name}, '{ast.features_query}')"),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if isinstance(ast, NotExpr):
|
|
132
|
+
return sa.not_(ast_to_sa_filter(ast.operand))
|
|
133
|
+
|
|
134
|
+
if isinstance(ast, AndExpr):
|
|
135
|
+
left_filter = ast_to_sa_filter(ast.left)
|
|
136
|
+
right_filter = ast_to_sa_filter(ast.right)
|
|
137
|
+
return sa.and_(left_filter, right_filter)
|
|
138
|
+
|
|
139
|
+
if isinstance(ast, OrExpr):
|
|
140
|
+
left_filter = ast_to_sa_filter(ast.left)
|
|
141
|
+
right_filter = ast_to_sa_filter(ast.right)
|
|
142
|
+
return sa.or_(left_filter, right_filter)
|
|
143
|
+
|
|
144
|
+
raise ValueError(f"unknown AST node type: {type(ast)}")
|
|
145
|
+
|
|
146
|
+
def converter(query: str) -> sa.ColumnElement[bool]:
|
|
147
|
+
ast, *_ = expr.parse_string(query, parse_all=True)
|
|
148
|
+
return ast_to_sa_filter(ast)
|
|
149
|
+
|
|
150
|
+
return converter
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TagTargetsAndPagination(pdt.BaseModel):
|
|
154
|
+
targets: list[DataTagTargetTable]
|
|
155
|
+
pagination: Pagination
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@router.get("/targets", response_model=TagTargetsAndPagination)
|
|
159
|
+
def list_targets(
|
|
160
|
+
tagger_name: str | None = None,
|
|
161
|
+
tagger_version: str | None = None,
|
|
162
|
+
vehicle_name: str | None = None,
|
|
163
|
+
begin_dt: datetime.datetime | None = None,
|
|
164
|
+
end_dt: datetime.datetime | None = None,
|
|
165
|
+
skip: int = Query(0),
|
|
166
|
+
limit: int = Query(1000),
|
|
167
|
+
db: sa_orm.Session = Depends(make_session),
|
|
168
|
+
) -> TagTargetsAndPagination:
|
|
169
|
+
with managed_db_session(db):
|
|
170
|
+
query_stmt = db.query(DataTagTargetTable)
|
|
171
|
+
if begin_dt is not None:
|
|
172
|
+
query_stmt = query_stmt.filter(DataTagTargetTable.end_dt >= begin_dt)
|
|
173
|
+
if end_dt is not None:
|
|
174
|
+
query_stmt = query_stmt.filter(DataTagTargetTable.begin_dt <= end_dt)
|
|
175
|
+
if tagger_name is not None:
|
|
176
|
+
query_stmt = query_stmt.filter(DataTagTargetTable.tagger_name == tagger_name)
|
|
177
|
+
if tagger_version is not None:
|
|
178
|
+
query_stmt = query_stmt.filter(DataTagTargetTable.tagger_version == tagger_version)
|
|
179
|
+
if vehicle_name is not None:
|
|
180
|
+
query_stmt = query_stmt.filter(DataTagTargetTable.vehicle_name == vehicle_name)
|
|
181
|
+
|
|
182
|
+
query_stmt = query_stmt.order_by(DataTagTargetTable.sqn.desc())
|
|
183
|
+
|
|
184
|
+
total = query_stmt.count()
|
|
185
|
+
if total == 0:
|
|
186
|
+
return TagTargetsAndPagination(targets=[], pagination=Pagination(total=0, skip=skip, limit=limit))
|
|
187
|
+
|
|
188
|
+
db_targets = query_stmt.offset(skip).limit(limit).all()
|
|
189
|
+
|
|
190
|
+
return TagTargetsAndPagination(targets=db_targets, pagination=Pagination(total=total, skip=skip, limit=limit))
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class TagRecordsAndPagination(pdt.BaseModel):
|
|
194
|
+
records: list[DataTagRecordTable]
|
|
195
|
+
pagination: Pagination
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@router.get("/records", response_model=TagRecordsAndPagination)
|
|
199
|
+
def list_records(
|
|
200
|
+
begin_dt: datetime.datetime | None = None,
|
|
201
|
+
end_dt: datetime.datetime | None = None,
|
|
202
|
+
tagset_namespace: str | None = None,
|
|
203
|
+
tagset_version: str | None = None,
|
|
204
|
+
query: str | None = Query(None, description="Tag query string (e.g. 'tag1 and tag2 {features_query} or not tag3')"),
|
|
205
|
+
tagger_name: str | None = Query(None),
|
|
206
|
+
tagger_version: str | None = Query(None),
|
|
207
|
+
vehicle_name: str | None = Query(None),
|
|
208
|
+
skip: int = Query(0),
|
|
209
|
+
limit: int = Query(1000),
|
|
210
|
+
db: sa_orm.Session = Depends(make_session),
|
|
211
|
+
) -> TagRecordsAndPagination:
|
|
212
|
+
with managed_db_session(db):
|
|
213
|
+
query_stmt = db.query(DataTagRecordTable)
|
|
214
|
+
if begin_dt is not None:
|
|
215
|
+
query_stmt = query_stmt.filter(DataTagRecordTable.end_dt >= begin_dt)
|
|
216
|
+
if end_dt is not None:
|
|
217
|
+
query_stmt = query_stmt.filter(DataTagRecordTable.begin_dt <= end_dt)
|
|
218
|
+
if tagset_namespace is not None:
|
|
219
|
+
query_stmt = query_stmt.filter(DataTagRecordTable.tagset_namespace == tagset_namespace)
|
|
220
|
+
if tagset_version is not None:
|
|
221
|
+
query_stmt = query_stmt.filter(DataTagRecordTable.tagset_version == tagset_version)
|
|
222
|
+
if query is not None:
|
|
223
|
+
try:
|
|
224
|
+
interpreter = tag_query_interpreter()
|
|
225
|
+
query_filter = interpreter(query)
|
|
226
|
+
query_stmt = query_stmt.filter(query_filter)
|
|
227
|
+
except pp.ParseException as e:
|
|
228
|
+
raise ValueError(f"failed to parse tag query: {e}")
|
|
229
|
+
if tagger_name is not None or tagger_version is not None:
|
|
230
|
+
query_stmt = query_stmt.join(DataTagTargetTable, DataTagRecordTable.target_uuid == DataTagTargetTable.uuid)
|
|
231
|
+
if tagger_name is not None:
|
|
232
|
+
query_stmt = query_stmt.filter(DataTagTargetTable.tagger_name == tagger_name)
|
|
233
|
+
if tagger_version is not None:
|
|
234
|
+
query_stmt = query_stmt.filter(DataTagTargetTable.tagger_version == tagger_version)
|
|
235
|
+
if vehicle_name is not None:
|
|
236
|
+
query_stmt = query_stmt.filter(DataTagRecordTable.vehicle_name == vehicle_name)
|
|
237
|
+
|
|
238
|
+
query_stmt = query_stmt.order_by(DataTagRecordTable.sqn.desc())
|
|
239
|
+
|
|
240
|
+
total = query_stmt.count()
|
|
241
|
+
if total == 0:
|
|
242
|
+
return TagRecordsAndPagination(records=[], pagination=Pagination(total=0, skip=skip, limit=limit))
|
|
243
|
+
|
|
244
|
+
db_records = query_stmt.offset(skip).limit(limit).all()
|
|
245
|
+
|
|
246
|
+
return TagRecordsAndPagination(records=db_records, pagination=Pagination(total=total, skip=skip, limit=limit))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class TagCacheSummary(pdt.BaseModel):
|
|
250
|
+
targets_count: int
|
|
251
|
+
records_count: int
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@router.post("/upload_tag_cache/", response_model=TagCacheSummary)
|
|
255
|
+
def upload_tag_cache(
|
|
256
|
+
tag_cache_file: UploadFile = File(...),
|
|
257
|
+
created_at: datetime.datetime = Query(default_factory=dt_utc_now),
|
|
258
|
+
db: sa_orm.Session = Depends(make_session),
|
|
259
|
+
) -> TagCacheSummary:
|
|
260
|
+
with managed_db_session(db), tempfile.NamedTemporaryFile() as temp_db_file:
|
|
261
|
+
temp_db_file.write(tag_cache_file.file.read())
|
|
262
|
+
temp_db_file.flush()
|
|
263
|
+
|
|
264
|
+
tag_cache = TagCache(file_path=temp_db_file.name)
|
|
265
|
+
|
|
266
|
+
for record in tag_cache.iter_records():
|
|
267
|
+
versioned_namespace = versioned_identifier(record.tagset_namespace, record.tagset_version)
|
|
268
|
+
if versioned_namespace not in predefined_tagsets():
|
|
269
|
+
raise ValueError(f"Tagset '{versioned_namespace}' is not predefined")
|
|
270
|
+
|
|
271
|
+
targets_count = 0
|
|
272
|
+
records_count = 0
|
|
273
|
+
|
|
274
|
+
for target in tag_cache.query_targets():
|
|
275
|
+
tag_target = DataTagTarget(
|
|
276
|
+
uuid=py_uuid.uuid4(),
|
|
277
|
+
tagger_name=target.tagger_name,
|
|
278
|
+
tagger_version=target.tagger_version,
|
|
279
|
+
vehicle_name=target.vehicle_name,
|
|
280
|
+
begin_dt=target.begin_dt,
|
|
281
|
+
end_dt=target.end_dt,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
tag_records = [
|
|
285
|
+
DataTagRecord(
|
|
286
|
+
target_uuid=tag_target.uuid,
|
|
287
|
+
vehicle_name=tag_target.vehicle_name,
|
|
288
|
+
begin_dt=record.begin_dt,
|
|
289
|
+
end_dt=record.end_dt,
|
|
290
|
+
tagset_namespace=record.tagset_namespace,
|
|
291
|
+
tagset_version=record.tagset_version or predefined_tagsets().get(record.tagset_namespace).version,
|
|
292
|
+
tag=record.tag,
|
|
293
|
+
features=record.features,
|
|
294
|
+
props=record.props,
|
|
295
|
+
)
|
|
296
|
+
for record in tag_cache.with_target(target.identifier).iter_records()
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
targets_count += 1
|
|
300
|
+
records_count += len(tag_records)
|
|
301
|
+
|
|
302
|
+
db_create_changing_model(db, DataTagTargetTable, tag_target, created_at=created_at)
|
|
303
|
+
db_tag_records = db_create_changing_models(db, DataTagRecordTable, tag_records, created_at=created_at)
|
|
304
|
+
|
|
305
|
+
tag_clips = [
|
|
306
|
+
DataTagClip(
|
|
307
|
+
source_sqn=db_tag_record.sqn,
|
|
308
|
+
vehicle_name=db_tag_record.vehicle_name,
|
|
309
|
+
clip_dt=clip_dt,
|
|
310
|
+
clip_duration_us=standard_clip_duration_us(),
|
|
311
|
+
begin_offset_us=td_to_us(begin_dt - clip_dt),
|
|
312
|
+
end_offset_us=td_to_us(end_dt - clip_dt),
|
|
313
|
+
tagset_namespace=db_tag_record.tagset_namespace,
|
|
314
|
+
tagset_version=db_tag_record.tagset_version,
|
|
315
|
+
tag=db_tag_record.tag,
|
|
316
|
+
)
|
|
317
|
+
for db_tag_record in db_tag_records
|
|
318
|
+
for clip_dt, _, begin_dt, end_dt in populate_clip_ranges(db_tag_record.begin_dt, db_tag_record.end_dt)
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
db_create_sequence_models(db, DataTagClipTable, tag_clips)
|
|
322
|
+
|
|
323
|
+
return TagCacheSummary(targets_count=targets_count, records_count=records_count)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
import sqlalchemy.orm as sa_orm
|
|
4
|
+
from fastapi import APIRouter, Depends
|
|
5
|
+
from fastapi.params import Query
|
|
6
|
+
from plexus.common.utils.dtutils import dt_utc_now
|
|
7
|
+
from plexus.pulse.common.utils.apiutils import managed_db_session
|
|
8
|
+
from plexus.pulse.common.utils.ormutils import (
|
|
9
|
+
db_create_revision_model,
|
|
10
|
+
db_create_revision_models,
|
|
11
|
+
db_expire_revision_model,
|
|
12
|
+
db_read_active_revision_model_of_record,
|
|
13
|
+
db_read_active_revision_models,
|
|
14
|
+
db_update_revision_model,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from plexus.pulse.demeter.app.core.database import make_session
|
|
18
|
+
from plexus.pulse.demeter.app.models.registry.user import User
|
|
19
|
+
from plexus.pulse.demeter.app.models.registry.user import UserTable
|
|
20
|
+
|
|
21
|
+
router = APIRouter(prefix="/pulse_demeter/user", tags=["user"])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.post("/user/", response_model=UserTable)
|
|
25
|
+
def create_user(
|
|
26
|
+
user: User,
|
|
27
|
+
created_at: datetime.datetime = Query(default_factory=dt_utc_now),
|
|
28
|
+
db: sa_orm.Session = Depends(make_session),
|
|
29
|
+
) -> UserTable:
|
|
30
|
+
with managed_db_session(db):
|
|
31
|
+
return db_create_revision_model(db, UserTable, user, created_at)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.post("/users/", response_model=list[UserTable])
|
|
35
|
+
def create_users(
|
|
36
|
+
users: list[User],
|
|
37
|
+
created_at: datetime.datetime = Query(default_factory=dt_utc_now),
|
|
38
|
+
db: sa_orm.Session = Depends(make_session),
|
|
39
|
+
) -> list[UserTable]:
|
|
40
|
+
with managed_db_session(db):
|
|
41
|
+
return db_create_revision_models(db, UserTable, users, created_at)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@router.get("/user/{record_sqn}", response_model=UserTable)
|
|
45
|
+
def read_user(record_sqn: int, db: sa_orm.Session = Depends(make_session)) -> UserTable:
|
|
46
|
+
with managed_db_session(db):
|
|
47
|
+
return db_read_active_revision_model_of_record(db, UserTable, record_sqn)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.get("/users/", response_model=list[UserTable])
|
|
51
|
+
def read_users(
|
|
52
|
+
skip: int = Query(0),
|
|
53
|
+
limit: int = Query(1000),
|
|
54
|
+
db: sa_orm.Session = Depends(make_session),
|
|
55
|
+
) -> list[UserTable]:
|
|
56
|
+
with managed_db_session(db):
|
|
57
|
+
return db_read_active_revision_models(db, UserTable, skip, limit)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@router.put("/user/{record_sqn}", response_model=UserTable)
|
|
61
|
+
def update_user(
|
|
62
|
+
record_sqn: int,
|
|
63
|
+
user: User,
|
|
64
|
+
updated_at: datetime.datetime = Query(default_factory=dt_utc_now),
|
|
65
|
+
db: sa_orm.Session = Depends(make_session),
|
|
66
|
+
) -> UserTable:
|
|
67
|
+
with managed_db_session(db):
|
|
68
|
+
return db_update_revision_model(db, UserTable, user, record_sqn, updated_at)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@router.delete("/user/{record_sqn}", response_model=UserTable)
|
|
72
|
+
def delete_user(
|
|
73
|
+
record_sqn: int,
|
|
74
|
+
updated_at: datetime.datetime = Query(default_factory=dt_utc_now),
|
|
75
|
+
db: sa_orm.Session = Depends(make_session),
|
|
76
|
+
) -> UserTable:
|
|
77
|
+
with managed_db_session(db):
|
|
78
|
+
return db_expire_revision_model(db, UserTable, record_sqn, updated_at)
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
|
|
3
|
+
import sqlalchemy as sa
|
|
4
|
+
import sqlalchemy.orm as sa_orm
|
|
5
|
+
from pydantic_settings import BaseSettings
|
|
6
|
+
|
|
7
|
+
from plexus.pulse.demeter.utils.config import config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Settings(BaseSettings):
|
|
11
|
+
host: str = config().get("pulse.apps.database", "host", "localhost")
|
|
12
|
+
port: int = config().getint("pulse.apps.database", "port", 5432)
|
|
13
|
+
username: str = config().get("pulse.apps.database", "username", "pulse")
|
|
14
|
+
password: str = config().get("pulse.apps.database", "password", "pulse")
|
|
15
|
+
database: str = config().get("pulse.apps.database", "database", "pulse-apps")
|
|
16
|
+
|
|
17
|
+
class Config:
|
|
18
|
+
env_prefix = "PULSE_APPS_DATABASE_"
|
|
19
|
+
env_file = ".env"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
settings = Settings()
|
|
23
|
+
|
|
24
|
+
url = sa.URL.create(
|
|
25
|
+
drivername="postgresql+psycopg",
|
|
26
|
+
host=settings.host,
|
|
27
|
+
port=settings.port,
|
|
28
|
+
username=settings.username,
|
|
29
|
+
password=settings.password,
|
|
30
|
+
database=settings.database,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
engine = sa.create_engine(url, echo=True, future=True)
|
|
34
|
+
session_maker = sa_orm.sessionmaker(autocommit=False, autoflush=False, bind=engine, future=True, expire_on_commit=False)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def make_session() -> Generator[sa_orm.Session, None, None]:
|
|
38
|
+
db = session_maker()
|
|
39
|
+
db.execute(sa.sql.text("SET TIMEZONE TO 'UTC'"))
|
|
40
|
+
try:
|
|
41
|
+
yield db
|
|
42
|
+
finally:
|
|
43
|
+
db.close()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from plexus.pulse.common.utils.s3utils import s3_make_client, s3_make_progressed_client
|
|
2
|
+
from pydantic_settings import BaseSettings
|
|
3
|
+
|
|
4
|
+
from plexus.pulse.demeter.utils.config import config
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Settings(BaseSettings):
|
|
8
|
+
access_key_id: str = config().get("pulse.apps.vault", "access_key_id", "")
|
|
9
|
+
secret_access_key: str = config().get("pulse.apps.vault", "secret_access_key", "")
|
|
10
|
+
endpoint_url: str = config().get("pulse.apps.vault", "endpoint_url", "http://localhost:9000")
|
|
11
|
+
|
|
12
|
+
class Config:
|
|
13
|
+
env_prefix = "PULSE_APPS_VAULT_"
|
|
14
|
+
env_file = ".env"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
settings = Settings()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def make_client():
|
|
21
|
+
return s3_make_client(access_key_id=settings.access_key_id,
|
|
22
|
+
secret_access_key=settings.secret_access_key,
|
|
23
|
+
endpoint_url=settings.endpoint_url)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def make_progressed_client():
|
|
27
|
+
return s3_make_progressed_client(access_key_id=settings.access_key_id,
|
|
28
|
+
secret_access_key=settings.secret_access_key,
|
|
29
|
+
endpoint_url=settings.endpoint_url)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from fastapi import Response
|
|
2
|
+
from fastapi import status
|
|
3
|
+
|
|
4
|
+
from plexus.pulse.demeter.app.api import make_api
|
|
5
|
+
|
|
6
|
+
app = make_api()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@app.get("/health")
|
|
10
|
+
async def check_health():
|
|
11
|
+
return Response(status_code=status.HTTP_200_OK)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.get("/live")
|
|
15
|
+
async def check_liveness():
|
|
16
|
+
return Response(status_code=status.HTTP_200_OK)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
import pydantic as pdt
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"Pagination",
|
|
7
|
+
"TableColumn",
|
|
8
|
+
"ColumnOrdering",
|
|
9
|
+
"ColumnFormatter",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Pagination(pdt.BaseModel):
|
|
14
|
+
skip: int
|
|
15
|
+
limit: int
|
|
16
|
+
total: int
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ColumnOrdering(pdt.BaseModel):
|
|
20
|
+
semantic: Literal["lexical", "numeric", "datetime", "semver"] = "lexical"
|
|
21
|
+
direction: Literal["ascending", "descending"] = "ascending"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ColumnFormatter(pdt.BaseModel):
|
|
25
|
+
syntax: Literal["python_print", "python_format", "python_fstring", "python_strftime"] = "python_print"
|
|
26
|
+
template: str
|
|
27
|
+
args: list[Any] | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TableColumn(pdt.BaseModel):
|
|
31
|
+
name: str
|
|
32
|
+
ordering: ColumnOrdering | None = None
|
|
33
|
+
formatter: ColumnFormatter | None = None
|