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,2161 @@
1
+ import dataclasses
2
+ import datetime
3
+ import uuid as py_uuid
4
+ from collections import Counter
5
+ from collections.abc import Callable
6
+ from typing import Any, Literal
7
+
8
+ import pydantic as pdt
9
+ import sqlalchemy as sa
10
+ import sqlalchemy.exc as sa_exc
11
+ import sqlalchemy.orm as sa_orm
12
+ from fastapi import APIRouter, Depends
13
+ from fastapi.params import Body, Path, Query
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_revision_model,
18
+ db_create_revision_models,
19
+ db_make_order_by_clause,
20
+ db_read_active_revision_models,
21
+ model_name_of,
22
+ )
23
+ from plexus.pulse.common.utils.sqlutils import escape_sql_like
24
+ from plexus.pulse.common.utils.tagutils import predefined_tagsets
25
+
26
+ from plexus.pulse.demeter.app.api import current_dt_clock
27
+ from plexus.pulse.demeter.app.core.database import make_session
28
+ from plexus.pulse.demeter.app.models import ColumnFormatter, ColumnOrdering, Pagination, TableColumn
29
+ from plexus.pulse.demeter.app.models.registry import JobStatusFlag
30
+ from plexus.pulse.demeter.app.models.registry.dataset import (
31
+ AnnotationGuidelineTable as GuidelineTable,
32
+ ClassifierTaxonomyTable as TaxonomyTable,
33
+ DatasetPromotionTable as DatasetPromotionTable,
34
+ DatasetRegistrationTable as RegistrationTable,
35
+ ModelArchitectureTable as ArchitectureTable,
36
+ ModelAssessmentPromotionTable as AssessmentPromotionTable,
37
+ ModelAssessmentTable as AssessmentTable,
38
+ ModelBenchmarkTable as BenchmarkTable,
39
+ ModelEvaluationTable as EvaluationTable,
40
+ ModelExperimentPromotionTable as ExperimentPromotionTable,
41
+ ModelExperimentTable as ExperimentTable,
42
+ ModelTrainingTable as TrainingTable,
43
+ RegisteredDatasetLayoutTable as DatasetLayoutTable,
44
+ RegisteredDatasetSampleTable as DatasetSampleTable,
45
+ RegisteredDatasetTable as DatasetTable,
46
+ RegisteredSampleTable as SampleTable,
47
+ SampleAnnotationTable as AnnotationTable,
48
+ SampleClassifierTable as ClassifierTable,
49
+ SampleLayoutTable as LayoutTable,
50
+ )
51
+ from plexus.pulse.demeter.app.models.registry.dataset import (
52
+ RegisteredDataset as Dataset,
53
+ RegisteredSample as Sample,
54
+ SampleAnnotation as Annotation,
55
+ SampleClassifier as Classifier,
56
+ )
57
+
58
+ router = APIRouter(prefix="/pulse_demeter/dataset_public", tags=["dataset_public"])
59
+
60
+
61
+ # Composite model naming convention
62
+ # - Use 'And' to join one-to-one relationships. For a chain of one-to-one relations, create a single composite model
63
+ # that covers the whole chain.
64
+ # - Use 'With' to indicate one-to-many relationships.
65
+ # - Use plural form for the target segment of a one-to-many relationship.
66
+ # - If the target is a one-to-one relationship, pluralize the entire relationship segment.
67
+ # - If the target is also one-to-many, pluralize the source segment of that nested relationship.
68
+ # - For multiple chained one-to-many relationships, pluralize each relevant segment.
69
+ # - Preserve the order of segments following the hierarchy.
70
+ # Examples:
71
+ # - A one-to-one B one-to-one C
72
+ # AAndBAndC(a: A, b: B, c: C)
73
+ # - A one-to-many B one-to-one C
74
+ # BAndC(b: B, c: C)
75
+ # AWithBAndCs(a: A, b_and_cs: list[BAndC]) # pluralize the entire one-to-one segment
76
+ # - A one-to-one B one-to-many C
77
+ # BWithCs(b: B, cs: list[C])
78
+ # AAndBWithCs(a: A, b_with_cs: BWithCs)
79
+ # - A one-to-many B one-to-many C
80
+ # BWithCs(b: B, cs: list[C])
81
+ # AWithBsWithCs(a: A, bs_with_cs: list[BWithCs]) # pluralize source segments
82
+ # - A one-to-many B one-to-many C one-to-many D
83
+ # CWithDs(c: C, ds: list[D])
84
+ # BWithCsWithDs(b: B, cs_with_ds: list[CWithDs])
85
+ # AWithBsWithCsWithDs(a: A, bs_with_cs_with_ds: list[BWithCsWithDs])
86
+
87
+
88
+ @router.get("/layouts", response_model=list[LayoutTable])
89
+ def layouts(db: sa_orm.Session = Depends(make_session)) -> list[LayoutTable]:
90
+ with managed_db_session(db):
91
+ return db_read_active_revision_models(db, LayoutTable)
92
+
93
+
94
+ @router.get("/layout", response_model=LayoutTable)
95
+ def get_layout(
96
+ uuid: py_uuid.UUID | None = Query(None),
97
+ name: str | None = Query(None),
98
+ db: sa_orm.Session = Depends(make_session),
99
+ ) -> LayoutTable:
100
+ if uuid is None and name is None:
101
+ raise sa_exc.ArgumentError("either 'uuid' or 'name' must be provided")
102
+ with managed_db_session(db):
103
+ query_stmt = db.query(LayoutTable).where(LayoutTable.expired_at.is_(None))
104
+ if uuid is not None:
105
+ query_stmt = query_stmt.where(LayoutTable.uuid == uuid)
106
+ if name is not None:
107
+ query_stmt = query_stmt.where(LayoutTable.name == name)
108
+ db_layout: LayoutTable | None = query_stmt.one_or_none()
109
+ if db_layout is None:
110
+ raise sa_exc.NoResultFound(
111
+ f"'{model_name_of(LayoutTable)}' with uuid '{uuid}' and/or name '{name}' not found")
112
+ return db_layout
113
+
114
+
115
+ @router.get("/taxonomies", response_model=list[TaxonomyTable])
116
+ def taxonomies(db: sa_orm.Session = Depends(make_session)) -> list[TaxonomyTable]:
117
+ with managed_db_session(db):
118
+ return db_read_active_revision_models(db, TaxonomyTable)
119
+
120
+
121
+ @router.get("/taxonomy", response_model=TaxonomyTable)
122
+ def get_taxonomy(
123
+ uuid: py_uuid.UUID | None = Query(None),
124
+ name: str | None = Query(None),
125
+ db: sa_orm.Session = Depends(make_session),
126
+ ) -> TaxonomyTable:
127
+ if uuid is None and name is None:
128
+ raise sa_exc.ArgumentError("either 'uuid' or 'name' must be provided")
129
+ with managed_db_session(db):
130
+ query_stmt = db.query(TaxonomyTable).where(TaxonomyTable.expired_at.is_(None))
131
+ if uuid is not None:
132
+ query_stmt = query_stmt.where(TaxonomyTable.uuid == uuid)
133
+ if name is not None:
134
+ query_stmt = query_stmt.where(TaxonomyTable.name == name)
135
+ db_taxonomy: TaxonomyTable | None = query_stmt.one_or_none()
136
+ if db_taxonomy is None:
137
+ raise sa_exc.NoResultFound(
138
+ f"'{model_name_of(TaxonomyTable)}' with uuid '{uuid}' and/or name '{name}' not found")
139
+ return db_taxonomy
140
+
141
+
142
+ @router.get("/guidelines", response_model=list[GuidelineTable])
143
+ def guidelines(db: sa_orm.Session = Depends(make_session)) -> list[GuidelineTable]:
144
+ with managed_db_session(db):
145
+ return db_read_active_revision_models(db, GuidelineTable)
146
+
147
+
148
+ @router.get("/guideline", response_model=GuidelineTable)
149
+ def get_guideline(
150
+ uuid: py_uuid.UUID | None = Query(None),
151
+ name: str | None = Query(None),
152
+ db: sa_orm.Session = Depends(make_session),
153
+ ) -> GuidelineTable:
154
+ if uuid is None and name is None:
155
+ raise sa_exc.ArgumentError("either 'uuid' or 'name' must be provided")
156
+ with managed_db_session(db):
157
+ query_stmt = db.query(GuidelineTable).where(GuidelineTable.expired_at.is_(None))
158
+ if uuid is not None:
159
+ query_stmt = query_stmt.where(GuidelineTable.uuid == uuid)
160
+ if name is not None:
161
+ query_stmt = query_stmt.where(GuidelineTable.name == name)
162
+ db_guideline: GuidelineTable | None = query_stmt.one_or_none()
163
+ if db_guideline is None:
164
+ raise sa_exc.NoResultFound(
165
+ f"'{model_name_of(GuidelineTable)}' with uuid '{uuid}' and/or name '{name}' not found")
166
+ return db_guideline
167
+
168
+
169
+ @router.get("/architectures", response_model=list[ArchitectureTable])
170
+ def architectures(db: sa_orm.Session = Depends(make_session)) -> list[ArchitectureTable]:
171
+ with managed_db_session(db):
172
+ return db_read_active_revision_models(db, ArchitectureTable)
173
+
174
+
175
+ @router.get("/architecture", response_model=ArchitectureTable)
176
+ def get_architecture(
177
+ uuid: py_uuid.UUID | None = Query(None),
178
+ name: str | None = Query(None),
179
+ db: sa_orm.Session = Depends(make_session),
180
+ ) -> ArchitectureTable:
181
+ if uuid is None and name is None:
182
+ raise sa_exc.ArgumentError("either 'uuid' or 'name' must be provided")
183
+ with managed_db_session(db):
184
+ query_stmt = db.query(ArchitectureTable).where(ArchitectureTable.expired_at.is_(None))
185
+ if uuid is not None:
186
+ query_stmt = query_stmt.where(ArchitectureTable.uuid == uuid)
187
+ if name is not None:
188
+ query_stmt = query_stmt.where(ArchitectureTable.name == name)
189
+ db_architecture: ArchitectureTable | None = query_stmt.one_or_none()
190
+ if db_architecture is None:
191
+ raise sa_exc.NoResultFound(
192
+ f"'{model_name_of(ArchitectureTable)}' with uuid '{uuid}' and/or name '{name}' not found")
193
+ return db_architecture
194
+
195
+
196
+ @router.get("/benchmarks", response_model=list[BenchmarkTable])
197
+ def benchmarks(db: sa_orm.Session = Depends(make_session)) -> list[BenchmarkTable]:
198
+ with managed_db_session(db):
199
+ return db_read_active_revision_models(db, BenchmarkTable)
200
+
201
+
202
+ @router.get("/benchmark", response_model=BenchmarkTable)
203
+ def get_benchmark(
204
+ uuid: py_uuid.UUID | None = Query(None),
205
+ name: str | None = Query(None),
206
+ db: sa_orm.Session = Depends(make_session),
207
+ ) -> BenchmarkTable:
208
+ if uuid is None and name is None:
209
+ raise sa_exc.ArgumentError("either 'uuid' or 'name' must be provided")
210
+ with managed_db_session(db):
211
+ query_stmt = db.query(BenchmarkTable).where(BenchmarkTable.expired_at.is_(None))
212
+ if uuid is not None:
213
+ query_stmt = query_stmt.where(BenchmarkTable.uuid == uuid)
214
+ if name is not None:
215
+ query_stmt = query_stmt.where(BenchmarkTable.name == name)
216
+ db_benchmark: BenchmarkTable | None = query_stmt.one_or_none()
217
+ if db_benchmark is None:
218
+ raise sa_exc.NoResultFound(
219
+ f"'{model_name_of(BenchmarkTable)}' with uuid '{uuid}' and/or name '{name}' not found")
220
+ return db_benchmark
221
+
222
+
223
+ class LayoutTableAndSamplesCount(pdt.BaseModel):
224
+ layout: LayoutTable
225
+ samples_count: int
226
+
227
+
228
+ class DatasetTableDetail(pdt.BaseModel):
229
+ dataset: DatasetTable
230
+ layouts: list[LayoutTableAndSamplesCount]
231
+ samples_count: int
232
+ registration: RegistrationTable | None
233
+ promotion: DatasetPromotionTable | None
234
+
235
+
236
+ class DatasetTableDetailsAndPagination(pdt.BaseModel):
237
+ dataset_details: list[DatasetTableDetail]
238
+ pagination: Pagination
239
+
240
+
241
+ def query_layout_samples_counts(
242
+ dataset_uuids: list[py_uuid.UUID],
243
+ db: sa_orm.Session,
244
+ ) -> dict[py_uuid.UUID, list[LayoutTableAndSamplesCount]]:
245
+ db_dataset_layouts = (
246
+ db
247
+ .query(DatasetLayoutTable)
248
+ .where(DatasetLayoutTable.dataset_uuid.in_(dataset_uuids))
249
+ .all()
250
+ )
251
+ db_dataset_layouts_dicts = {}
252
+ for db_dataset_layout in db_dataset_layouts: (
253
+ db_dataset_layouts_dicts
254
+ .setdefault(db_dataset_layout.dataset_uuid, {})
255
+ .setdefault(db_dataset_layout.layout_uuid, db_dataset_layout)
256
+ )
257
+
258
+ layout_uuids = list(set(layout_uuid
259
+ for db_dataset_layouts_dict in db_dataset_layouts_dicts.values()
260
+ for layout_uuid in db_dataset_layouts_dict))
261
+ db_layouts = (
262
+ db
263
+ .query(LayoutTable)
264
+ .where(LayoutTable.expired_at.is_(None), LayoutTable.uuid.in_(layout_uuids))
265
+ .all()
266
+ )
267
+ db_layouts_dict = {db_layout.uuid: db_layout for db_layout in db_layouts}
268
+ for layout_uuid in layout_uuids:
269
+ if layout_uuid not in db_layouts_dict:
270
+ raise sa_exc.NoResultFound(f"'{model_name_of(LayoutTable)}' with uuid '{layout_uuid}' not found")
271
+
272
+ # NOTE:
273
+ # - Layouts here are intentionally ordered by `samples_count` in descending order
274
+ # so that the most populated layouts appear first.
275
+ # - Other endpoints may order layouts differently (for example, by `LayoutTable.sqn`)
276
+ # to provide a stable schema-oriented ordering.
277
+ # This difference is intentional and reflects the distinct use-cases.
278
+ return {
279
+ dataset_uuid: [
280
+ LayoutTableAndSamplesCount(layout=db_layouts_dict.get(db_dataset_layout.layout_uuid),
281
+ samples_count=db_dataset_layout.samples_count)
282
+ for db_dataset_layout
283
+ in sorted(db_dataset_layouts_dict.values(), key=lambda x: x.samples_count, reverse=True)
284
+ ]
285
+ for dataset_uuid, db_dataset_layouts_dict in db_dataset_layouts_dicts.items()
286
+ }
287
+
288
+
289
+ @router.get("/list_datasets", response_model=DatasetTableDetailsAndPagination)
290
+ def list_datasets(
291
+ skip: int = Query(0),
292
+ limit: int = Query(100),
293
+ begin_at: datetime.datetime | None = Query(None),
294
+ end_at: datetime.datetime | None = Query(None),
295
+ major_layout: str | None = Query(None),
296
+ name_match: str | None = Query(None),
297
+ contributor_sso_sid: int | None = Query(None),
298
+ only_completed_jobs: bool = Query(True),
299
+ only_shared: bool = Query(True),
300
+ order_by: list[str] | None = Query(None),
301
+ db: sa_orm.Session = Depends(make_session),
302
+ ) -> DatasetTableDetailsAndPagination:
303
+ with managed_db_session(db):
304
+ query_stmt = (
305
+ db
306
+ .query(DatasetTable, RegistrationTable, DatasetPromotionTable)
307
+ .outerjoin(RegistrationTable, DatasetTable.uuid == RegistrationTable.dataset_uuid)
308
+ .outerjoin(DatasetPromotionTable, DatasetTable.uuid == DatasetPromotionTable.dataset_uuid)
309
+ .where(DatasetTable.expired_at.is_(None),
310
+ RegistrationTable.expired_at.is_(None),
311
+ DatasetPromotionTable.expired_at.is_(None))
312
+ )
313
+ if only_completed_jobs:
314
+ query_stmt = query_stmt.where(sa.or_(RegistrationTable.sqn.is_(None),
315
+ RegistrationTable.flags.op("&")(JobStatusFlag.Completed.value) > 0))
316
+ if only_shared:
317
+ query_stmt = query_stmt.where(DatasetPromotionTable.flags.op("&")(JobStatusFlag.Completed.value) > 0)
318
+ if begin_at is not None:
319
+ query_stmt = query_stmt.where(DatasetTable.created_at >= begin_at)
320
+ if end_at is not None:
321
+ query_stmt = query_stmt.where(DatasetTable.created_at < end_at)
322
+ if name_match is not None:
323
+ query_stmt = query_stmt.where(DatasetTable.name.ilike(f"%{escape_sql_like(name_match)}%", escape="\\"))
324
+ if contributor_sso_sid is not None:
325
+ query_stmt = query_stmt.where(DatasetTable.contributor_sso_sid == contributor_sso_sid)
326
+ query_stmt = query_stmt.order_by(*db_make_order_by_clause(DatasetTable, order_by))
327
+
328
+ total = query_stmt.count()
329
+ if total == 0:
330
+ return DatasetTableDetailsAndPagination(
331
+ dataset_details=[],
332
+ pagination=Pagination(skip=skip, limit=limit, total=0),
333
+ )
334
+
335
+ db_dataset_registration_promotions = query_stmt.offset(skip).limit(limit).all()
336
+
337
+ dataset_uuids = list(set(db_dataset.uuid for db_dataset, _, _ in db_dataset_registration_promotions))
338
+ layout_samples_counts_by_datasets = query_layout_samples_counts(dataset_uuids, db)
339
+
340
+ return DatasetTableDetailsAndPagination(
341
+ dataset_details=[
342
+ DatasetTableDetail(
343
+ dataset=db_dataset,
344
+ layouts=layout_samples_counts_by_datasets.get(db_dataset.uuid, []),
345
+ samples_count=sum(layout_samples_count.samples_count
346
+ for layout_samples_count in
347
+ layout_samples_counts_by_datasets.get(db_dataset.uuid, [])),
348
+ registration=db_registration,
349
+ promotion=db_promotion,
350
+ )
351
+ for db_dataset, db_registration, db_promotion in db_dataset_registration_promotions
352
+ ],
353
+ pagination=Pagination(skip=skip, limit=limit, total=total),
354
+ )
355
+
356
+
357
+ @router.get("/get_dataset/{dataset_uuid}", response_model=DatasetTableDetail)
358
+ def get_dataset(
359
+ dataset_uuid: py_uuid.UUID = Path(),
360
+ db: sa_orm.Session = Depends(make_session),
361
+ ) -> DatasetTableDetail:
362
+ with managed_db_session(db):
363
+ db_dataset_registration_promotion = (
364
+ db
365
+ .query(DatasetTable, RegistrationTable, DatasetPromotionTable)
366
+ .outerjoin(RegistrationTable, DatasetTable.uuid == RegistrationTable.dataset_uuid)
367
+ .outerjoin(DatasetPromotionTable, DatasetTable.uuid == DatasetPromotionTable.dataset_uuid)
368
+ .where(DatasetTable.expired_at.is_(None),
369
+ RegistrationTable.expired_at.is_(None),
370
+ DatasetPromotionTable.expired_at.is_(None),
371
+ DatasetTable.uuid == dataset_uuid)
372
+ .one_or_none()
373
+ )
374
+ if db_dataset_registration_promotion is None:
375
+ raise sa_exc.NoResultFound(f"'{model_name_of(DatasetTable)}' with uuid '{dataset_uuid}' not found")
376
+
377
+ db_dataset, db_registration, db_promotion = db_dataset_registration_promotion
378
+
379
+ layout_samples_counts_by_datasets = query_layout_samples_counts([dataset_uuid], db)
380
+ layout_samples_counts = layout_samples_counts_by_datasets.get(dataset_uuid, [])
381
+
382
+ return DatasetTableDetail(
383
+ dataset=db_dataset,
384
+ layouts=layout_samples_counts,
385
+ samples_count=sum(layout_samples_count.samples_count for layout_samples_count in layout_samples_counts),
386
+ registration=db_registration,
387
+ promotion=db_promotion,
388
+ )
389
+
390
+
391
+ class ExperimentTableDetail(pdt.BaseModel):
392
+ experiment: ExperimentTable
393
+ training: TrainingTable | None
394
+ promotion: ExperimentPromotionTable | None
395
+ architecture: ArchitectureTable | None
396
+ dataset: DatasetTable | None
397
+ layouts: list[LayoutTableAndSamplesCount] | None
398
+ samples_count: int
399
+
400
+
401
+ class ExperimentTableDetailsAndPagination(pdt.BaseModel):
402
+ experiment_details: list[ExperimentTableDetail]
403
+ pagination: Pagination
404
+
405
+
406
+ @router.get("/list_model_experiments/", response_model=ExperimentTableDetailsAndPagination)
407
+ def list_model_experiments(
408
+ skip: int = Query(0),
409
+ limit: int = Query(100),
410
+ begin_at: datetime.datetime | None = Query(None),
411
+ end_at: datetime.datetime | None = Query(None),
412
+ architecture: str | None = Query(None),
413
+ name_match: str | None = Query(None),
414
+ contributor_sso_sid: int | None = Query(None),
415
+ only_completed_jobs: bool = Query(True),
416
+ only_shared: bool = Query(True),
417
+ order_by: list[str] | None = Query(None),
418
+ db: sa_orm.Session = Depends(make_session),
419
+ ) -> ExperimentTableDetailsAndPagination:
420
+ with managed_db_session(db):
421
+ if architecture is not None:
422
+ db_architecture = (
423
+ db
424
+ .query(ArchitectureTable)
425
+ .where(ArchitectureTable.expired_at.is_(None), ArchitectureTable.name == architecture)
426
+ .one_or_none()
427
+ )
428
+ if db_architecture is None:
429
+ raise sa_exc.NoResultFound(
430
+ f"'{model_name_of(ArchitectureTable)}' with name '{architecture}' not found")
431
+ architecture_uuid = db_architecture.uuid
432
+ else:
433
+ architecture_uuid = None
434
+
435
+ query_stmt = (
436
+ db
437
+ .query(ExperimentTable, TrainingTable, ExperimentPromotionTable)
438
+ .outerjoin(TrainingTable, ExperimentTable.uuid == TrainingTable.experiment_uuid)
439
+ .outerjoin(ExperimentPromotionTable, ExperimentTable.uuid == ExperimentPromotionTable.experiment_uuid)
440
+ .where(ExperimentTable.expired_at.is_(None),
441
+ TrainingTable.expired_at.is_(None),
442
+ ExperimentPromotionTable.expired_at.is_(None))
443
+ )
444
+ if only_completed_jobs:
445
+ query_stmt = query_stmt.where(sa.or_(TrainingTable.sqn.is_(None),
446
+ TrainingTable.flags.op("&")(JobStatusFlag.Completed.value) > 0))
447
+ if only_shared:
448
+ query_stmt = query_stmt.where(ExperimentPromotionTable.flags.op("&")(JobStatusFlag.Completed.value) > 0)
449
+ if begin_at is not None:
450
+ query_stmt = query_stmt.where(ExperimentTable.created_at >= begin_at)
451
+ if end_at is not None:
452
+ query_stmt = query_stmt.where(ExperimentTable.created_at < end_at)
453
+ if architecture_uuid is not None:
454
+ query_stmt = query_stmt.where(ExperimentTable.architecture_uuid == architecture_uuid)
455
+ if name_match is not None:
456
+ query_stmt = query_stmt.where(ExperimentTable.name.ilike(f"%{escape_sql_like(name_match)}%", escape="\\"))
457
+ if contributor_sso_sid is not None:
458
+ query_stmt = query_stmt.where(ExperimentTable.contributor_sso_sid == contributor_sso_sid)
459
+ query_stmt = query_stmt.order_by(*db_make_order_by_clause(ExperimentTable, order_by))
460
+
461
+ total = query_stmt.count()
462
+ if total == 0:
463
+ return ExperimentTableDetailsAndPagination(
464
+ experiment_details=[],
465
+ pagination=Pagination(skip=skip, limit=limit, total=0),
466
+ )
467
+
468
+ db_experiment_training_promotions = query_stmt.offset(skip).limit(limit).all()
469
+
470
+ architecture_uuids = list(set(db_experiment.architecture_uuid
471
+ for db_experiment, _, _ in db_experiment_training_promotions))
472
+ db_architectures = (
473
+ db
474
+ .query(ArchitectureTable)
475
+ .where(ArchitectureTable.expired_at.is_(None), ArchitectureTable.uuid.in_(architecture_uuids))
476
+ .all()
477
+ )
478
+ db_architectures_dict = {db_architecture.uuid: db_architecture for db_architecture in db_architectures}
479
+
480
+ dataset_uuids = list(set(db_experiment.dataset_uuid
481
+ for db_experiment, _, _ in db_experiment_training_promotions))
482
+ layout_samples_counts_by_datasets = query_layout_samples_counts(dataset_uuids, db)
483
+
484
+ db_datasets = (
485
+ db
486
+ .query(DatasetTable)
487
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid.in_(dataset_uuids))
488
+ .all()
489
+ )
490
+ db_datasets_dict = {db_dataset.uuid: db_dataset for db_dataset in db_datasets}
491
+
492
+ return ExperimentTableDetailsAndPagination(
493
+ experiment_details=[
494
+ ExperimentTableDetail(
495
+ experiment=db_experiment,
496
+ training=db_training,
497
+ promotion=db_promotion,
498
+ architecture=db_architectures_dict.get(db_experiment.architecture_uuid),
499
+ dataset=db_datasets_dict.get(db_experiment.dataset_uuid),
500
+ layouts=layout_samples_counts_by_datasets.get(db_experiment.dataset_uuid, []),
501
+ samples_count=sum(layout_samples_count.samples_count
502
+ for layout_samples_count in
503
+ layout_samples_counts_by_datasets.get(db_experiment.dataset_uuid, [])),
504
+ )
505
+ for db_experiment, db_training, db_promotion in db_experiment_training_promotions
506
+ ],
507
+ pagination=Pagination(skip=skip, limit=limit, total=total),
508
+ )
509
+
510
+
511
+ @router.get("/get_model_experiment/{experiment_uuid}", response_model=ExperimentTableDetail)
512
+ def get_model_experiment(
513
+ experiment_uuid: py_uuid.UUID = Path(),
514
+ db: sa_orm.Session = Depends(make_session),
515
+ ) -> ExperimentTableDetail:
516
+ with managed_db_session(db):
517
+ db_experiment_training_promotion = (
518
+ db
519
+ .query(ExperimentTable, TrainingTable, ExperimentPromotionTable)
520
+ .outerjoin(TrainingTable, ExperimentTable.uuid == TrainingTable.experiment_uuid)
521
+ .outerjoin(ExperimentPromotionTable, ExperimentTable.uuid == ExperimentPromotionTable.experiment_uuid)
522
+ .where(ExperimentTable.expired_at.is_(None),
523
+ TrainingTable.expired_at.is_(None),
524
+ ExperimentPromotionTable.expired_at.is_(None),
525
+ ExperimentTable.uuid == experiment_uuid)
526
+ .one_or_none()
527
+ )
528
+ if db_experiment_training_promotion is None:
529
+ raise sa_exc.NoResultFound(f"'{model_name_of(ExperimentTable)}' with uuid '{experiment_uuid}' not found")
530
+
531
+ db_experiment, db_training, db_promotion = db_experiment_training_promotion
532
+
533
+ db_architecture = (
534
+ db
535
+ .query(ArchitectureTable)
536
+ .where(ArchitectureTable.expired_at.is_(None), ArchitectureTable.uuid == db_experiment.architecture_uuid)
537
+ .one_or_none()
538
+ )
539
+
540
+ layout_samples_counts_by_datasets = query_layout_samples_counts([db_experiment.dataset_uuid], db)
541
+ layout_samples_counts = layout_samples_counts_by_datasets.get(db_experiment.dataset_uuid, [])
542
+
543
+ db_dataset = (
544
+ db
545
+ .query(DatasetTable)
546
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid == db_experiment.dataset_uuid)
547
+ .one_or_none()
548
+ )
549
+
550
+ return ExperimentTableDetail(
551
+ experiment=db_experiment,
552
+ training=db_training,
553
+ promotion=db_promotion,
554
+ architecture=db_architecture,
555
+ dataset=db_dataset,
556
+ layouts=layout_samples_counts,
557
+ samples_count=sum(layout_samples_count.samples_count for layout_samples_count in layout_samples_counts),
558
+ )
559
+
560
+
561
+ class AssessmentTableDetail(pdt.BaseModel):
562
+ assessment: AssessmentTable
563
+ evaluation: EvaluationTable | None
564
+ promotion: AssessmentPromotionTable | None
565
+ benchmark: BenchmarkTable | None
566
+ experiment: ExperimentTable | None
567
+ dataset: DatasetTable | None
568
+ layouts: list[LayoutTableAndSamplesCount] | None
569
+ samples_count: int
570
+ architecture: ArchitectureTable | None
571
+
572
+
573
+ class AssessmentTableDetailsAndPagination(pdt.BaseModel):
574
+ assessment_details: list[AssessmentTableDetail]
575
+ pagination: Pagination
576
+
577
+
578
+ @router.get("/list_model_assessments/", response_model=AssessmentTableDetailsAndPagination)
579
+ def list_model_assessments(
580
+ skip: int = Query(0),
581
+ limit: int = Query(100),
582
+ begin_at: datetime.datetime | None = Query(None),
583
+ end_at: datetime.datetime | None = Query(None),
584
+ benchmark: str | None = Query(None),
585
+ name_match: str | None = Query(None),
586
+ contributor_sso_sid: int | None = Query(None),
587
+ only_completed_jobs: bool = Query(True),
588
+ only_shared: bool = Query(True),
589
+ order_by: list[str] | None = Query(None),
590
+ db: sa_orm.Session = Depends(make_session),
591
+ ) -> AssessmentTableDetailsAndPagination:
592
+ with managed_db_session(db):
593
+ if benchmark is not None:
594
+ db_benchmark = (
595
+ db
596
+ .query(BenchmarkTable)
597
+ .where(BenchmarkTable.expired_at.is_(None), BenchmarkTable.name == benchmark)
598
+ .one_or_none()
599
+ )
600
+ if db_benchmark is None:
601
+ raise sa_exc.NoResultFound(f"'{model_name_of(BenchmarkTable)}' with name '{benchmark}' not found")
602
+ benchmark_uuid = db_benchmark.uuid
603
+ else:
604
+ benchmark_uuid = None
605
+
606
+ query_stmt = (
607
+ db
608
+ .query(AssessmentTable, EvaluationTable, AssessmentPromotionTable)
609
+ .outerjoin(EvaluationTable, AssessmentTable.uuid == EvaluationTable.assessment_uuid)
610
+ .outerjoin(AssessmentPromotionTable, AssessmentTable.uuid == AssessmentPromotionTable.assessment_uuid)
611
+ .where(AssessmentTable.expired_at.is_(None),
612
+ EvaluationTable.expired_at.is_(None),
613
+ AssessmentPromotionTable.expired_at.is_(None))
614
+ )
615
+ if only_completed_jobs:
616
+ query_stmt = query_stmt.where(sa.or_(EvaluationTable.sqn.is_(None),
617
+ EvaluationTable.flags.op("&")(JobStatusFlag.Completed.value) > 0))
618
+ if only_shared:
619
+ query_stmt = query_stmt.where(AssessmentPromotionTable.flags.op("&")(JobStatusFlag.Completed.value) > 0)
620
+ if begin_at is not None:
621
+ query_stmt = query_stmt.where(AssessmentTable.created_at >= begin_at)
622
+ if end_at is not None:
623
+ query_stmt = query_stmt.where(AssessmentTable.created_at < end_at)
624
+ if benchmark_uuid is not None:
625
+ query_stmt = query_stmt.where(AssessmentTable.benchmark_uuid == benchmark_uuid)
626
+ if name_match is not None:
627
+ query_stmt = query_stmt.where(AssessmentTable.name.ilike(f"%{escape_sql_like(name_match)}%", escape="\\"))
628
+ if contributor_sso_sid is not None:
629
+ query_stmt = query_stmt.where(AssessmentTable.contributor_sso_sid == contributor_sso_sid)
630
+ query_stmt = query_stmt.order_by(*db_make_order_by_clause(AssessmentTable, order_by))
631
+
632
+ total = query_stmt.count()
633
+ if total == 0:
634
+ return AssessmentTableDetailsAndPagination(
635
+ assessment_details=[],
636
+ pagination=Pagination(skip=skip, limit=limit, total=0),
637
+ )
638
+
639
+ db_assessment_evaluation_promotions = query_stmt.offset(skip).limit(limit).all()
640
+
641
+ benchmark_uuids = list(set(db_assessment.benchmark_uuid
642
+ for db_assessment, _, _ in db_assessment_evaluation_promotions))
643
+ db_benchmarks = (
644
+ db
645
+ .query(BenchmarkTable)
646
+ .where(BenchmarkTable.expired_at.is_(None), BenchmarkTable.uuid.in_(benchmark_uuids))
647
+ .all()
648
+ )
649
+ db_benchmarks_dict = {db_benchmark.uuid: db_benchmark for db_benchmark in db_benchmarks}
650
+
651
+ experiment_uuids = list(set(db_assessment.experiment_uuid
652
+ for db_assessment, _, _ in db_assessment_evaluation_promotions))
653
+ db_experiments = (
654
+ db
655
+ .query(ExperimentTable)
656
+ .where(ExperimentTable.expired_at.is_(None), ExperimentTable.uuid.in_(experiment_uuids))
657
+ .all()
658
+ )
659
+ db_experiments_dict = {db_experiment.uuid: db_experiment for db_experiment in db_experiments}
660
+
661
+ dataset_uuids = list(set(db_assessment.dataset_uuid
662
+ for db_assessment, _, _ in db_assessment_evaluation_promotions))
663
+ layout_samples_counts_by_datasets = query_layout_samples_counts(dataset_uuids, db)
664
+
665
+ db_datasets = (
666
+ db
667
+ .query(DatasetTable)
668
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid.in_(dataset_uuids))
669
+ .all()
670
+ )
671
+ db_datasets_dict = {db_dataset.uuid: db_dataset for db_dataset in db_datasets}
672
+
673
+ architecture_uuids = list(set(db_experiment.architecture_uuid for db_experiment in db_experiments))
674
+ db_architectures = (
675
+ db
676
+ .query(ArchitectureTable)
677
+ .where(ArchitectureTable.expired_at.is_(None), ArchitectureTable.uuid.in_(architecture_uuids))
678
+ .all()
679
+ )
680
+ db_architectures_dict = {db_architecture.uuid: db_architecture for db_architecture in db_architectures}
681
+
682
+ return AssessmentTableDetailsAndPagination(
683
+ assessment_details=[
684
+ AssessmentTableDetail(
685
+ assessment=db_assessment,
686
+ evaluation=db_evaluation,
687
+ promotion=db_promotion,
688
+ benchmark=db_benchmarks_dict.get(db_assessment.benchmark_uuid),
689
+ experiment=db_experiments_dict.get(db_assessment.experiment_uuid),
690
+ dataset=db_datasets_dict.get(db_assessment.dataset_uuid),
691
+ layouts=layout_samples_counts_by_datasets.get(db_assessment.dataset_uuid, []),
692
+ samples_count=sum(layout_samples_count.samples_count
693
+ for layout_samples_count in
694
+ layout_samples_counts_by_datasets.get(db_assessment.dataset_uuid, [])),
695
+ architecture=(
696
+ db_architectures_dict.get(
697
+ db_experiments_dict.get(db_assessment.experiment_uuid).architecture_uuid)
698
+ if db_assessment.experiment_uuid in db_experiments_dict else None
699
+ ),
700
+ )
701
+ for db_assessment, db_evaluation, db_promotion in db_assessment_evaluation_promotions
702
+ ],
703
+ pagination=Pagination(skip=skip, limit=limit, total=total),
704
+ )
705
+
706
+
707
+ @router.get("/get_model_assessment/{assessment_uuid}", response_model=AssessmentTableDetail)
708
+ def get_model_assessment(
709
+ assessment_uuid: py_uuid.UUID = Path(),
710
+ db: sa_orm.Session = Depends(make_session),
711
+ ) -> AssessmentTableDetail:
712
+ with managed_db_session(db):
713
+ db_assessment_evaluation_promotion = (
714
+ db
715
+ .query(AssessmentTable, EvaluationTable, AssessmentPromotionTable)
716
+ .outerjoin(EvaluationTable, AssessmentTable.uuid == EvaluationTable.assessment_uuid)
717
+ .outerjoin(AssessmentPromotionTable, AssessmentTable.uuid == AssessmentPromotionTable.assessment_uuid)
718
+ .where(AssessmentTable.expired_at.is_(None),
719
+ EvaluationTable.expired_at.is_(None),
720
+ AssessmentPromotionTable.expired_at.is_(None),
721
+ AssessmentTable.uuid == assessment_uuid)
722
+ .one_or_none()
723
+ )
724
+ if db_assessment_evaluation_promotion is None:
725
+ raise sa_exc.NoResultFound(f"'{model_name_of(AssessmentTable)}' with uuid '{assessment_uuid}' not found")
726
+
727
+ db_assessment, db_evaluation, db_promotion = db_assessment_evaluation_promotion
728
+
729
+ db_benchmark = (
730
+ db
731
+ .query(BenchmarkTable)
732
+ .where(BenchmarkTable.expired_at.is_(None), BenchmarkTable.uuid == db_assessment.benchmark_uuid)
733
+ .one_or_none()
734
+ )
735
+
736
+ db_experiment = (
737
+ db
738
+ .query(ExperimentTable)
739
+ .where(ExperimentTable.expired_at.is_(None), ExperimentTable.uuid == db_assessment.experiment_uuid)
740
+ .one_or_none()
741
+ )
742
+
743
+ layout_samples_counts_by_datasets = query_layout_samples_counts([db_assessment.dataset_uuid], db)
744
+ layout_samples_counts = layout_samples_counts_by_datasets.get(db_assessment.dataset_uuid, [])
745
+
746
+ db_dataset = (
747
+ db
748
+ .query(DatasetTable)
749
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid == db_assessment.dataset_uuid)
750
+ .one_or_none()
751
+ )
752
+
753
+ db_architecture = (
754
+ db
755
+ .query(ArchitectureTable)
756
+ .where(ArchitectureTable.expired_at.is_(None), ArchitectureTable.uuid == db_experiment.architecture_uuid)
757
+ .one_or_none()
758
+ ) if db_experiment is not None else None
759
+
760
+ return AssessmentTableDetail(
761
+ assessment=db_assessment,
762
+ evaluation=db_evaluation,
763
+ promotion=db_promotion,
764
+ benchmark=db_benchmark,
765
+ experiment=db_experiment,
766
+ dataset=db_dataset,
767
+ layouts=layout_samples_counts,
768
+ samples_count=sum(layout_samples_count.samples_count for layout_samples_count in layout_samples_counts),
769
+ architecture=db_architecture,
770
+ )
771
+
772
+
773
+ class DatasetWithSamples(pdt.BaseModel):
774
+ dataset: Dataset
775
+ samples: list[Sample] = pdt.Field(default_factory=list)
776
+
777
+
778
+ class DatasetTableWithSampleTables(pdt.BaseModel):
779
+ dataset: DatasetTable
780
+ layouts: list[LayoutTableAndSamplesCount] = pdt.Field(default_factory=list)
781
+ samples: list[SampleTable] = pdt.Field(default_factory=list)
782
+
783
+
784
+ @router.post("/create_dataset_with_samples/", response_model=DatasetTableWithSampleTables)
785
+ def create_dataset_with_samples(
786
+ dataset_with_samples: DatasetWithSamples = Body(),
787
+ current_dt: datetime.datetime = Depends(current_dt_clock),
788
+ db: sa_orm.Session = Depends(make_session),
789
+ ) -> DatasetTableWithSampleTables:
790
+ with managed_db_session(db):
791
+ db_dataset = db_create_revision_model(db, DatasetTable, dataset_with_samples.dataset, current_dt)
792
+
793
+ db_samples = db_create_revision_models(db, SampleTable, dataset_with_samples.samples, current_dt)
794
+ db.add_all([DatasetSampleTable(dataset_uuid=db_dataset.uuid, sample_uuid=db_sample.uuid)
795
+ for db_sample in db_samples])
796
+ db.flush()
797
+
798
+ samples_counts = Counter(db_sample.layout_uuid for db_sample in db_samples)
799
+
800
+ db.add_all([DatasetLayoutTable(dataset_uuid=db_dataset.uuid,
801
+ layout_uuid=layout_uuid,
802
+ samples_count=samples_count)
803
+ for layout_uuid, samples_count in samples_counts.items()])
804
+ db.flush()
805
+
806
+ return get_dataset_with_samples(dataset_uuid=db_dataset.uuid, db=db)
807
+
808
+
809
+ @router.post("/append_dataset_with_samples/{dataset_uuid}", response_model=DatasetTableWithSampleTables)
810
+ def append_dataset_with_samples(
811
+ dataset_uuid: py_uuid.UUID = Path(),
812
+ samples: list[Sample] = Body(),
813
+ current_dt: datetime.datetime = Depends(current_dt_clock),
814
+ db: sa_orm.Session = Depends(make_session),
815
+ ) -> DatasetTableWithSampleTables:
816
+ with managed_db_session(db):
817
+ db_dataset = (
818
+ db
819
+ .query(DatasetTable)
820
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid == dataset_uuid)
821
+ .one_or_none()
822
+ )
823
+ if db_dataset is None:
824
+ raise sa_exc.NoResultFound(f"'{model_name_of(DatasetTable)}' with uuid '{dataset_uuid}' not found")
825
+
826
+ db_samples = db_create_revision_models(db, SampleTable, samples, current_dt)
827
+ db.add_all([DatasetSampleTable(dataset_uuid=db_dataset.uuid, sample_uuid=db_sample.uuid)
828
+ for db_sample in db_samples])
829
+ db.flush()
830
+
831
+ samples_counts = Counter(db_sample.layout_uuid for db_sample in db_samples)
832
+
833
+ db_dataset_layouts = (
834
+ db
835
+ .query(DatasetLayoutTable)
836
+ .where(DatasetLayoutTable.dataset_uuid == db_dataset.uuid,
837
+ DatasetLayoutTable.layout_uuid.in_(list(samples_counts)))
838
+ .all()
839
+ )
840
+ db_dataset_layouts_dict = {db_dataset_layout.layout_uuid: db_dataset_layout
841
+ for db_dataset_layout in db_dataset_layouts}
842
+
843
+ new_db_dataset_layouts = []
844
+ for layout_uuid, samples_count in samples_counts.items():
845
+ db_dataset_layout = db_dataset_layouts_dict.get(layout_uuid)
846
+ if db_dataset_layout is None:
847
+ new_db_dataset_layouts.append(DatasetLayoutTable(dataset_uuid=db_dataset.uuid,
848
+ layout_uuid=layout_uuid,
849
+ samples_count=samples_count))
850
+ else:
851
+ db_dataset_layout.samples_count = db_dataset_layout.samples_count + samples_count
852
+ if new_db_dataset_layouts:
853
+ db.add_all(new_db_dataset_layouts)
854
+ db.flush()
855
+
856
+ return get_dataset_with_samples(dataset_uuid=db_dataset.uuid, db=db)
857
+
858
+
859
+ @router.get("/get_dataset_with_samples/{dataset_uuid}", response_model=DatasetTableWithSampleTables)
860
+ def get_dataset_with_samples(
861
+ dataset_uuid: py_uuid.UUID = Path(),
862
+ db: sa_orm.Session = Depends(make_session),
863
+ ) -> DatasetTableWithSampleTables:
864
+ with managed_db_session(db):
865
+ db_dataset = (
866
+ db
867
+ .query(DatasetTable)
868
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid == dataset_uuid)
869
+ .one_or_none()
870
+ )
871
+ if db_dataset is None:
872
+ raise sa_exc.NoResultFound(f"'{model_name_of(DatasetTable)}' with uuid '{dataset_uuid}' not found")
873
+
874
+ db_layout_dataset_layouts = (
875
+ db
876
+ .query(LayoutTable, DatasetLayoutTable)
877
+ .join(DatasetLayoutTable, LayoutTable.uuid == DatasetLayoutTable.layout_uuid)
878
+ .where(LayoutTable.expired_at.is_(None),
879
+ DatasetLayoutTable.dataset_uuid == db_dataset.uuid)
880
+ .order_by(LayoutTable.sqn)
881
+ .all()
882
+ )
883
+ layout_and_samples_counts = [LayoutTableAndSamplesCount(layout=db_layout,
884
+ samples_count=db_dataset_layout.samples_count)
885
+ for db_layout, db_dataset_layout in db_layout_dataset_layouts]
886
+
887
+ db_samples = (
888
+ db
889
+ .query(SampleTable)
890
+ .join(DatasetSampleTable, SampleTable.uuid == DatasetSampleTable.sample_uuid)
891
+ .where(SampleTable.expired_at.is_(None),
892
+ DatasetSampleTable.dataset_uuid == dataset_uuid)
893
+ .order_by(SampleTable.sqn)
894
+ .all()
895
+ )
896
+
897
+ return DatasetTableWithSampleTables(dataset=db_dataset, layouts=layout_and_samples_counts, samples=db_samples)
898
+
899
+
900
+ class SampleAndAnnotation(pdt.BaseModel):
901
+ sample: Sample
902
+ annotation: Annotation
903
+
904
+
905
+ class DatasetWithSampleAndAnnotations(pdt.BaseModel):
906
+ dataset: Dataset
907
+ sample_and_annotations: list[SampleAndAnnotation] = pdt.Field(default_factory=list)
908
+
909
+
910
+ class SampleTableAndAnnotationTable(pdt.BaseModel):
911
+ sample: SampleTable
912
+ annotation: AnnotationTable
913
+
914
+
915
+ class DatasetTableWithSampleTableAndAnnotationTables(pdt.BaseModel):
916
+ dataset: DatasetTable
917
+ layouts: list[LayoutTableAndSamplesCount] = pdt.Field(default_factory=list)
918
+ sample_and_annotations: list[SampleTableAndAnnotationTable] = pdt.Field(default_factory=list)
919
+
920
+
921
+ @router.post("/create_dataset_with_sample_and_annotations/",
922
+ response_model=DatasetTableWithSampleTableAndAnnotationTables)
923
+ def create_dataset_with_sample_and_annotations(
924
+ dataset_with_sample_and_annotations: DatasetWithSampleAndAnnotations = Body(),
925
+ current_dt: datetime.datetime = Depends(current_dt_clock),
926
+ db: sa_orm.Session = Depends(make_session),
927
+ ) -> DatasetTableWithSampleTableAndAnnotationTables:
928
+ with managed_db_session(db):
929
+ db_dataset = db_create_revision_model(db,
930
+ DatasetTable,
931
+ dataset_with_sample_and_annotations.dataset,
932
+ current_dt)
933
+
934
+ db_samples = db_create_revision_models(
935
+ db,
936
+ SampleTable,
937
+ [sample_and_annotation.sample
938
+ for sample_and_annotation in dataset_with_sample_and_annotations.sample_and_annotations],
939
+ current_dt,
940
+ )
941
+ db_annotations = db_create_revision_models(
942
+ db,
943
+ AnnotationTable,
944
+ [sample_and_annotation.annotation
945
+ for sample_and_annotation in dataset_with_sample_and_annotations.sample_and_annotations],
946
+ current_dt,
947
+ )
948
+ db.add_all([DatasetSampleTable(dataset_uuid=db_dataset.uuid,
949
+ sample_uuid=db_sample.uuid,
950
+ annotation_uuid=db_annotation.uuid)
951
+ for db_sample, db_annotation in zip(db_samples, db_annotations)])
952
+ db.flush()
953
+
954
+ samples_counts = Counter(db_sample.layout_uuid for db_sample in db_samples)
955
+
956
+ db.add_all([DatasetLayoutTable(dataset_uuid=db_dataset.uuid,
957
+ layout_uuid=layout_uuid,
958
+ samples_count=samples_count)
959
+ for layout_uuid, samples_count in samples_counts.items()])
960
+ db.flush()
961
+
962
+ return get_dataset_with_sample_and_annotations(dataset_uuid=db_dataset.uuid, db=db)
963
+
964
+
965
+ @router.post("/append_dataset_with_sample_and_annotations/{dataset_uuid}",
966
+ response_model=DatasetTableWithSampleTableAndAnnotationTables)
967
+ def append_dataset_with_sample_and_annotations(
968
+ dataset_uuid: py_uuid.UUID = Path(),
969
+ sample_and_annotations: list[SampleAndAnnotation] = Body(),
970
+ current_dt: datetime.datetime = Depends(current_dt_clock),
971
+ db: sa_orm.Session = Depends(make_session),
972
+ ) -> DatasetTableWithSampleTableAndAnnotationTables:
973
+ with managed_db_session(db):
974
+ db_dataset = (
975
+ db
976
+ .query(DatasetTable)
977
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid == dataset_uuid)
978
+ .one_or_none()
979
+ )
980
+ if db_dataset is None:
981
+ raise sa_exc.NoResultFound(f"'{model_name_of(DatasetTable)}' with uuid '{dataset_uuid}' not found")
982
+
983
+ db_samples = db_create_revision_models(
984
+ db,
985
+ SampleTable,
986
+ [sample_and_annotation.sample for sample_and_annotation in sample_and_annotations],
987
+ current_dt,
988
+ )
989
+ db_annotations = db_create_revision_models(
990
+ db,
991
+ AnnotationTable,
992
+ [sample_and_annotation.annotation for sample_and_annotation in sample_and_annotations],
993
+ current_dt,
994
+ )
995
+ db.add_all([DatasetSampleTable(dataset_uuid=db_dataset.uuid,
996
+ sample_uuid=db_sample.uuid,
997
+ annotation_uuid=db_annotation.uuid)
998
+ for db_sample, db_annotation in zip(db_samples, db_annotations)])
999
+ db.flush()
1000
+
1001
+ samples_counts = Counter(db_sample.layout_uuid for db_sample in db_samples)
1002
+
1003
+ db_dataset_layouts = (
1004
+ db
1005
+ .query(DatasetLayoutTable)
1006
+ .where(DatasetLayoutTable.dataset_uuid == db_dataset.uuid,
1007
+ DatasetLayoutTable.layout_uuid.in_(list(samples_counts)))
1008
+ .all()
1009
+ )
1010
+ db_dataset_layouts_dict = {db_dataset_layout.layout_uuid: db_dataset_layout
1011
+ for db_dataset_layout in db_dataset_layouts}
1012
+
1013
+ new_db_dataset_layouts = []
1014
+ for layout_uuid, samples_count in samples_counts.items():
1015
+ db_dataset_layout = db_dataset_layouts_dict.get(layout_uuid)
1016
+ if db_dataset_layout is None:
1017
+ new_db_dataset_layouts.append(DatasetLayoutTable(dataset_uuid=db_dataset.uuid,
1018
+ layout_uuid=layout_uuid,
1019
+ samples_count=samples_count))
1020
+ else:
1021
+ db_dataset_layout.samples_count = db_dataset_layout.samples_count + samples_count
1022
+ if new_db_dataset_layouts:
1023
+ db.add_all(new_db_dataset_layouts)
1024
+ db.flush()
1025
+
1026
+ return get_dataset_with_sample_and_annotations(dataset_uuid=db_dataset.uuid, db=db)
1027
+
1028
+
1029
+ @router.get("/get_dataset_with_sample_and_annotations/{dataset_uuid}",
1030
+ response_model=DatasetTableWithSampleTableAndAnnotationTables)
1031
+ def get_dataset_with_sample_and_annotations(
1032
+ dataset_uuid: py_uuid.UUID = Path(),
1033
+ db: sa_orm.Session = Depends(make_session),
1034
+ ) -> DatasetTableWithSampleTableAndAnnotationTables:
1035
+ with managed_db_session(db):
1036
+ db_dataset = (
1037
+ db
1038
+ .query(DatasetTable)
1039
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid == dataset_uuid)
1040
+ .one_or_none()
1041
+ )
1042
+ if db_dataset is None:
1043
+ raise sa_exc.NoResultFound(f"'{model_name_of(DatasetTable)}' with uuid '{dataset_uuid}' not found")
1044
+
1045
+ db_layout_dataset_layouts = (
1046
+ db
1047
+ .query(LayoutTable, DatasetLayoutTable)
1048
+ .join(DatasetLayoutTable, LayoutTable.uuid == DatasetLayoutTable.layout_uuid)
1049
+ .where(LayoutTable.expired_at.is_(None),
1050
+ DatasetLayoutTable.dataset_uuid == db_dataset.uuid)
1051
+ .order_by(LayoutTable.sqn)
1052
+ .all()
1053
+ )
1054
+ layout_and_samples_counts = [LayoutTableAndSamplesCount(layout=db_layout,
1055
+ samples_count=db_dataset_layout.samples_count)
1056
+ for db_layout, db_dataset_layout in db_layout_dataset_layouts]
1057
+
1058
+ db_dataset_sample_sample_annotations = (
1059
+ db
1060
+ .query(DatasetSampleTable, SampleTable, AnnotationTable)
1061
+ .join(SampleTable, SampleTable.uuid == DatasetSampleTable.sample_uuid)
1062
+ .join(AnnotationTable, AnnotationTable.uuid == DatasetSampleTable.annotation_uuid)
1063
+ .where(SampleTable.expired_at.is_(None),
1064
+ AnnotationTable.expired_at.is_(None),
1065
+ DatasetSampleTable.dataset_uuid == dataset_uuid)
1066
+ .order_by(SampleTable.sqn)
1067
+ .all()
1068
+ )
1069
+ db_sample_and_annotations = [SampleTableAndAnnotationTable(sample=db_sample, annotation=db_annotation)
1070
+ for _, db_sample, db_annotation in db_dataset_sample_sample_annotations]
1071
+
1072
+ return DatasetTableWithSampleTableAndAnnotationTables(
1073
+ dataset=db_dataset,
1074
+ layouts=layout_and_samples_counts,
1075
+ sample_and_annotations=db_sample_and_annotations,
1076
+ )
1077
+
1078
+
1079
+ class SampleWithClassifiers(pdt.BaseModel):
1080
+ sample: Sample
1081
+ classifiers: list[Classifier] = pdt.Field(default_factory=list)
1082
+
1083
+
1084
+ class DatasetWithSamplesWithClassifiers(pdt.BaseModel):
1085
+ dataset: Dataset
1086
+ samples_with_classifiers: list[SampleWithClassifiers] = pdt.Field(default_factory=list)
1087
+
1088
+
1089
+ class SampleTableWithClassifierTables(pdt.BaseModel):
1090
+ sample: SampleTable
1091
+ classifiers: list[ClassifierTable] = pdt.Field(default_factory=list)
1092
+
1093
+
1094
+ class DatasetTableWithSampleTablesWithClassifierTables(pdt.BaseModel):
1095
+ dataset: DatasetTable
1096
+ layouts: list[LayoutTableAndSamplesCount] = pdt.Field(default_factory=list)
1097
+ samples_with_classifiers: list[SampleTableWithClassifierTables] = pdt.Field(default_factory=list)
1098
+
1099
+
1100
+ @router.post("/create_dataset_with_samples_with_classifiers",
1101
+ response_model=DatasetTableWithSampleTablesWithClassifierTables)
1102
+ def create_dataset_with_samples_with_classifiers(
1103
+ dataset_with_samples_with_classifiers: DatasetWithSamplesWithClassifiers = Body(),
1104
+ current_dt: datetime.datetime = Depends(current_dt_clock),
1105
+ db: sa_orm.Session = Depends(make_session),
1106
+ ) -> DatasetTableWithSampleTablesWithClassifierTables:
1107
+ with managed_db_session(db):
1108
+ db_dataset = db_create_revision_model(db,
1109
+ DatasetTable,
1110
+ dataset_with_samples_with_classifiers.dataset,
1111
+ current_dt)
1112
+
1113
+ db_samples = db_create_revision_models(
1114
+ db,
1115
+ SampleTable,
1116
+ [sample_with_classifiers.sample
1117
+ for sample_with_classifiers in dataset_with_samples_with_classifiers.samples_with_classifiers],
1118
+ current_dt,
1119
+ )
1120
+ db_classifiers = db_create_revision_models(
1121
+ db,
1122
+ ClassifierTable,
1123
+ [classifier
1124
+ for sample_with_classifiers in dataset_with_samples_with_classifiers.samples_with_classifiers
1125
+ for classifier in sample_with_classifiers.classifiers],
1126
+ current_dt,
1127
+ )
1128
+ db.add_all([DatasetSampleTable(dataset_uuid=db_dataset.uuid, sample_uuid=db_sample.uuid)
1129
+ for db_sample in db_samples])
1130
+ db.flush()
1131
+
1132
+ samples_counts = Counter(db_sample.layout_uuid for db_sample in db_samples)
1133
+
1134
+ db.add_all([DatasetLayoutTable(dataset_uuid=db_dataset.uuid,
1135
+ layout_uuid=layout_uuid,
1136
+ samples_count=samples_count)
1137
+ for layout_uuid, samples_count in samples_counts.items()])
1138
+ db.flush()
1139
+
1140
+ return get_dataset_with_samples_with_classifiers(dataset_uuid=db_dataset.uuid, db=db)
1141
+
1142
+
1143
+ @router.post("/append_dataset_with_samples_with_classifiers/{dataset_uuid}",
1144
+ response_model=DatasetTableWithSampleTablesWithClassifierTables)
1145
+ def append_dataset_with_samples_with_classifiers(
1146
+ dataset_uuid: py_uuid.UUID = Path(),
1147
+ samples_with_classifiers: list[SampleWithClassifiers] = Body(),
1148
+ current_dt: datetime.datetime = Depends(current_dt_clock),
1149
+ db: sa_orm.Session = Depends(make_session),
1150
+ ) -> DatasetTableWithSampleTablesWithClassifierTables:
1151
+ with managed_db_session(db):
1152
+ db_dataset = (
1153
+ db
1154
+ .query(DatasetTable)
1155
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid == dataset_uuid)
1156
+ .one_or_none()
1157
+ )
1158
+ if db_dataset is None:
1159
+ raise sa_exc.NoResultFound(f"'{model_name_of(DatasetTable)}' with uuid '{dataset_uuid}' not found")
1160
+
1161
+ db_samples = db_create_revision_models(
1162
+ db,
1163
+ SampleTable,
1164
+ [sample_with_classifiers.sample for sample_with_classifiers in samples_with_classifiers],
1165
+ current_dt,
1166
+ )
1167
+ db_classifiers = db_create_revision_models(
1168
+ db,
1169
+ ClassifierTable,
1170
+ [classifier
1171
+ for sample_with_classifiers in samples_with_classifiers
1172
+ for classifier in sample_with_classifiers.classifiers],
1173
+ current_dt,
1174
+ )
1175
+ db.add_all([DatasetSampleTable(dataset_uuid=db_dataset.uuid, sample_uuid=db_sample.uuid)
1176
+ for db_sample in db_samples])
1177
+ db.flush()
1178
+
1179
+ samples_counts = Counter(db_sample.layout_uuid for db_sample in db_samples)
1180
+
1181
+ db_dataset_layouts = (
1182
+ db
1183
+ .query(DatasetLayoutTable)
1184
+ .where(DatasetLayoutTable.dataset_uuid == db_dataset.uuid,
1185
+ DatasetLayoutTable.layout_uuid.in_(list(samples_counts)))
1186
+ .all()
1187
+ )
1188
+ db_dataset_layouts_dict = {db_dataset_layout.layout_uuid: db_dataset_layout
1189
+ for db_dataset_layout in db_dataset_layouts}
1190
+
1191
+ new_db_dataset_layouts = []
1192
+ for layout_uuid, samples_count in samples_counts.items():
1193
+ db_dataset_layout = db_dataset_layouts_dict.get(layout_uuid)
1194
+ if db_dataset_layout is None:
1195
+ new_db_dataset_layouts.append(DatasetLayoutTable(dataset_uuid=db_dataset.uuid,
1196
+ layout_uuid=layout_uuid,
1197
+ samples_count=samples_count))
1198
+ else:
1199
+ db_dataset_layout.samples_count = db_dataset_layout.samples_count + samples_count
1200
+ if new_db_dataset_layouts:
1201
+ db.add_all(new_db_dataset_layouts)
1202
+ db.flush()
1203
+
1204
+ return get_dataset_with_samples_with_classifiers(dataset_uuid=db_dataset.uuid, db=db)
1205
+
1206
+
1207
+ @router.get("/get_dataset_with_samples_with_classifiers/{dataset_uuid}",
1208
+ response_model=DatasetTableWithSampleTablesWithClassifierTables)
1209
+ def get_dataset_with_samples_with_classifiers(
1210
+ dataset_uuid: py_uuid.UUID = Path(),
1211
+ db: sa_orm.Session = Depends(make_session),
1212
+ ) -> DatasetTableWithSampleTablesWithClassifierTables:
1213
+ with managed_db_session(db):
1214
+ db_dataset = (
1215
+ db
1216
+ .query(DatasetTable)
1217
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid == dataset_uuid)
1218
+ .one_or_none()
1219
+ )
1220
+ if db_dataset is None:
1221
+ raise sa_exc.NoResultFound(f"'{model_name_of(DatasetTable)}' with uuid '{dataset_uuid}' not found")
1222
+
1223
+ db_layout_dataset_layouts = (
1224
+ db
1225
+ .query(LayoutTable, DatasetLayoutTable)
1226
+ .join(DatasetLayoutTable, LayoutTable.uuid == DatasetLayoutTable.layout_uuid)
1227
+ .where(LayoutTable.expired_at.is_(None),
1228
+ DatasetLayoutTable.dataset_uuid == db_dataset.uuid)
1229
+ .order_by(LayoutTable.sqn)
1230
+ .all()
1231
+ )
1232
+ layout_and_samples_counts = [LayoutTableAndSamplesCount(layout=db_layout,
1233
+ samples_count=db_dataset_layout.samples_count)
1234
+ for db_layout, db_dataset_layout in db_layout_dataset_layouts]
1235
+
1236
+ db_samples = (
1237
+ db
1238
+ .query(SampleTable)
1239
+ .join(DatasetSampleTable, SampleTable.uuid == DatasetSampleTable.sample_uuid)
1240
+ .where(SampleTable.expired_at.is_(None),
1241
+ DatasetSampleTable.dataset_uuid == dataset_uuid)
1242
+ .order_by(SampleTable.sqn)
1243
+ .all()
1244
+ )
1245
+ db_classifiers = (
1246
+ db
1247
+ .query(ClassifierTable)
1248
+ .join(DatasetSampleTable, ClassifierTable.sample_uuid == DatasetSampleTable.sample_uuid)
1249
+ .where(ClassifierTable.expired_at.is_(None),
1250
+ DatasetSampleTable.dataset_uuid == dataset_uuid)
1251
+ .order_by(ClassifierTable.sqn)
1252
+ .all()
1253
+ )
1254
+ db_samples_classifiers_dict = {}
1255
+ for db_classifier in db_classifiers:
1256
+ db_samples_classifiers_dict.setdefault(db_classifier.sample_uuid, []).append(db_classifier)
1257
+ db_samples_with_classifiers = [
1258
+ SampleTableWithClassifierTables(sample=db_sample,
1259
+ classifiers=db_samples_classifiers_dict.get(db_sample.uuid, []))
1260
+ for db_sample in db_samples
1261
+ ]
1262
+
1263
+ return DatasetTableWithSampleTablesWithClassifierTables(
1264
+ dataset=db_dataset,
1265
+ layouts=layout_and_samples_counts,
1266
+ samples_with_classifiers=db_samples_with_classifiers,
1267
+ )
1268
+
1269
+
1270
+ class DatasetConditions(pdt.BaseModel):
1271
+ included_dataset_uuids: list[py_uuid.UUID]
1272
+ excluded_dataset_uuids: list[py_uuid.UUID] = pdt.Field(default_factory=list)
1273
+ required_tags: list[str] = pdt.Field(default_factory=list)
1274
+ unwanted_tags: list[str] = pdt.Field(default_factory=list)
1275
+
1276
+
1277
+ class CreateDatasetFromConditionsRequest(pdt.BaseModel):
1278
+ dataset: Dataset
1279
+ conditions: DatasetConditions
1280
+
1281
+
1282
+ @router.post("/create_dataset_from_conditions/", response_model=DatasetTableDetail)
1283
+ def create_dataset_from_conditions(
1284
+ request: CreateDatasetFromConditionsRequest = Body(),
1285
+ current_dt: datetime.datetime = Depends(current_dt_clock),
1286
+ db: sa_orm.Session = Depends(make_session),
1287
+ ) -> DatasetTableDetail:
1288
+ with managed_db_session(db):
1289
+ db_dataset = db_create_revision_model(db, DatasetTable, request.dataset, current_dt)
1290
+
1291
+ query_stmt = (
1292
+ sa
1293
+ .select(
1294
+ sa.literal(db_dataset.uuid).label("dataset_uuid"),
1295
+ DatasetSampleTable.sample_uuid.label("sample_uuid"),
1296
+ )
1297
+ .select_from(DatasetSampleTable)
1298
+ .where(DatasetSampleTable.dataset_uuid.in_(request.conditions.included_dataset_uuids))
1299
+ )
1300
+ if request.conditions.excluded_dataset_uuids:
1301
+ query_stmt = query_stmt.where(
1302
+ DatasetSampleTable.dataset_uuid.notin_(request.conditions.excluded_dataset_uuids),
1303
+ )
1304
+ if request.conditions.required_tags or request.conditions.unwanted_tags:
1305
+ query_stmt = query_stmt.join(SampleTable, SampleTable.uuid == DatasetSampleTable.sample_uuid)
1306
+ for tag in (request.conditions.required_tags or []):
1307
+ query_stmt = query_stmt.where(SampleTable.expired_at.is_(None), SampleTable.tags.contains([tag]))
1308
+ for tag in (request.conditions.unwanted_tags or []):
1309
+ query_stmt = query_stmt.where(SampleTable.expired_at.is_(None), ~SampleTable.tags.contains([tag]))
1310
+ insert_stmt = (
1311
+ sa
1312
+ .insert(DatasetSampleTable)
1313
+ .from_select(["dataset_uuid", "sample_uuid"], query_stmt)
1314
+ )
1315
+ db.execute(insert_stmt)
1316
+ db.flush()
1317
+
1318
+ query_stmt = (
1319
+ sa
1320
+ .select(
1321
+ sa.literal(db_dataset.uuid).label("dataset_uuid"),
1322
+ SampleTable.layout_uuid.label("layout_uuid"),
1323
+ sa.func.count().label("samples_count"),
1324
+ )
1325
+ .select_from(SampleTable)
1326
+ .join(DatasetSampleTable, SampleTable.uuid == DatasetSampleTable.sample_uuid)
1327
+ .where(SampleTable.expired_at.is_(None),
1328
+ DatasetSampleTable.dataset_uuid == db_dataset.uuid)
1329
+ .group_by(SampleTable.layout_uuid)
1330
+ )
1331
+ insert_stmt = (
1332
+ sa
1333
+ .insert(DatasetLayoutTable)
1334
+ .from_select(["dataset_uuid", "layout_uuid", "samples_count"], query_stmt)
1335
+ )
1336
+ db.execute(insert_stmt)
1337
+ db.flush()
1338
+
1339
+ return get_dataset(dataset_uuid=db_dataset.uuid, db=db)
1340
+
1341
+
1342
+ class ClassifierStatItem(pdt.BaseModel):
1343
+ classifier: str
1344
+ count: int
1345
+
1346
+
1347
+ class DatasetTableClassifierStat(pdt.BaseModel):
1348
+ dataset: DatasetTable
1349
+ items: list[ClassifierStatItem]
1350
+
1351
+
1352
+ class DatasetTablesClassifiersStat(pdt.BaseModel):
1353
+ stats: list[DatasetTableClassifierStat]
1354
+
1355
+
1356
+ @router.get("/stat_datasets_classifiers/", response_model=DatasetTablesClassifiersStat)
1357
+ def stat_datasets_classifiers(
1358
+ dataset_uuids: list[py_uuid.UUID] = Query(),
1359
+ db: sa_orm.Session = Depends(make_session),
1360
+ ) -> DatasetTablesClassifiersStat:
1361
+ with managed_db_session(db):
1362
+ db_datasets = (
1363
+ db
1364
+ .query(DatasetTable)
1365
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid.in_(dataset_uuids))
1366
+ .all()
1367
+ )
1368
+ for dataset_uuid in dataset_uuids:
1369
+ if dataset_uuid not in set(db_dataset.uuid for db_dataset in db_datasets):
1370
+ raise sa_exc.NoResultFound(f"'{model_name_of(DatasetTable)}' with uuid '{dataset_uuid}' not found")
1371
+
1372
+ stats = []
1373
+ for db_dataset in db_datasets:
1374
+ db_classifiers = (
1375
+ db
1376
+ .query(ClassifierTable)
1377
+ .join(DatasetSampleTable, ClassifierTable.sample_uuid == DatasetSampleTable.sample_uuid)
1378
+ .where(ClassifierTable.expired_at.is_(None),
1379
+ DatasetSampleTable.dataset_uuid == db_dataset.uuid)
1380
+ .all()
1381
+ )
1382
+
1383
+ counter = Counter(classifiers.get("name")
1384
+ for db_classifier in db_classifiers
1385
+ for classifiers in db_classifier.classifier_data.get("classifiers", [])
1386
+ if classifiers.get("name") is not None)
1387
+
1388
+ stats.append(
1389
+ DatasetTableClassifierStat(
1390
+ dataset=db_dataset,
1391
+ items=[ClassifierStatItem(classifier=classifier, count=count)
1392
+ for classifier, count in sorted(counter.items())],
1393
+ )
1394
+ )
1395
+
1396
+ return DatasetTablesClassifiersStat(stats=stats)
1397
+
1398
+
1399
+ class ClassifierComparisonMetricItem(pdt.BaseModel):
1400
+ name: str
1401
+ data: float
1402
+
1403
+
1404
+ class ClassifierComparisonMetric(pdt.BaseModel):
1405
+ dataset_uuid: py_uuid.UUID
1406
+ items: list[ClassifierComparisonMetricItem]
1407
+
1408
+
1409
+ class ClassifierComparisonCollection(pdt.BaseModel):
1410
+ name: str
1411
+ metrics: list[ClassifierComparisonMetric]
1412
+
1413
+
1414
+ class DatasetTablesClassifiersComparison(pdt.BaseModel):
1415
+ comparison_view: Literal["default"]
1416
+ datasets: list[DatasetTable]
1417
+ collections: list[ClassifierComparisonCollection]
1418
+
1419
+
1420
+ class CompareDatasetClassifiersRequest(pdt.BaseModel):
1421
+ comparison_view: Literal["default"] = pdt.Field(default="default")
1422
+ dataset_uuids: list[py_uuid.UUID]
1423
+
1424
+
1425
+ @router.post("/compare_dataset_classifiers/", response_model=DatasetTablesClassifiersComparison)
1426
+ def compare_dataset_classifiers(
1427
+ request: CompareDatasetClassifiersRequest = Body(),
1428
+ db: sa_orm.Session = Depends(make_session),
1429
+ ) -> DatasetTablesClassifiersComparison:
1430
+ with managed_db_session(db):
1431
+ db_datasets = (
1432
+ db
1433
+ .query(DatasetTable)
1434
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid.in_(request.dataset_uuids))
1435
+ .all()
1436
+ )
1437
+ for dataset_uuid in request.dataset_uuids:
1438
+ if dataset_uuid not in set(db_dataset.uuid for db_dataset in db_datasets):
1439
+ raise sa_exc.NoResultFound(f"'{model_name_of(DatasetTable)}' with uuid '{dataset_uuid}' not found")
1440
+
1441
+ collections = {}
1442
+
1443
+ classifiers_view_query_stmt = (
1444
+ sa
1445
+ .lateral(sa.func.jsonb_array_elements(ClassifierTable.classifier_data["classifiers"]).table_valued("value"))
1446
+ .alias("classifiers")
1447
+ )
1448
+
1449
+ classifier_query_stmt = (
1450
+ sa
1451
+ .select(DatasetSampleTable.dataset_uuid.label("dataset_uuid"),
1452
+ classifiers_view_query_stmt.c.value.op("->>")("name").label("classifier_name"))
1453
+ .select_from(ClassifierTable)
1454
+ .join(DatasetSampleTable, ClassifierTable.sample_uuid == DatasetSampleTable.sample_uuid)
1455
+ .join(classifiers_view_query_stmt, sa.true())
1456
+ .where(ClassifierTable.expired_at.is_(None),
1457
+ DatasetSampleTable.dataset_uuid.in_(request.dataset_uuids))
1458
+ .subquery()
1459
+ )
1460
+
1461
+ classifier_count_query_stmt = (
1462
+ sa
1463
+ .select(classifier_query_stmt.c.dataset_uuid,
1464
+ classifier_query_stmt.c.classifier_name,
1465
+ sa.func.count().label("count"))
1466
+ .group_by(classifier_query_stmt.c.dataset_uuid, classifier_query_stmt.c.classifier_name)
1467
+ )
1468
+
1469
+ for dataset_uuid, classifier_name, count in db.execute(classifier_count_query_stmt).all():
1470
+ *collection_name_segments, metric_name = classifier_name.split(":")
1471
+ collection_name = ":".join(collection_name_segments)
1472
+ collections.setdefault(collection_name, {}).setdefault(dataset_uuid, {}).setdefault(metric_name, 0)
1473
+ collections[collection_name][dataset_uuid][metric_name] += count
1474
+
1475
+ return DatasetTablesClassifiersComparison(
1476
+ datasets=db_datasets,
1477
+ comparison_view=request.comparison_view,
1478
+ collections=[
1479
+ ClassifierComparisonCollection(
1480
+ name=collection_name,
1481
+ metrics=[
1482
+ ClassifierComparisonMetric(dataset_uuid=dataset_uuid,
1483
+ items=[
1484
+ ClassifierComparisonMetricItem(name=metric_name, data=metric_value)
1485
+ for metric_name, metric_value in sorted(metrics.items())
1486
+ ])
1487
+ for dataset_uuid, metrics in sorted(dataset_values.items())
1488
+ ],
1489
+ )
1490
+ for collection_name, dataset_values in sorted(collections.items())
1491
+ ],
1492
+ )
1493
+
1494
+
1495
+ class CompareDatasetClassifiersWithConditionsRequest(pdt.BaseModel):
1496
+ comparison_view: Literal["default"] = pdt.Field(default="default")
1497
+ dataset_uuids: list[py_uuid.UUID]
1498
+ conditions: DatasetConditions
1499
+
1500
+
1501
+ @router.post("/compare_dataset_classifiers_with_conditions/", response_model=DatasetTablesClassifiersComparison)
1502
+ def compare_dataset_classifiers_with_conditions(
1503
+ request: CompareDatasetClassifiersWithConditionsRequest = Body(),
1504
+ db: sa_orm.Session = Depends(make_session),
1505
+ ) -> DatasetTablesClassifiersComparison:
1506
+ with managed_db_session(db):
1507
+ db_datasets = (
1508
+ db
1509
+ .query(DatasetTable)
1510
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid.in_(request.dataset_uuids))
1511
+ .all()
1512
+ )
1513
+ for dataset_uuid in request.dataset_uuids:
1514
+ if dataset_uuid not in set(db_dataset.uuid for db_dataset in db_datasets):
1515
+ raise sa_exc.NoResultFound(f"'{model_name_of(DatasetTable)}' with uuid '{dataset_uuid}' not found")
1516
+
1517
+ collections = {}
1518
+
1519
+ # The first query counts classifiers only for samples in datasets matching the conditions
1520
+ classifiers_view_query_stmt = (
1521
+ sa
1522
+ .lateral(sa.func.jsonb_array_elements(ClassifierTable.classifier_data["classifiers"]).table_valued("value"))
1523
+ .alias("classifiers")
1524
+ )
1525
+
1526
+ classifier_query_stmt = (
1527
+ sa
1528
+ .select(
1529
+ sa.cast(sa.literal("00000000-0000-0000-0000-000000000000"), sa.UUID).label("dataset_uuid"),
1530
+ classifiers_view_query_stmt.c.value.op("->>")("name").label("classifier_name"),
1531
+ )
1532
+ .select_from(ClassifierTable)
1533
+ .join(DatasetSampleTable, ClassifierTable.sample_uuid == DatasetSampleTable.sample_uuid)
1534
+ .join(classifiers_view_query_stmt, sa.true())
1535
+ .where(ClassifierTable.expired_at.is_(None),
1536
+ DatasetSampleTable.dataset_uuid.in_(request.dataset_uuids))
1537
+ )
1538
+ if request.conditions.included_dataset_uuids:
1539
+ classifier_query_stmt = classifier_query_stmt.where(
1540
+ DatasetSampleTable.dataset_uuid.in_(request.conditions.included_dataset_uuids),
1541
+ )
1542
+ if request.conditions.excluded_dataset_uuids:
1543
+ classifier_query_stmt = classifier_query_stmt.where(
1544
+ DatasetSampleTable.dataset_uuid.notin_(request.conditions.excluded_dataset_uuids),
1545
+ )
1546
+ if request.conditions.required_tags or request.conditions.unwanted_tags:
1547
+ classifier_query_stmt = classifier_query_stmt.join(SampleTable,
1548
+ SampleTable.uuid == DatasetSampleTable.sample_uuid)
1549
+ for tag in (request.conditions.required_tags or []):
1550
+ classifier_query_stmt = classifier_query_stmt.where(SampleTable.expired_at.is_(None),
1551
+ SampleTable.tags.contains([tag]))
1552
+ for tag in (request.conditions.unwanted_tags or []):
1553
+ classifier_query_stmt = classifier_query_stmt.where(SampleTable.expired_at.is_(None),
1554
+ ~SampleTable.tags.contains([tag]))
1555
+ classifier_query_stmt = classifier_query_stmt.subquery()
1556
+
1557
+ classifier_count_query_stmt = (
1558
+ sa
1559
+ .select(
1560
+ classifier_query_stmt.c.dataset_uuid,
1561
+ classifier_query_stmt.c.classifier_name,
1562
+ sa.func.count().label("count"),
1563
+ )
1564
+ .group_by(classifier_query_stmt.c.dataset_uuid, classifier_query_stmt.c.classifier_name)
1565
+ )
1566
+
1567
+ for dataset_uuid, classifier_name, count in db.execute(classifier_count_query_stmt).all():
1568
+ *collection_name_segments, metric_name = classifier_name.split(":")
1569
+ collection_name = ":".join(collection_name_segments)
1570
+ collections.setdefault(collection_name, {}).setdefault(dataset_uuid, {}).setdefault(metric_name, 0)
1571
+ collections[collection_name][dataset_uuid][metric_name] += count
1572
+
1573
+ # The second query counts classifiers for samples in all reference datasets
1574
+ classifier_query_stmt = (
1575
+ sa
1576
+ .select(DatasetSampleTable.dataset_uuid.label("dataset_uuid"),
1577
+ classifiers_view_query_stmt.c.value.op("->>")("name").label("classifier_name"))
1578
+ .select_from(ClassifierTable)
1579
+ .join(DatasetSampleTable, ClassifierTable.sample_uuid == DatasetSampleTable.sample_uuid)
1580
+ .join(classifiers_view_query_stmt, sa.true())
1581
+ .where(ClassifierTable.expired_at.is_(None),
1582
+ DatasetSampleTable.dataset_uuid.in_(request.dataset_uuids))
1583
+ .subquery()
1584
+ )
1585
+
1586
+ classifier_count_query_stmt = (
1587
+ sa
1588
+ .select(
1589
+ classifier_query_stmt.c.dataset_uuid,
1590
+ classifier_query_stmt.c.classifier_name,
1591
+ sa.func.count().label("count"),
1592
+ )
1593
+ .group_by(classifier_query_stmt.c.dataset_uuid, classifier_query_stmt.c.classifier_name)
1594
+ )
1595
+
1596
+ for dataset_uuid, classifier_name, count in db.execute(classifier_count_query_stmt).all():
1597
+ *collection_name_segments, metric_name = classifier_name.split(":")
1598
+ collection_name = ":".join(collection_name_segments)
1599
+ collections.setdefault(collection_name, {}).setdefault(dataset_uuid, {}).setdefault(metric_name, 0)
1600
+ collections[collection_name][dataset_uuid][metric_name] += count
1601
+
1602
+ return DatasetTablesClassifiersComparison(
1603
+ datasets=db_datasets,
1604
+ comparison_view=request.comparison_view,
1605
+ collections=[ClassifierComparisonCollection(
1606
+ name=collection_name,
1607
+ metrics=[ClassifierComparisonMetric(
1608
+ dataset_uuid=dataset_uuid,
1609
+ items=[ClassifierComparisonMetricItem(
1610
+ name=metric_name,
1611
+ data=metric_value,
1612
+ ) for metric_name, metric_value in sorted(metrics.items())],
1613
+ ) for dataset_uuid, metrics in sorted(dataset_values.items())],
1614
+ ) for collection_name, dataset_values in sorted(collections.items())],
1615
+ )
1616
+
1617
+
1618
+ @router.get("/get_model_experiment_lineage/{experiment_uuid}", response_model=list[ExperimentTableDetail])
1619
+ def get_model_experiment_lineage(
1620
+ experiment_uuid: py_uuid.UUID = Path(),
1621
+ db: sa_orm.Session = Depends(make_session),
1622
+ ) -> list[ExperimentTableDetail]:
1623
+ with managed_db_session(db):
1624
+ experiment_lineage: list[ExperimentTableDetail] = []
1625
+ current_experiment_uuid = experiment_uuid
1626
+ while current_experiment_uuid is not None:
1627
+ try:
1628
+ experiment_table_detail = get_model_experiment(current_experiment_uuid, db)
1629
+ except Exception:
1630
+ experiment_table_detail = None
1631
+ if experiment_table_detail is None:
1632
+ if len(experiment_lineage) == 0:
1633
+ raise sa_exc.NoResultFound(
1634
+ f"'{model_name_of(ExperimentTable)}' with uuid '{experiment_uuid}' not found")
1635
+ else:
1636
+ last_experiment_table_detail = experiment_lineage[-1]
1637
+ raise sa_exc.NoResultFound(
1638
+ f"'{model_name_of(ExperimentTable)}' with uuid '{last_experiment_table_detail.experiment.uuid}' has unreachable parent")
1639
+ experiment_lineage.append(experiment_table_detail)
1640
+ current_experiment_uuid = experiment_table_detail.experiment.parent_uuid
1641
+
1642
+ return experiment_lineage
1643
+
1644
+
1645
+ class DetailedMetricPivotHeader(pdt.BaseModel):
1646
+ name: str
1647
+ title_columns: list[TableColumn]
1648
+ metric_columns: list[TableColumn]
1649
+
1650
+
1651
+ class DetailedMetricPivotTable(pdt.BaseModel):
1652
+ name: str
1653
+ title_values_sets: list[list[str]]
1654
+ metric_values_sets: list[list[float]]
1655
+
1656
+
1657
+ @dataclasses.dataclass
1658
+ class MetricsDisplayOperation(object):
1659
+ order_maker: Callable[[type[AssessmentTable]], list[Any]]
1660
+ metric_columns: list[TableColumn]
1661
+ detailed_metric_pivot_headers: list[DetailedMetricPivotHeader]
1662
+ metric_values_maker: Callable[[AssessmentTable, list[TableColumn]], list[float]]
1663
+ detailed_metric_pivot_tables_maker: Callable[
1664
+ [AssessmentTable, list[DetailedMetricPivotHeader]], list[DetailedMetricPivotTable]]
1665
+
1666
+
1667
+ @singleton
1668
+ def registered_metrics_display_operations() -> dict[str, MetricsDisplayOperation]:
1669
+ """
1670
+ Register metrics display operations for different benchmarks.
1671
+
1672
+ Example:
1673
+
1674
+ >>> import sqlalchemy
1675
+ >>> def order_maker(assessment_table: type[AssessmentTable]) -> list[Any]:
1676
+ ... return [sqlalchemy.desc(assessment_table.assessment_data["accuracy"]), assessment_table.created_at.asc()]
1677
+ ...
1678
+ >>> metric_columns = [
1679
+ ... TableColumn(name="accuracy",
1680
+ ... ordering=ColumnOrdering(semantic="numeric", direction="descending"),
1681
+ ... formatter=ColumnFormatter(syntax="python_format", template="{:.2%}")),
1682
+ ... TableColumn(name="error_rate",
1683
+ ... ordering=ColumnOrdering(semantic="numeric", direction="ascending"),
1684
+ ... formatter=ColumnFormatter(syntax="python_format", template="{:.2%}")),
1685
+ ... TableColumn(name="created_at",
1686
+ ... ordering=ColumnOrdering(semantic="datetime", direction="ascending"),
1687
+ ... formatter=ColumnFormatter(syntax="python_strftime", template="%Y-%m-%d %H:%M:%S")),
1688
+ ... ]
1689
+ ...
1690
+ >>> detailed_metric_pivot_headers = [
1691
+ ... DetailedMetricPivotHeader(
1692
+ ... name="per_class_metrics",
1693
+ ... title_columns=[
1694
+ ... TableColumn(name="class",
1695
+ ... ordering=ColumnOrdering(semantic="lexical", direction="ascending"),
1696
+ ... formatter=ColumnFormatter(syntax="python_format", template="{}")),
1697
+ ... ],
1698
+ ... metric_columns=[
1699
+ ... TableColumn(name="accuracy",
1700
+ ... ordering=ColumnOrdering(semantic="numeric", direction="descending"),
1701
+ ... formatter=ColumnFormatter(syntax="python_format", template="{:.2%}")),
1702
+ ... TableColumn(name="error_rate",
1703
+ ... ordering=ColumnOrdering(semantic="numeric", direction="ascending"),
1704
+ ... formatter=ColumnFormatter(syntax="python_format", template="{:.2%}")),
1705
+ ... ],
1706
+ ... ),
1707
+ ... ]
1708
+ ...
1709
+ >>> def metric_values_maker(assessment: AssessmentTable, columns: list[TableColumn]) -> list[float]:
1710
+ ... return [
1711
+ ... assessment.assessment_data.get("accuracy", 0.0),
1712
+ ... assessment.assessment_data.get("error_rate", 0.0),
1713
+ ... assessment.created_at,
1714
+ ... ]
1715
+ ...
1716
+ >>> def detailed_metric_pivot_tables_maker(
1717
+ ... assessment: AssessmentTable,
1718
+ ... headers: list[DetailedMetricPivotHeader],
1719
+ ... ) -> list[DetailedMetricPivotTable]:
1720
+ ... pivot_tables = []
1721
+ ... for header in headers:
1722
+ ... pivot_data = assessment.assessment_data.get(header.name, {})
1723
+ ... title_values_sets = [[class_name] for class_name in sorted(pivot_data.keys())]
1724
+ ... metric_values_sets = [[pivot_data[class_name].get("accuracy", 0.0),
1725
+ ... pivot_data[class_name].get("error_rate", 0.0),
1726
+ ... ]
1727
+ ... for class_name in sorted(pivot_data.keys())]
1728
+ ... # You don't need to validate data dimensions here, as they will be validated by the caller.
1729
+ ... pivot_tables.append(
1730
+ ... DetailedMetricPivotTable(
1731
+ ... name="per_class_metrics",
1732
+ ... title_values_sets=title_values_sets,
1733
+ ... metric_values_sets=metric_values_sets,
1734
+ ... )
1735
+ ... )
1736
+ ... return pivot_tables
1737
+ ...
1738
+ >>> registered_metrics_display_operations()["my/benchmark"] = MetricsDisplayOperation(
1739
+ ... order_maker,
1740
+ ... metric_columns,
1741
+ ... detailed_metric_pivot_headers,
1742
+ ... metric_values_maker,
1743
+ ... detailed_metric_pivot_tables_maker,
1744
+ ... )
1745
+
1746
+ :return: A dictionary mapping benchmark names to their corresponding ``MetricsOperation``.
1747
+ """
1748
+ return {}
1749
+
1750
+
1751
+ class AssessmentTableDetailWithMetricValues(pdt.BaseModel):
1752
+ assessment_detail: AssessmentTableDetail
1753
+ metric_columns: list[TableColumn]
1754
+ metric_values: list[float] = pdt.Field(default_factory=list)
1755
+ detailed_metric_pivot_headers: list[DetailedMetricPivotHeader]
1756
+ detailed_metric_pivot_tables: list[DetailedMetricPivotTable] = pdt.Field(default_factory=list)
1757
+
1758
+
1759
+ @router.get("/list_model_assessments_of/{experiment_uuid}",
1760
+ response_model=list[AssessmentTableDetailWithMetricValues])
1761
+ def list_model_assessments_of(
1762
+ experiment_uuid: py_uuid.UUID = Path(),
1763
+ db: sa_orm.Session = Depends(make_session),
1764
+ ) -> list[AssessmentTableDetail]:
1765
+ with managed_db_session(db):
1766
+ db_assessment_evaluation_promotions = (
1767
+ db
1768
+ .query(AssessmentTable, EvaluationTable, AssessmentPromotionTable)
1769
+ .outerjoin(EvaluationTable, AssessmentTable.uuid == EvaluationTable.assessment_uuid)
1770
+ .outerjoin(AssessmentPromotionTable, AssessmentTable.uuid == AssessmentPromotionTable.assessment_uuid)
1771
+ .where(AssessmentTable.expired_at.is_(None),
1772
+ EvaluationTable.expired_at.is_(None),
1773
+ AssessmentPromotionTable.expired_at.is_(None),
1774
+ AssessmentTable.experiment_uuid == experiment_uuid)
1775
+ .order_by(AssessmentTable.created_at.desc())
1776
+ .all()
1777
+ )
1778
+ if not db_assessment_evaluation_promotions:
1779
+ raise sa_exc.NoResultFound(
1780
+ f"'{model_name_of(ExperimentTable)}' of experiment_uuid '{experiment_uuid}' not found")
1781
+
1782
+ benchmark_uuids = list(set(db_assessment.benchmark_uuid
1783
+ for db_assessment, _, _ in db_assessment_evaluation_promotions))
1784
+ db_benchmarks = (
1785
+ db
1786
+ .query(BenchmarkTable)
1787
+ .where(BenchmarkTable.expired_at.is_(None), BenchmarkTable.uuid.in_(benchmark_uuids))
1788
+ .all()
1789
+ )
1790
+ db_benchmarks_dict = {db_benchmark.uuid: db_benchmark for db_benchmark in db_benchmarks}
1791
+
1792
+ dataset_uuids = list(set(db_assessment.dataset_uuid
1793
+ for db_assessment, _, _ in db_assessment_evaluation_promotions))
1794
+ layout_samples_counts_by_datasets = query_layout_samples_counts(dataset_uuids, db)
1795
+
1796
+ db_datasets = (
1797
+ db
1798
+ .query(DatasetTable)
1799
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid.in_(dataset_uuids))
1800
+ .all()
1801
+ )
1802
+ db_datasets_dict = {db_dataset.uuid: db_dataset for db_dataset in db_datasets}
1803
+
1804
+ result = []
1805
+
1806
+ for db_assessment, db_evaluation, db_promotion in db_assessment_evaluation_promotions:
1807
+ db_benchmark = db_benchmarks_dict.get(db_assessment.benchmark_uuid)
1808
+ metrics_display_operation = registered_metrics_display_operations().get(db_benchmark.name)
1809
+
1810
+ def metric_values_maker(assessment: AssessmentTable, columns: list[TableColumn]) -> list[float]:
1811
+ metric_values = metrics_display_operation.metric_values_maker(assessment, columns)
1812
+ if len(metric_values) != len(columns):
1813
+ raise ValueError("mismatched metric columns and metric values lengths")
1814
+ return metric_values
1815
+
1816
+ def detailed_metric_pivot_tables_maker(
1817
+ assessment: AssessmentTable,
1818
+ headers: list[DetailedMetricPivotHeader],
1819
+ ) -> list[DetailedMetricPivotTable]:
1820
+ pivot_tables = metrics_display_operation.detailed_metric_pivot_tables_maker(assessment, headers)
1821
+ if len(pivot_tables) != len(headers):
1822
+ raise ValueError("mismatched detailed metric pivot headers and pivot tables lengths")
1823
+ for header, pivot_table in zip(headers, pivot_tables):
1824
+ if len(pivot_table.title_values_sets) != len(pivot_table.metric_values_sets):
1825
+ raise ValueError("mismatched title values sets and metric values sets lengths")
1826
+ for title_values in pivot_table.title_values_sets:
1827
+ if len(title_values) != len(header.title_columns):
1828
+ raise ValueError("mismatched title columns and title values lengths")
1829
+ for metric_values in pivot_table.metric_values_sets:
1830
+ if len(metric_values) != len(header.metric_columns):
1831
+ raise ValueError("mismatched metric columns and metric values lengths")
1832
+ return pivot_tables
1833
+
1834
+ result.append(
1835
+ AssessmentTableDetailWithMetricValues(
1836
+ assessment_detail=AssessmentTableDetail(
1837
+ assessment=db_assessment,
1838
+ evaluation=db_evaluation,
1839
+ promotion=db_promotion,
1840
+ benchmark=db_benchmarks_dict.get(db_assessment.benchmark_uuid),
1841
+ experiment=None,
1842
+ dataset=db_datasets_dict.get(db_assessment.dataset_uuid),
1843
+ layouts=layout_samples_counts_by_datasets.get(db_assessment.dataset_uuid, []),
1844
+ samples_count=sum(layout_samples_count.samples_count
1845
+ for layout_samples_count in
1846
+ layout_samples_counts_by_datasets.get(db_assessment.dataset_uuid, [])),
1847
+ architecture=None,
1848
+ ),
1849
+ metric_columns=metrics_display_operation.metric_columns,
1850
+ metric_values=metric_values_maker(
1851
+ db_assessment,
1852
+ metrics_display_operation.metric_columns,
1853
+ ),
1854
+ detailed_metric_pivot_headers=metrics_display_operation.detailed_metric_pivot_headers,
1855
+ detailed_metric_pivot_tables=detailed_metric_pivot_tables_maker(
1856
+ db_assessment,
1857
+ metrics_display_operation.detailed_metric_pivot_headers,
1858
+ ),
1859
+ )
1860
+ )
1861
+
1862
+ return result
1863
+
1864
+
1865
+ class AssessmentTableDetailWithRankingMetricValues(pdt.BaseModel):
1866
+ assessment_detail: AssessmentTableDetail
1867
+ metric_values: list[float] = pdt.Field(default_factory=list)
1868
+ detailed_metric_pivot_tables: list[DetailedMetricPivotTable] = pdt.Field(default_factory=list)
1869
+
1870
+
1871
+ class AssessmentTableDetailWithRankingMetricValuesAndPagination(pdt.BaseModel):
1872
+ metric_columns: list[TableColumn]
1873
+ detailed_metric_pivot_headers: list[DetailedMetricPivotHeader]
1874
+ assessment_details_with_metrics: list[AssessmentTableDetailWithRankingMetricValues]
1875
+ pagination: Pagination
1876
+
1877
+
1878
+ @router.get("/benchmark_rankings/", response_model=AssessmentTableDetailWithRankingMetricValuesAndPagination)
1879
+ def benchmark_rankings(
1880
+ benchmark: str = Query(),
1881
+ skip: int = Query(0),
1882
+ limit: int = Query(1000),
1883
+ begin_at: datetime.datetime | None = Query(None),
1884
+ end_at: datetime.datetime | None = Query(None),
1885
+ name_match: str | None = Query(None),
1886
+ contributor_sso_sid: int | None = Query(None),
1887
+ only_completed_jobs: bool = Query(True),
1888
+ only_shared: bool = Query(True),
1889
+ db: sa_orm.Session = Depends(make_session),
1890
+ ) -> AssessmentTableDetailWithRankingMetricValuesAndPagination:
1891
+ with managed_db_session(db):
1892
+ metrics_display_operation = registered_metrics_display_operations().get(benchmark)
1893
+ if metrics_display_operation is None:
1894
+ raise ValueError(f"missing ranking metrics registered for benchmark '{benchmark}'")
1895
+
1896
+ def metric_values_maker(assessment: AssessmentTable, columns: list[TableColumn]) -> list[float]:
1897
+ metric_values = metrics_display_operation.metric_values_maker(assessment, columns)
1898
+ if len(metric_values) != len(columns):
1899
+ raise ValueError("mismatched metric columns and metric values lengths")
1900
+ return metric_values
1901
+
1902
+ def detailed_metric_pivot_tables_maker(
1903
+ assessment: AssessmentTable,
1904
+ headers: list[DetailedMetricPivotHeader],
1905
+ ) -> list[DetailedMetricPivotTable]:
1906
+ pivot_tables = metrics_display_operation.detailed_metric_pivot_tables_maker(assessment, headers)
1907
+ if len(pivot_tables) != len(headers):
1908
+ raise ValueError("mismatched detailed metric pivot headers and pivot tables lengths")
1909
+ for header, pivot_table in zip(headers, pivot_tables):
1910
+ if len(pivot_table.title_values_sets) != len(pivot_table.metric_values_sets):
1911
+ raise ValueError("mismatched title values sets and metric values sets lengths")
1912
+ for title_values in pivot_table.title_values_sets:
1913
+ if len(title_values) != len(header.title_columns):
1914
+ raise ValueError("mismatched title columns and title values lengths")
1915
+ for metric_values in pivot_table.metric_values_sets:
1916
+ if len(metric_values) != len(header.metric_columns):
1917
+ raise ValueError("mismatched metric columns and metric values lengths")
1918
+ return pivot_tables
1919
+
1920
+ db_benchmark = (
1921
+ db
1922
+ .query(BenchmarkTable)
1923
+ .where(BenchmarkTable.expired_at.is_(None), BenchmarkTable.name == benchmark)
1924
+ .one_or_none()
1925
+ )
1926
+ if db_benchmark is None:
1927
+ raise sa_exc.NoResultFound(f"'{model_name_of(BenchmarkTable)}' with name '{benchmark}' not found")
1928
+
1929
+ query_stmt = (
1930
+ db
1931
+ .query(AssessmentTable, EvaluationTable, AssessmentPromotionTable)
1932
+ .outerjoin(EvaluationTable, AssessmentTable.uuid == EvaluationTable.assessment_uuid)
1933
+ .outerjoin(AssessmentPromotionTable, AssessmentTable.uuid == AssessmentPromotionTable.assessment_uuid)
1934
+ .where(AssessmentTable.expired_at.is_(None),
1935
+ EvaluationTable.expired_at.is_(None),
1936
+ AssessmentPromotionTable.expired_at.is_(None),
1937
+ AssessmentTable.benchmark_uuid == db_benchmark.uuid)
1938
+ )
1939
+ if only_completed_jobs:
1940
+ query_stmt = query_stmt.where(sa.or_(EvaluationTable.sqn.is_(None),
1941
+ EvaluationTable.flags.op("&")(JobStatusFlag.Completed.value) > 0))
1942
+ if only_shared:
1943
+ query_stmt = query_stmt.where(AssessmentPromotionTable.flags.op("&")(JobStatusFlag.Completed.value) > 0)
1944
+ if begin_at is not None:
1945
+ query_stmt = query_stmt.where(AssessmentTable.created_at >= begin_at)
1946
+ if end_at is not None:
1947
+ query_stmt = query_stmt.where(AssessmentTable.created_at < end_at)
1948
+ if name_match is not None:
1949
+ query_stmt = query_stmt.where(AssessmentTable.name.ilike(f"%{escape_sql_like(name_match)}%", escape="\\"))
1950
+ if contributor_sso_sid is not None:
1951
+ query_stmt = query_stmt.where(AssessmentTable.contributor_sso_sid == contributor_sso_sid)
1952
+
1953
+ total = query_stmt.count()
1954
+ if total == 0:
1955
+ return AssessmentTableDetailWithRankingMetricValuesAndPagination(
1956
+ metric_columns=metrics_display_operation.metric_columns,
1957
+ detailed_metric_pivot_headers=metrics_display_operation.detailed_metric_pivot_headers,
1958
+ assessment_details_with_metrics=[],
1959
+ pagination=Pagination(skip=skip, limit=limit, total=0),
1960
+ )
1961
+
1962
+ if contributor_sso_sid is None:
1963
+ query_stmt = query_stmt.order_by(*metrics_display_operation.order_maker(AssessmentTable))
1964
+ else:
1965
+ query_stmt = query_stmt.order_by(AssessmentTable.sqn)
1966
+
1967
+ db_assessment_evaluation_promotions = query_stmt.offset(skip).limit(limit).all()
1968
+
1969
+ benchmark_uuids = list(set(db_assessment.benchmark_uuid
1970
+ for db_assessment, _, _ in db_assessment_evaluation_promotions))
1971
+ db_benchmarks = (
1972
+ db
1973
+ .query(BenchmarkTable)
1974
+ .where(BenchmarkTable.expired_at.is_(None), BenchmarkTable.uuid.in_(benchmark_uuids))
1975
+ .all()
1976
+ )
1977
+ db_benchmarks_dict = {db_benchmark.uuid: db_benchmark for db_benchmark in db_benchmarks}
1978
+
1979
+ experiment_uuids = list(set(db_assessment.experiment_uuid
1980
+ for db_assessment, _, _ in db_assessment_evaluation_promotions))
1981
+ db_experiments = (
1982
+ db
1983
+ .query(ExperimentTable)
1984
+ .where(ExperimentTable.expired_at.is_(None), ExperimentTable.uuid.in_(experiment_uuids))
1985
+ .all()
1986
+ )
1987
+ db_experiments_dict = {db_experiment.uuid: db_experiment for db_experiment in db_experiments}
1988
+
1989
+ dataset_uuids = list(set(db_assessment.dataset_uuid
1990
+ for db_assessment, _, _ in db_assessment_evaluation_promotions))
1991
+ layout_samples_counts_by_datasets = query_layout_samples_counts(dataset_uuids, db)
1992
+
1993
+ db_datasets = (
1994
+ db
1995
+ .query(DatasetTable)
1996
+ .where(DatasetTable.expired_at.is_(None), DatasetTable.uuid.in_(dataset_uuids))
1997
+ .all()
1998
+ )
1999
+ db_datasets_dict = {db_dataset.uuid: db_dataset for db_dataset in db_datasets}
2000
+
2001
+ architecture_uuids = list(set(db_experiment.architecture_uuid for db_experiment in db_experiments))
2002
+ db_architectures = (
2003
+ db
2004
+ .query(ArchitectureTable)
2005
+ .where(ArchitectureTable.expired_at.is_(None), ArchitectureTable.uuid.in_(architecture_uuids))
2006
+ .all()
2007
+ )
2008
+ db_architectures_dict = {db_architecture.uuid: db_architecture for db_architecture in db_architectures}
2009
+
2010
+ return AssessmentTableDetailWithRankingMetricValuesAndPagination(
2011
+ metric_columns=metrics_display_operation.metric_columns,
2012
+ detailed_metric_pivot_headers=metrics_display_operation.detailed_metric_pivot_headers,
2013
+ assessment_details_with_metrics=[
2014
+ AssessmentTableDetailWithRankingMetricValues(
2015
+ assessment_detail=AssessmentTableDetail(
2016
+ assessment=db_assessment,
2017
+ evaluation=db_evaluation,
2018
+ promotion=db_promotion,
2019
+ benchmark=db_benchmarks_dict.get(db_assessment.benchmark_uuid),
2020
+ experiment=db_experiments_dict.get(db_assessment.experiment_uuid),
2021
+ dataset=db_datasets_dict.get(db_assessment.dataset_uuid),
2022
+ layouts=layout_samples_counts_by_datasets.get(db_assessment.dataset_uuid, []),
2023
+ samples_count=sum(layout_samples_count.samples_count
2024
+ for layout_samples_count in
2025
+ layout_samples_counts_by_datasets.get(db_assessment.dataset_uuid, [])),
2026
+ architecture=(
2027
+ db_architectures_dict.get(
2028
+ db_experiments_dict.get(db_assessment.experiment_uuid).architecture_uuid)
2029
+ if db_assessment.experiment_uuid in db_experiments_dict else None
2030
+ ),
2031
+ ),
2032
+ metric_values=metric_values_maker(
2033
+ db_assessment,
2034
+ metrics_display_operation.metric_columns,
2035
+ ),
2036
+ detailed_metric_pivot_tables=detailed_metric_pivot_tables_maker(
2037
+ db_assessment,
2038
+ metrics_display_operation.detailed_metric_pivot_headers,
2039
+ ),
2040
+ )
2041
+ for db_assessment, db_evaluation, db_promotion in db_assessment_evaluation_promotions
2042
+ ],
2043
+ pagination=Pagination(skip=skip, limit=limit, total=total),
2044
+ )
2045
+
2046
+
2047
+ class SamplesCountByLayout(pdt.BaseModel):
2048
+ layout: LayoutTable
2049
+ count: int
2050
+
2051
+
2052
+ class ExperimentsCountByArchitecture(pdt.BaseModel):
2053
+ architecture: ArchitectureTable
2054
+ count: int
2055
+
2056
+
2057
+ class AssessmentsCountByBenchmark(pdt.BaseModel):
2058
+ benchmark: BenchmarkTable
2059
+ count: int
2060
+
2061
+
2062
+ class Dashboard(pdt.BaseModel):
2063
+ samples_counts_by_layouts: list[SamplesCountByLayout]
2064
+ experiments_counts_by_architectures: list[ExperimentsCountByArchitecture]
2065
+ assessments_counts_by_benchmarks: list[AssessmentsCountByBenchmark]
2066
+ samples_counts_by_layouts_for_annotation: list[SamplesCountByLayout] = pdt.Field(default_factory=list)
2067
+ samples_counts_by_layouts_for_evaluation: list[SamplesCountByLayout] = pdt.Field(default_factory=list)
2068
+
2069
+
2070
+ @router.get("/dashboard/", response_model=Dashboard)
2071
+ def dashboard(db: sa_orm.Session = Depends(make_session)) -> Dashboard:
2072
+ with managed_db_session(db):
2073
+ def compute_samples_counts_by_layouts():
2074
+ counts_query_stmt = (
2075
+ db
2076
+ .query(SampleTable.layout_uuid, sa.func.count().label("counts"))
2077
+ .where(SampleTable.expired_at.is_(None))
2078
+ .group_by(SampleTable.layout_uuid)
2079
+ .subquery()
2080
+ )
2081
+
2082
+ counts = (
2083
+ db
2084
+ .query(LayoutTable, sa.func.coalesce(counts_query_stmt.c.counts, 0))
2085
+ .where(LayoutTable.expired_at.is_(None))
2086
+ .outerjoin(counts_query_stmt, LayoutTable.uuid == counts_query_stmt.c.layout_uuid)
2087
+ .order_by(LayoutTable.sqn)
2088
+ .all()
2089
+ )
2090
+
2091
+ return [SamplesCountByLayout(layout=db_layout, count=count) for db_layout, count in counts]
2092
+
2093
+ def compute_experiments_counts_by_architectures():
2094
+ counts_query_stmt = (
2095
+ db
2096
+ .query(ExperimentTable.architecture_uuid, sa.func.count().label("counts"))
2097
+ .where(ExperimentTable.expired_at.is_(None))
2098
+ .group_by(ExperimentTable.architecture_uuid)
2099
+ .subquery()
2100
+ )
2101
+
2102
+ counts = (
2103
+ db
2104
+ .query(ArchitectureTable, sa.func.coalesce(counts_query_stmt.c.counts, 0))
2105
+ .where(ArchitectureTable.expired_at.is_(None))
2106
+ .outerjoin(counts_query_stmt, ArchitectureTable.uuid == counts_query_stmt.c.architecture_uuid)
2107
+ .order_by(ArchitectureTable.sqn)
2108
+ .all()
2109
+ )
2110
+
2111
+ return [ExperimentsCountByArchitecture(architecture=db_architecture, count=count)
2112
+ for db_architecture, count in counts]
2113
+
2114
+ def compute_assessments_counts_by_benchmarks():
2115
+ counts_query_stmt = (
2116
+ db
2117
+ .query(AssessmentTable.benchmark_uuid, sa.func.count().label("counts"))
2118
+ .where(AssessmentTable.expired_at.is_(None))
2119
+ .group_by(AssessmentTable.benchmark_uuid)
2120
+ .subquery()
2121
+ )
2122
+
2123
+ counts = (
2124
+ db
2125
+ .query(BenchmarkTable, sa.func.coalesce(counts_query_stmt.c.counts, 0))
2126
+ .where(BenchmarkTable.expired_at.is_(None))
2127
+ .outerjoin(counts_query_stmt, BenchmarkTable.uuid == counts_query_stmt.c.benchmark_uuid)
2128
+ .order_by(BenchmarkTable.sqn)
2129
+ .all()
2130
+ )
2131
+
2132
+ return [AssessmentsCountByBenchmark(benchmark=db_benchmark, count=count)
2133
+ for db_benchmark, count in counts]
2134
+
2135
+ return Dashboard(
2136
+ samples_counts_by_layouts=compute_samples_counts_by_layouts(),
2137
+ experiments_counts_by_architectures=compute_experiments_counts_by_architectures(),
2138
+ assessments_counts_by_benchmarks=compute_assessments_counts_by_benchmarks(),
2139
+ )
2140
+
2141
+
2142
+ class Tagset(pdt.BaseModel):
2143
+ namespace: str
2144
+ tags: list[str]
2145
+
2146
+ @pdt.field_validator("namespace", mode="after")
2147
+ @classmethod
2148
+ def validate_namespace(cls, v: str) -> str:
2149
+ if v not in predefined_tagsets():
2150
+ raise ValueError(f"invalid tagset namespace '{v}'")
2151
+ return v
2152
+
2153
+
2154
+ @router.get("/get_tagset/", response_model=Tagset)
2155
+ def get_tagset(namespace: str = Query("universal")) -> Tagset:
2156
+ if namespace not in predefined_tagsets():
2157
+ raise ValueError(f"invalid tagset namespace '{namespace}'")
2158
+
2159
+ predefined_tagset = predefined_tagsets()[namespace]
2160
+
2161
+ return Tagset(namespace=predefined_tagset.namespace, tags=[tag.name for tag in predefined_tagset.tags])