albert 1.10.0rc2__py3-none-any.whl → 1.11.0__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.
- albert/__init__.py +1 -1
- albert/client.py +5 -0
- albert/collections/custom_templates.py +3 -0
- albert/collections/data_templates.py +118 -264
- albert/collections/entity_types.py +19 -3
- albert/collections/inventory.py +1 -1
- albert/collections/notebooks.py +154 -26
- albert/collections/parameters.py +1 -0
- albert/collections/property_data.py +384 -280
- albert/collections/reports.py +4 -0
- albert/collections/synthesis.py +292 -0
- albert/collections/tasks.py +2 -1
- albert/collections/worksheets.py +3 -0
- albert/core/shared/models/base.py +3 -1
- albert/core/shared/models/patch.py +1 -1
- albert/resources/batch_data.py +4 -2
- albert/resources/cas.py +3 -1
- albert/resources/custom_fields.py +3 -1
- albert/resources/data_templates.py +60 -12
- albert/resources/inventory.py +6 -4
- albert/resources/lists.py +3 -1
- albert/resources/notebooks.py +12 -7
- albert/resources/parameter_groups.py +3 -1
- albert/resources/property_data.py +64 -5
- albert/resources/sheets.py +16 -14
- albert/resources/synthesis.py +61 -0
- albert/resources/tags.py +3 -1
- albert/resources/tasks.py +4 -7
- albert/resources/workflows.py +4 -2
- albert/utils/data_template.py +392 -37
- albert/utils/property_data.py +638 -0
- albert/utils/tasks.py +3 -3
- {albert-1.10.0rc2.dist-info → albert-1.11.0.dist-info}/METADATA +1 -1
- {albert-1.10.0rc2.dist-info → albert-1.11.0.dist-info}/RECORD +36 -33
- {albert-1.10.0rc2.dist-info → albert-1.11.0.dist-info}/WHEEL +0 -0
- {albert-1.10.0rc2.dist-info → albert-1.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import re
|
|
2
1
|
from collections.abc import Iterator
|
|
3
2
|
from contextlib import suppress
|
|
4
3
|
from enum import Enum
|
|
@@ -23,26 +22,24 @@ from albert.core.shared.identifiers import (
|
|
|
23
22
|
TaskId,
|
|
24
23
|
UserId,
|
|
25
24
|
)
|
|
26
|
-
from albert.core.shared.models.base import EntityLink
|
|
27
25
|
from albert.core.shared.models.patch import PatchOperation
|
|
28
26
|
from albert.exceptions import NotFoundError
|
|
29
27
|
from albert.resources.property_data import (
|
|
30
28
|
BulkPropertyData,
|
|
31
29
|
CheckPropertyData,
|
|
30
|
+
CurvePropertyValue,
|
|
32
31
|
DataEntity,
|
|
32
|
+
ImagePropertyValue,
|
|
33
33
|
InventoryDataColumn,
|
|
34
34
|
InventoryPropertyData,
|
|
35
35
|
InventoryPropertyDataCreate,
|
|
36
36
|
PropertyDataPatchDatum,
|
|
37
37
|
PropertyDataSearchItem,
|
|
38
|
-
PropertyValue,
|
|
39
38
|
ReturnScope,
|
|
40
|
-
TaskDataColumn,
|
|
41
39
|
TaskPropertyCreate,
|
|
42
40
|
TaskPropertyData,
|
|
43
|
-
Trial,
|
|
44
41
|
)
|
|
45
|
-
from albert.
|
|
42
|
+
from albert.utils import property_data as property_data_utils
|
|
46
43
|
|
|
47
44
|
|
|
48
45
|
class PropertyDataCollection(BaseCollection):
|
|
@@ -62,12 +59,6 @@ class PropertyDataCollection(BaseCollection):
|
|
|
62
59
|
super().__init__(session=session)
|
|
63
60
|
self.base_path = f"/api/{PropertyDataCollection._api_version}/propertydata"
|
|
64
61
|
|
|
65
|
-
@validate_call
|
|
66
|
-
def _get_task_from_id(self, *, id: TaskId) -> PropertyTask:
|
|
67
|
-
from albert.collections.tasks import TaskCollection
|
|
68
|
-
|
|
69
|
-
return TaskCollection(session=self.session).get_by_id(id=id)
|
|
70
|
-
|
|
71
62
|
@validate_call
|
|
72
63
|
def get_properties_on_inventory(self, *, inventory_id: InventoryId) -> InventoryPropertyData:
|
|
73
64
|
"""Returns all the properties of an inventory item.
|
|
@@ -233,7 +224,7 @@ class PropertyDataCollection(BaseCollection):
|
|
|
233
224
|
list[CheckPropertyData]
|
|
234
225
|
A list of CheckPropertyData entities representing the data status of each block + inventory item of the task.
|
|
235
226
|
"""
|
|
236
|
-
task_info = self.
|
|
227
|
+
task_info = property_data_utils.get_task_from_id(session=self.session, id=task_id)
|
|
237
228
|
|
|
238
229
|
params = {
|
|
239
230
|
"entity": "block",
|
|
@@ -263,7 +254,7 @@ class PropertyDataCollection(BaseCollection):
|
|
|
263
254
|
Returns
|
|
264
255
|
-------
|
|
265
256
|
CheckPropertyData
|
|
266
|
-
|
|
257
|
+
CheckPropertyData representing the data status of each block + inventory item of the task.
|
|
267
258
|
"""
|
|
268
259
|
params = {
|
|
269
260
|
"entity": "block",
|
|
@@ -310,37 +301,6 @@ class PropertyDataCollection(BaseCollection):
|
|
|
310
301
|
)
|
|
311
302
|
return all_info
|
|
312
303
|
|
|
313
|
-
def _resolve_return_scope(
|
|
314
|
-
self,
|
|
315
|
-
*,
|
|
316
|
-
task_id: TaskId,
|
|
317
|
-
return_scope: ReturnScope,
|
|
318
|
-
inventory_id: InventoryId | None,
|
|
319
|
-
block_id: BlockId | None,
|
|
320
|
-
lot_id: LotId | None,
|
|
321
|
-
prefetched_block: TaskPropertyData | None = None,
|
|
322
|
-
) -> list[TaskPropertyData]:
|
|
323
|
-
if return_scope == "task":
|
|
324
|
-
return self.get_all_task_properties(task_id=task_id)
|
|
325
|
-
|
|
326
|
-
if return_scope == "block":
|
|
327
|
-
if prefetched_block is not None:
|
|
328
|
-
return [prefetched_block]
|
|
329
|
-
if inventory_id is None or block_id is None:
|
|
330
|
-
raise ValueError(
|
|
331
|
-
"inventory_id and block_id are required when return_scope='combo'."
|
|
332
|
-
)
|
|
333
|
-
return [
|
|
334
|
-
self.get_task_block_properties(
|
|
335
|
-
inventory_id=inventory_id,
|
|
336
|
-
task_id=task_id,
|
|
337
|
-
block_id=block_id,
|
|
338
|
-
lot_id=lot_id,
|
|
339
|
-
)
|
|
340
|
-
]
|
|
341
|
-
|
|
342
|
-
return []
|
|
343
|
-
|
|
344
304
|
@validate_call
|
|
345
305
|
def update_property_on_task(
|
|
346
306
|
self,
|
|
@@ -359,7 +319,8 @@ class PropertyDataCollection(BaseCollection):
|
|
|
359
319
|
task_id : TaskId
|
|
360
320
|
The ID of the task.
|
|
361
321
|
patch_payload : list[PropertyDataPatchDatum]
|
|
362
|
-
The specific patch to make to update the property.
|
|
322
|
+
The specific patch to make to update the property. ImagePropertyValue and
|
|
323
|
+
CurvePropertyValue updates require update_or_create_task_properties.
|
|
363
324
|
inventory_id : InventoryId | None, optional
|
|
364
325
|
Required when return_scope="block".
|
|
365
326
|
block_id : BlockId | None, optional
|
|
@@ -376,19 +337,302 @@ class PropertyDataCollection(BaseCollection):
|
|
|
376
337
|
A list of TaskPropertyData entities representing the properties within the task.
|
|
377
338
|
"""
|
|
378
339
|
if len(patch_payload) > 0:
|
|
340
|
+
resolved_payload = property_data_utils.resolve_patch_payload(
|
|
341
|
+
session=self.session,
|
|
342
|
+
task_id=task_id,
|
|
343
|
+
patch_payload=patch_payload,
|
|
344
|
+
)
|
|
379
345
|
self.session.patch(
|
|
380
346
|
url=f"{self.base_path}/{task_id}",
|
|
381
|
-
json=
|
|
382
|
-
x.model_dump(exclude_none=True, by_alias=True, mode="json")
|
|
383
|
-
for x in patch_payload
|
|
384
|
-
],
|
|
347
|
+
json=resolved_payload,
|
|
385
348
|
)
|
|
386
|
-
return
|
|
349
|
+
return property_data_utils.resolve_return_scope(
|
|
387
350
|
task_id=task_id,
|
|
388
351
|
return_scope=return_scope,
|
|
389
352
|
inventory_id=inventory_id,
|
|
390
353
|
block_id=block_id,
|
|
391
354
|
lot_id=lot_id,
|
|
355
|
+
prefetched_block=None,
|
|
356
|
+
get_all_task_properties=self.get_all_task_properties,
|
|
357
|
+
get_task_block_properties=self.get_task_block_properties,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
@validate_call
|
|
361
|
+
def void_task_data(
|
|
362
|
+
self,
|
|
363
|
+
*,
|
|
364
|
+
task_id: TaskId,
|
|
365
|
+
inventory_id: InventoryId,
|
|
366
|
+
block_id: BlockId,
|
|
367
|
+
lot_id: LotId | None = None,
|
|
368
|
+
) -> None:
|
|
369
|
+
"""Void all property data for a task.
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
task_id : TaskId
|
|
374
|
+
The ID of the task.
|
|
375
|
+
inventory_id : InventoryId
|
|
376
|
+
The ID of the inventory item.
|
|
377
|
+
block_id : BlockId
|
|
378
|
+
The ID of the block.
|
|
379
|
+
lot_id : LotId | None, optional
|
|
380
|
+
The ID of the lot, by default None.
|
|
381
|
+
|
|
382
|
+
Returns
|
|
383
|
+
-------
|
|
384
|
+
None
|
|
385
|
+
"""
|
|
386
|
+
payload = {
|
|
387
|
+
"operation": "void",
|
|
388
|
+
"by": "task",
|
|
389
|
+
"id": task_id,
|
|
390
|
+
"inventoryId": inventory_id,
|
|
391
|
+
"blockId": block_id,
|
|
392
|
+
"lotId": lot_id,
|
|
393
|
+
}
|
|
394
|
+
payload = {k: v for k, v in payload.items() if v is not None}
|
|
395
|
+
self.session.patch(
|
|
396
|
+
url=f"{self.base_path}/{task_id}",
|
|
397
|
+
json=payload,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
@validate_call
|
|
401
|
+
def unvoid_task_data(
|
|
402
|
+
self,
|
|
403
|
+
*,
|
|
404
|
+
task_id: TaskId,
|
|
405
|
+
inventory_id: InventoryId,
|
|
406
|
+
block_id: BlockId,
|
|
407
|
+
lot_id: LotId | None = None,
|
|
408
|
+
) -> None:
|
|
409
|
+
"""Unvoid all property data for a task.
|
|
410
|
+
|
|
411
|
+
Parameters
|
|
412
|
+
----------
|
|
413
|
+
task_id : TaskId
|
|
414
|
+
The ID of the task.
|
|
415
|
+
inventory_id : InventoryId
|
|
416
|
+
The ID of the inventory item.
|
|
417
|
+
block_id : BlockId
|
|
418
|
+
The ID of the block.
|
|
419
|
+
lot_id : LotId | None, optional
|
|
420
|
+
The ID of the lot, by default None.
|
|
421
|
+
|
|
422
|
+
Returns
|
|
423
|
+
-------
|
|
424
|
+
None
|
|
425
|
+
"""
|
|
426
|
+
payload = {
|
|
427
|
+
"operation": "unvoid",
|
|
428
|
+
"by": "task",
|
|
429
|
+
"id": task_id,
|
|
430
|
+
"inventoryId": inventory_id,
|
|
431
|
+
"blockId": block_id,
|
|
432
|
+
"lotId": lot_id,
|
|
433
|
+
}
|
|
434
|
+
payload = {k: v for k, v in payload.items() if v is not None}
|
|
435
|
+
self.session.patch(
|
|
436
|
+
url=f"{self.base_path}/{task_id}",
|
|
437
|
+
json=payload,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
@validate_call
|
|
441
|
+
def void_interval_data(
|
|
442
|
+
self,
|
|
443
|
+
*,
|
|
444
|
+
task_id: TaskId,
|
|
445
|
+
interval_id: str,
|
|
446
|
+
inventory_id: InventoryId,
|
|
447
|
+
block_id: BlockId,
|
|
448
|
+
lot_id: LotId | None = None,
|
|
449
|
+
data_template_id: DataTemplateId | None = None,
|
|
450
|
+
) -> None:
|
|
451
|
+
"""Void all property data for a specific interval combination.
|
|
452
|
+
|
|
453
|
+
Parameters
|
|
454
|
+
----------
|
|
455
|
+
task_id : TaskId
|
|
456
|
+
The ID of the task.
|
|
457
|
+
interval_id : str
|
|
458
|
+
The interval combination identifier (``CheckPropertyData.interval_id``).
|
|
459
|
+
Use ``check_for_task_data`` to list interval combinations for a task.
|
|
460
|
+
inventory_id : InventoryId
|
|
461
|
+
The ID of the inventory item.
|
|
462
|
+
block_id : BlockId
|
|
463
|
+
The ID of the block.
|
|
464
|
+
lot_id : LotId | None, optional
|
|
465
|
+
The ID of the lot, by default None.
|
|
466
|
+
data_template_id : DataTemplateId | None, optional
|
|
467
|
+
When provided, limits the voiding to a specific data template.
|
|
468
|
+
|
|
469
|
+
Returns
|
|
470
|
+
-------
|
|
471
|
+
None
|
|
472
|
+
"""
|
|
473
|
+
payload = {
|
|
474
|
+
"operation": "void",
|
|
475
|
+
"by": "intervalCombination",
|
|
476
|
+
"id": interval_id,
|
|
477
|
+
"parentId": task_id,
|
|
478
|
+
"inventoryId": inventory_id,
|
|
479
|
+
"blockId": block_id,
|
|
480
|
+
"lotId": lot_id,
|
|
481
|
+
"dataTemplateId": data_template_id,
|
|
482
|
+
}
|
|
483
|
+
payload = {k: v for k, v in payload.items() if v is not None}
|
|
484
|
+
self.session.patch(
|
|
485
|
+
url=f"{self.base_path}/{task_id}",
|
|
486
|
+
json=payload,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
@validate_call
|
|
490
|
+
def unvoid_interval_data(
|
|
491
|
+
self,
|
|
492
|
+
*,
|
|
493
|
+
task_id: TaskId,
|
|
494
|
+
interval_id: str,
|
|
495
|
+
inventory_id: InventoryId,
|
|
496
|
+
block_id: BlockId,
|
|
497
|
+
lot_id: LotId | None = None,
|
|
498
|
+
data_template_id: DataTemplateId | None = None,
|
|
499
|
+
) -> None:
|
|
500
|
+
"""Unvoid all property data for a specific interval combination.
|
|
501
|
+
|
|
502
|
+
Parameters
|
|
503
|
+
----------
|
|
504
|
+
task_id : TaskId
|
|
505
|
+
The ID of the task.
|
|
506
|
+
interval_id : str
|
|
507
|
+
The interval combination identifier (``CheckPropertyData.interval_id``).
|
|
508
|
+
Use ``check_for_task_data`` to list interval combinations for a task.
|
|
509
|
+
inventory_id : InventoryId
|
|
510
|
+
The ID of the inventory item.
|
|
511
|
+
block_id : BlockId
|
|
512
|
+
The ID of the block.
|
|
513
|
+
lot_id : LotId | None, optional
|
|
514
|
+
The ID of the lot, by default None.
|
|
515
|
+
data_template_id : DataTemplateId | None, optional
|
|
516
|
+
When provided, limits the unvoiding to a specific data template.
|
|
517
|
+
|
|
518
|
+
Returns
|
|
519
|
+
-------
|
|
520
|
+
None
|
|
521
|
+
"""
|
|
522
|
+
payload = {
|
|
523
|
+
"operation": "unvoid",
|
|
524
|
+
"by": "intervalCombination",
|
|
525
|
+
"id": interval_id,
|
|
526
|
+
"parentId": task_id,
|
|
527
|
+
"inventoryId": inventory_id,
|
|
528
|
+
"blockId": block_id,
|
|
529
|
+
"lotId": lot_id,
|
|
530
|
+
"dataTemplateId": data_template_id,
|
|
531
|
+
}
|
|
532
|
+
payload = {k: v for k, v in payload.items() if v is not None}
|
|
533
|
+
self.session.patch(
|
|
534
|
+
url=f"{self.base_path}/{task_id}",
|
|
535
|
+
json=payload,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
@validate_call
|
|
539
|
+
def void_trial_data(
|
|
540
|
+
self,
|
|
541
|
+
*,
|
|
542
|
+
task_id: TaskId,
|
|
543
|
+
interval_id: str,
|
|
544
|
+
trial_number: int,
|
|
545
|
+
inventory_id: InventoryId,
|
|
546
|
+
block_id: BlockId,
|
|
547
|
+
lot_id: LotId | None = None,
|
|
548
|
+
) -> None:
|
|
549
|
+
"""Void property data for a specific trial in an interval combination.
|
|
550
|
+
|
|
551
|
+
Parameters
|
|
552
|
+
----------
|
|
553
|
+
task_id : TaskId
|
|
554
|
+
The ID of the task.
|
|
555
|
+
interval_id : str
|
|
556
|
+
The interval combination identifier (``CheckPropertyData.interval_id``).
|
|
557
|
+
Use ``check_for_task_data`` to list interval combinations for a task.
|
|
558
|
+
trial_number : int
|
|
559
|
+
The trial number to void.
|
|
560
|
+
inventory_id : InventoryId
|
|
561
|
+
The ID of the inventory item.
|
|
562
|
+
block_id : BlockId
|
|
563
|
+
The ID of the block.
|
|
564
|
+
lot_id : LotId | None, optional
|
|
565
|
+
The ID of the lot, by default None.
|
|
566
|
+
|
|
567
|
+
Returns
|
|
568
|
+
-------
|
|
569
|
+
None
|
|
570
|
+
"""
|
|
571
|
+
payload = [
|
|
572
|
+
{
|
|
573
|
+
"operation": "void",
|
|
574
|
+
"by": "trial",
|
|
575
|
+
"trial": trial_number,
|
|
576
|
+
"id": interval_id,
|
|
577
|
+
"inventoryId": inventory_id,
|
|
578
|
+
"blockId": block_id,
|
|
579
|
+
"lotId": lot_id,
|
|
580
|
+
}
|
|
581
|
+
]
|
|
582
|
+
payload = [{k: v for k, v in item.items() if v is not None} for item in payload]
|
|
583
|
+
self.session.patch(
|
|
584
|
+
url=f"{self.base_path}/{task_id}",
|
|
585
|
+
json=payload,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
@validate_call
|
|
589
|
+
def unvoid_trial_data(
|
|
590
|
+
self,
|
|
591
|
+
*,
|
|
592
|
+
task_id: TaskId,
|
|
593
|
+
interval_id: str,
|
|
594
|
+
trial_number: int,
|
|
595
|
+
inventory_id: InventoryId,
|
|
596
|
+
block_id: BlockId,
|
|
597
|
+
lot_id: LotId | None = None,
|
|
598
|
+
) -> None:
|
|
599
|
+
"""Unvoid property data for a specific trial in an interval combination.
|
|
600
|
+
|
|
601
|
+
Parameters
|
|
602
|
+
----------
|
|
603
|
+
task_id : TaskId
|
|
604
|
+
The ID of the task.
|
|
605
|
+
interval_id : str
|
|
606
|
+
The interval combination identifier (``CheckPropertyData.interval_id``).
|
|
607
|
+
Use ``check_for_task_data`` to list interval combinations for a task.
|
|
608
|
+
trial_number : int
|
|
609
|
+
The trial number to unvoid.
|
|
610
|
+
inventory_id : InventoryId
|
|
611
|
+
The ID of the inventory item.
|
|
612
|
+
block_id : BlockId
|
|
613
|
+
The ID of the block.
|
|
614
|
+
lot_id : LotId | None, optional
|
|
615
|
+
The ID of the lot, by default None.
|
|
616
|
+
|
|
617
|
+
Returns
|
|
618
|
+
-------
|
|
619
|
+
None
|
|
620
|
+
"""
|
|
621
|
+
payload = [
|
|
622
|
+
{
|
|
623
|
+
"operation": "unvoid",
|
|
624
|
+
"by": "trial",
|
|
625
|
+
"trial": trial_number,
|
|
626
|
+
"id": interval_id,
|
|
627
|
+
"inventoryId": inventory_id,
|
|
628
|
+
"blockId": block_id,
|
|
629
|
+
"lotId": lot_id,
|
|
630
|
+
}
|
|
631
|
+
]
|
|
632
|
+
payload = [{k: v for k, v in item.items() if v is not None} for item in payload]
|
|
633
|
+
self.session.patch(
|
|
634
|
+
url=f"{self.base_path}/{task_id}",
|
|
635
|
+
json=payload,
|
|
392
636
|
)
|
|
393
637
|
|
|
394
638
|
@validate_call
|
|
@@ -439,20 +683,33 @@ class PropertyDataCollection(BaseCollection):
|
|
|
439
683
|
"history": "true",
|
|
440
684
|
}
|
|
441
685
|
params = {k: v for k, v in params.items() if v is not None}
|
|
686
|
+
payload = (
|
|
687
|
+
property_data_utils.resolve_task_property_payload(
|
|
688
|
+
session=self.session,
|
|
689
|
+
task_id=task_id,
|
|
690
|
+
block_id=block_id,
|
|
691
|
+
properties=properties,
|
|
692
|
+
)
|
|
693
|
+
if any(
|
|
694
|
+
isinstance(prop.value, ImagePropertyValue | CurvePropertyValue)
|
|
695
|
+
for prop in properties
|
|
696
|
+
)
|
|
697
|
+
else [x.model_dump(exclude_none=True, by_alias=True, mode="json") for x in properties]
|
|
698
|
+
)
|
|
442
699
|
response = self.session.post(
|
|
443
700
|
url=f"{self.base_path}/{task_id}",
|
|
444
|
-
json=
|
|
701
|
+
json=payload,
|
|
445
702
|
params=params,
|
|
446
703
|
)
|
|
447
|
-
|
|
448
704
|
registered_properties = [
|
|
449
705
|
TaskPropertyCreate(**x) for x in response.json() if "DataTemplate" in x
|
|
450
706
|
]
|
|
451
707
|
existing_data_rows = self.get_task_block_properties(
|
|
452
708
|
inventory_id=inventory_id, task_id=task_id, block_id=block_id, lot_id=lot_id
|
|
453
709
|
)
|
|
454
|
-
patches =
|
|
455
|
-
existing_data_rows=existing_data_rows,
|
|
710
|
+
patches = property_data_utils.form_calculated_task_property_patches(
|
|
711
|
+
existing_data_rows=existing_data_rows,
|
|
712
|
+
properties=registered_properties,
|
|
456
713
|
)
|
|
457
714
|
if len(patches) > 0:
|
|
458
715
|
return self.update_property_on_task(
|
|
@@ -464,13 +721,15 @@ class PropertyDataCollection(BaseCollection):
|
|
|
464
721
|
lot_id=lot_id,
|
|
465
722
|
)
|
|
466
723
|
|
|
467
|
-
return
|
|
724
|
+
return property_data_utils.resolve_return_scope(
|
|
468
725
|
task_id=task_id,
|
|
469
726
|
return_scope=return_scope,
|
|
470
727
|
inventory_id=inventory_id,
|
|
471
728
|
block_id=block_id,
|
|
472
729
|
lot_id=lot_id,
|
|
473
730
|
prefetched_block=existing_data_rows,
|
|
731
|
+
get_all_task_properties=self.get_all_task_properties,
|
|
732
|
+
get_task_block_properties=self.get_task_block_properties,
|
|
474
733
|
)
|
|
475
734
|
|
|
476
735
|
@validate_call
|
|
@@ -514,15 +773,21 @@ class PropertyDataCollection(BaseCollection):
|
|
|
514
773
|
The updated or newly created task properties.
|
|
515
774
|
|
|
516
775
|
"""
|
|
776
|
+
|
|
517
777
|
existing_data_rows = self.get_task_block_properties(
|
|
518
778
|
inventory_id=inventory_id, task_id=task_id, block_id=block_id, lot_id=lot_id
|
|
519
779
|
)
|
|
520
|
-
update_patches, new_values =
|
|
521
|
-
|
|
780
|
+
update_patches, new_values = property_data_utils.form_existing_row_value_patches(
|
|
781
|
+
session=self.session,
|
|
782
|
+
task_id=task_id,
|
|
783
|
+
block_id=block_id,
|
|
784
|
+
existing_data_rows=existing_data_rows,
|
|
785
|
+
properties=properties,
|
|
522
786
|
)
|
|
523
787
|
|
|
524
|
-
calculated_patches =
|
|
525
|
-
existing_data_rows=existing_data_rows,
|
|
788
|
+
calculated_patches = property_data_utils.form_calculated_task_property_patches(
|
|
789
|
+
existing_data_rows=existing_data_rows,
|
|
790
|
+
properties=properties,
|
|
526
791
|
)
|
|
527
792
|
all_patches = update_patches + calculated_patches
|
|
528
793
|
if len(new_values) > 0:
|
|
@@ -535,6 +800,58 @@ class PropertyDataCollection(BaseCollection):
|
|
|
535
800
|
block_id=block_id,
|
|
536
801
|
lot_id=lot_id,
|
|
537
802
|
)
|
|
803
|
+
if any(
|
|
804
|
+
isinstance(prop.value, ImagePropertyValue | CurvePropertyValue)
|
|
805
|
+
for prop in new_values
|
|
806
|
+
):
|
|
807
|
+
params = {
|
|
808
|
+
"blockId": block_id,
|
|
809
|
+
"inventoryId": inventory_id,
|
|
810
|
+
}
|
|
811
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
812
|
+
payload = property_data_utils.resolve_task_property_payload(
|
|
813
|
+
session=self.session,
|
|
814
|
+
task_id=task_id,
|
|
815
|
+
block_id=block_id,
|
|
816
|
+
properties=new_values,
|
|
817
|
+
)
|
|
818
|
+
response = self.session.post(
|
|
819
|
+
url=f"{self.base_path}/{task_id}",
|
|
820
|
+
json=payload,
|
|
821
|
+
params=params,
|
|
822
|
+
)
|
|
823
|
+
registered_properties = [
|
|
824
|
+
TaskPropertyCreate(**x) for x in response.json() if "DataTemplate" in x
|
|
825
|
+
]
|
|
826
|
+
existing_data_rows = self.get_task_block_properties(
|
|
827
|
+
inventory_id=inventory_id,
|
|
828
|
+
task_id=task_id,
|
|
829
|
+
block_id=block_id,
|
|
830
|
+
lot_id=lot_id,
|
|
831
|
+
)
|
|
832
|
+
patches = property_data_utils.form_calculated_task_property_patches(
|
|
833
|
+
existing_data_rows=existing_data_rows,
|
|
834
|
+
properties=registered_properties,
|
|
835
|
+
)
|
|
836
|
+
if len(patches) > 0:
|
|
837
|
+
return self.update_property_on_task(
|
|
838
|
+
task_id=task_id,
|
|
839
|
+
patch_payload=patches,
|
|
840
|
+
return_scope=return_scope,
|
|
841
|
+
inventory_id=inventory_id,
|
|
842
|
+
block_id=block_id,
|
|
843
|
+
lot_id=lot_id,
|
|
844
|
+
)
|
|
845
|
+
return property_data_utils.resolve_return_scope(
|
|
846
|
+
task_id=task_id,
|
|
847
|
+
return_scope=return_scope,
|
|
848
|
+
inventory_id=inventory_id,
|
|
849
|
+
block_id=block_id,
|
|
850
|
+
lot_id=lot_id,
|
|
851
|
+
prefetched_block=existing_data_rows,
|
|
852
|
+
get_all_task_properties=self.get_all_task_properties,
|
|
853
|
+
get_task_block_properties=self.get_task_block_properties,
|
|
854
|
+
)
|
|
538
855
|
return self.add_properties_to_task(
|
|
539
856
|
inventory_id=inventory_id,
|
|
540
857
|
task_id=task_id,
|
|
@@ -612,51 +929,18 @@ class PropertyDataCollection(BaseCollection):
|
|
|
612
929
|
{x.data_column_name: x.data_series for x in property_data.columns}
|
|
613
930
|
)
|
|
614
931
|
|
|
615
|
-
def _get_column_map(dataframe: pd.DataFrame, property_data: TaskPropertyData):
|
|
616
|
-
data_col_info = property_data.data[0].trials[0].data_columns # PropertyValue
|
|
617
|
-
column_map = {}
|
|
618
|
-
for col in dataframe.columns:
|
|
619
|
-
column = [x for x in data_col_info if x.name == col]
|
|
620
|
-
if len(column) == 1:
|
|
621
|
-
column_map[col] = column[0]
|
|
622
|
-
else:
|
|
623
|
-
raise ValueError(
|
|
624
|
-
f"Column '{col}' not found in block data columns or multiple matches found."
|
|
625
|
-
)
|
|
626
|
-
return column_map
|
|
627
|
-
|
|
628
|
-
def _df_to_task_prop_create_list(
|
|
629
|
-
dataframe: pd.DataFrame,
|
|
630
|
-
column_map: dict[str, PropertyValue],
|
|
631
|
-
data_template_id: DataTemplateId,
|
|
632
|
-
) -> list[TaskPropertyCreate]:
|
|
633
|
-
task_prop_create_list = []
|
|
634
|
-
for i, row in dataframe.iterrows():
|
|
635
|
-
for col_name, col_info in column_map.items():
|
|
636
|
-
if col_name not in dataframe.columns:
|
|
637
|
-
raise ValueError(f"Column '{col_name}' not found in DataFrame.")
|
|
638
|
-
|
|
639
|
-
task_prop_create = TaskPropertyCreate(
|
|
640
|
-
data_column=TaskDataColumn(
|
|
641
|
-
data_column_id=col_info.id,
|
|
642
|
-
column_sequence=col_info.sequence,
|
|
643
|
-
),
|
|
644
|
-
value=str(row[col_name]),
|
|
645
|
-
visible_trial_number=i + 1,
|
|
646
|
-
interval_combination=interval,
|
|
647
|
-
data_template=EntityLink(id=data_template_id),
|
|
648
|
-
)
|
|
649
|
-
task_prop_create_list.append(task_prop_create)
|
|
650
|
-
return task_prop_create_list
|
|
651
|
-
|
|
652
932
|
task_prop_data = self.get_task_block_properties(
|
|
653
933
|
inventory_id=inventory_id, task_id=task_id, block_id=block_id, lot_id=lot_id
|
|
654
934
|
)
|
|
655
|
-
column_map = _get_column_map(
|
|
656
|
-
|
|
935
|
+
column_map = property_data_utils._get_column_map(
|
|
936
|
+
dataframe=property_df,
|
|
937
|
+
property_data=task_prop_data,
|
|
938
|
+
)
|
|
939
|
+
all_task_prop_create = property_data_utils._df_to_task_prop_create_list(
|
|
657
940
|
dataframe=property_df,
|
|
658
941
|
column_map=column_map,
|
|
659
942
|
data_template_id=task_prop_data.data_template.id,
|
|
943
|
+
interval=interval,
|
|
660
944
|
)
|
|
661
945
|
with suppress(NotFoundError):
|
|
662
946
|
# This is expected if the task is new and has no data yet.
|
|
@@ -676,6 +960,7 @@ class PropertyDataCollection(BaseCollection):
|
|
|
676
960
|
return_scope=return_scope,
|
|
677
961
|
)
|
|
678
962
|
|
|
963
|
+
@validate_call
|
|
679
964
|
def bulk_delete_task_data(
|
|
680
965
|
self,
|
|
681
966
|
*,
|
|
@@ -714,187 +999,6 @@ class PropertyDataCollection(BaseCollection):
|
|
|
714
999
|
params = {k: v for k, v in params.items() if v is not None}
|
|
715
1000
|
self.session.delete(f"{self.base_path}/{task_id}", params=params)
|
|
716
1001
|
|
|
717
|
-
################### Methods to support Updated Row Value patches #################
|
|
718
|
-
|
|
719
|
-
def _form_existing_row_value_patches(
|
|
720
|
-
self, *, existing_data_rows: TaskPropertyData, properties: list[TaskPropertyCreate]
|
|
721
|
-
):
|
|
722
|
-
patches = []
|
|
723
|
-
new_properties = []
|
|
724
|
-
|
|
725
|
-
for prop in properties:
|
|
726
|
-
if prop.trial_number is None:
|
|
727
|
-
new_properties.append(prop)
|
|
728
|
-
continue
|
|
729
|
-
|
|
730
|
-
prop_patches = self._process_property(prop, existing_data_rows)
|
|
731
|
-
if prop_patches:
|
|
732
|
-
patches.extend(prop_patches)
|
|
733
|
-
else:
|
|
734
|
-
new_properties.append(prop)
|
|
735
|
-
return patches, new_properties
|
|
736
|
-
|
|
737
|
-
def _process_property(
|
|
738
|
-
self, prop: TaskPropertyCreate, existing_data_rows: TaskPropertyData
|
|
739
|
-
) -> list | None:
|
|
740
|
-
for interval in existing_data_rows.data:
|
|
741
|
-
if interval.interval_combination != prop.interval_combination:
|
|
742
|
-
continue
|
|
743
|
-
|
|
744
|
-
for trial in interval.trials:
|
|
745
|
-
if trial.trial_number != prop.trial_number:
|
|
746
|
-
continue
|
|
747
|
-
|
|
748
|
-
trial_patches = self._process_trial(trial, prop)
|
|
749
|
-
if trial_patches:
|
|
750
|
-
return trial_patches
|
|
751
|
-
|
|
752
|
-
return None
|
|
753
|
-
|
|
754
|
-
def _process_trial(self, trial: Trial, prop: TaskPropertyCreate) -> list | None:
|
|
755
|
-
for data_column in trial.data_columns:
|
|
756
|
-
if (
|
|
757
|
-
data_column.data_column_unique_id
|
|
758
|
-
== f"{prop.data_column.data_column_id}#{prop.data_column.column_sequence}"
|
|
759
|
-
and data_column.property_data is not None
|
|
760
|
-
):
|
|
761
|
-
if data_column.property_data.value == prop.value:
|
|
762
|
-
# No need to update this value
|
|
763
|
-
return None
|
|
764
|
-
return [
|
|
765
|
-
PropertyDataPatchDatum(
|
|
766
|
-
id=data_column.property_data.id,
|
|
767
|
-
operation=PatchOperation.UPDATE,
|
|
768
|
-
attribute="value",
|
|
769
|
-
new_value=prop.value,
|
|
770
|
-
old_value=data_column.property_data.value,
|
|
771
|
-
)
|
|
772
|
-
]
|
|
773
|
-
|
|
774
|
-
return None
|
|
775
|
-
|
|
776
|
-
################### Methods to support calculated value patches ##################
|
|
777
|
-
|
|
778
|
-
def _form_calculated_task_property_patches(
|
|
779
|
-
self, *, existing_data_rows: TaskPropertyData, properties: list[TaskPropertyCreate]
|
|
780
|
-
):
|
|
781
|
-
patches = []
|
|
782
|
-
covered_interval_trials = set()
|
|
783
|
-
first_row_data_column = existing_data_rows.data[0].trials[0].data_columns
|
|
784
|
-
columns_used_in_calculations = self._get_all_columns_used_in_calculations(
|
|
785
|
-
first_row_data_column=first_row_data_column
|
|
786
|
-
)
|
|
787
|
-
for posted_prop in properties:
|
|
788
|
-
this_interval_trial = f"{posted_prop.interval_combination}-{posted_prop.trial_number}"
|
|
789
|
-
if (
|
|
790
|
-
this_interval_trial in covered_interval_trials
|
|
791
|
-
or posted_prop.data_column.column_sequence not in columns_used_in_calculations
|
|
792
|
-
):
|
|
793
|
-
continue # we don't need to worry about it hence we skip
|
|
794
|
-
on_platform_row = self._get_on_platform_row(
|
|
795
|
-
existing_data_rows=existing_data_rows,
|
|
796
|
-
trial_number=posted_prop.trial_number,
|
|
797
|
-
interval_combination=posted_prop.interval_combination,
|
|
798
|
-
)
|
|
799
|
-
if on_platform_row is not None:
|
|
800
|
-
these_patches = self._generate_data_patch_payload(trial=on_platform_row)
|
|
801
|
-
patches.extend(these_patches)
|
|
802
|
-
covered_interval_trials.add(this_interval_trial)
|
|
803
|
-
return patches
|
|
804
|
-
|
|
805
|
-
def _get_on_platform_row(
|
|
806
|
-
self, *, existing_data_rows: TaskPropertyData, interval_combination: str, trial_number: int
|
|
807
|
-
):
|
|
808
|
-
for interval in existing_data_rows.data:
|
|
809
|
-
if interval.interval_combination == interval_combination:
|
|
810
|
-
for trial in interval.trials:
|
|
811
|
-
if trial.trial_number == trial_number:
|
|
812
|
-
return trial
|
|
813
|
-
return None
|
|
814
|
-
|
|
815
|
-
def _get_columns_used_in_calculation(self, *, calculation: str | None, used_columns: set[str]):
|
|
816
|
-
if calculation is None:
|
|
817
|
-
return used_columns
|
|
818
|
-
column_pattern = r"COL\d+"
|
|
819
|
-
matches = re.findall(column_pattern, calculation)
|
|
820
|
-
used_columns.update(set(matches))
|
|
821
|
-
return used_columns
|
|
822
|
-
|
|
823
|
-
def _get_all_columns_used_in_calculations(self, *, first_row_data_column: list[PropertyValue]):
|
|
824
|
-
used_columns = set()
|
|
825
|
-
for calc in [x.calculation for x in first_row_data_column]:
|
|
826
|
-
used_columns = self._get_columns_used_in_calculation(
|
|
827
|
-
calculation=calc, used_columns=used_columns
|
|
828
|
-
)
|
|
829
|
-
return used_columns
|
|
830
|
-
|
|
831
|
-
def _evaluate_calculation(self, *, calculation: str, column_values: dict) -> float | None:
|
|
832
|
-
calculation = calculation.lstrip("=") # Remove '=' at the start of the calculation
|
|
833
|
-
try:
|
|
834
|
-
if column_values:
|
|
835
|
-
# Replace column names with their numeric values in the calculation string.
|
|
836
|
-
# Regex ensures COL1 does not accidentally match COL10, etc.
|
|
837
|
-
escaped_cols = [re.escape(col) for col in column_values]
|
|
838
|
-
pattern = re.compile(rf"\b({'|'.join(escaped_cols)})\b")
|
|
839
|
-
|
|
840
|
-
def repl(match: re.Match) -> str:
|
|
841
|
-
col = match.group(0)
|
|
842
|
-
return str(column_values.get(col, match.group(0)))
|
|
843
|
-
|
|
844
|
-
calculation = pattern.sub(repl, calculation)
|
|
845
|
-
|
|
846
|
-
calculation = calculation.replace(
|
|
847
|
-
"^", "**"
|
|
848
|
-
) # Replace '^' with '**' for exponentiation
|
|
849
|
-
# Evaluate the resulting expression
|
|
850
|
-
return eval(calculation)
|
|
851
|
-
except Exception as e:
|
|
852
|
-
logger.info(
|
|
853
|
-
f"Error evaluating calculation '{calculation}': {e}. Likely do not have all values needed."
|
|
854
|
-
)
|
|
855
|
-
return None
|
|
856
|
-
|
|
857
|
-
def _generate_data_patch_payload(self, *, trial: Trial) -> list[PropertyDataPatchDatum]:
|
|
858
|
-
column_values = {
|
|
859
|
-
col.sequence: col.property_data.value
|
|
860
|
-
for col in trial.data_columns
|
|
861
|
-
if col.property_data is not None
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
patch_data = []
|
|
865
|
-
for column in trial.data_columns:
|
|
866
|
-
if column.calculation:
|
|
867
|
-
# Evaluate the recalculated value
|
|
868
|
-
recalculated_value = self._evaluate_calculation(
|
|
869
|
-
calculation=column.calculation, column_values=column_values
|
|
870
|
-
)
|
|
871
|
-
if recalculated_value is not None:
|
|
872
|
-
# Determine whether this is an ADD or UPDATE operation
|
|
873
|
-
if column.property_data.value is None: # No existing value
|
|
874
|
-
patch_data.append(
|
|
875
|
-
PropertyDataPatchDatum(
|
|
876
|
-
id=column.property_data.id,
|
|
877
|
-
operation=PatchOperation.ADD,
|
|
878
|
-
attribute="value",
|
|
879
|
-
new_value=recalculated_value,
|
|
880
|
-
old_value=None,
|
|
881
|
-
)
|
|
882
|
-
)
|
|
883
|
-
elif str(column.property_data.value) != str(
|
|
884
|
-
recalculated_value
|
|
885
|
-
): # Existing value differs
|
|
886
|
-
patch_data.append(
|
|
887
|
-
PropertyDataPatchDatum(
|
|
888
|
-
id=column.property_data.id,
|
|
889
|
-
operation=PatchOperation.UPDATE,
|
|
890
|
-
attribute="value",
|
|
891
|
-
new_value=recalculated_value,
|
|
892
|
-
old_value=column.property_data.value,
|
|
893
|
-
)
|
|
894
|
-
)
|
|
895
|
-
|
|
896
|
-
return patch_data
|
|
897
|
-
|
|
898
1002
|
@validate_call
|
|
899
1003
|
def search(
|
|
900
1004
|
self,
|