Skip to content

Commit 856f921

Browse files
committed
Release Bloom 8.0.0 with TapDB 9.0.10
1 parent e9961c9 commit 856f921

42 files changed

Lines changed: 735 additions & 324 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
Bloom is the LSMC internal material and container graph service. It owns laboratory containers, materials/content, equipment, lineage, recursive template creation, search, and graph views. Atlas owns orders and customer-facing accession context; Bloom owns the physical/material execution graph.
2323

24-
Current Dayhoff pin: `7.0.22`. Current TapDB dependency: `daylily-tapdb @ ...@9.0.9`.
24+
Current Dayhoff pin: `8.0.0`. Current TapDB dependency: `daylily-tapdb @ ...@9.0.10`.
2525

2626
Bloom is internal-only in Dayhoff exposure policy. It must be reachable only through approved LSMC networks and Dayhoff-generated service credentials.
2727

bloom_lims/ai_agent_access.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,13 @@ def _endpoint_id_for_request(method: str, path: str) -> str:
7878
resolved_method = str(method or "").upper()
7979
resolved_path = str(path or "").split("?", 1)[0].rstrip("/") or "/"
8080
for spec in ENDPOINT_CATALOG:
81-
if spec.method == resolved_method and _template_matches(spec.path_template, resolved_path):
81+
if spec.method == resolved_method and _template_matches(
82+
spec.path_template, resolved_path
83+
):
8284
return spec.endpoint_id
83-
raise AgentTokenError("AI-agent token is not authorized for this endpoint", status_code=403)
85+
raise AgentTokenError(
86+
"AI-agent token is not authorized for this endpoint", status_code=403
87+
)
8488

8589

8690
def _parse_expiry(value: str) -> datetime:
@@ -95,15 +99,21 @@ def _parse_expiry(value: str) -> datetime:
9599

96100
def _load_grants(path: Path) -> list[dict[str, Any]]:
97101
if not path.is_absolute():
98-
raise AgentTokenError("AI-agent grant store path must be absolute", status_code=500)
102+
raise AgentTokenError(
103+
"AI-agent grant store path must be absolute", status_code=500
104+
)
99105
if not path.exists():
100106
raise AgentTokenError("AI-agent grant store is missing", status_code=500)
101107
try:
102108
payload = json.loads(path.read_text(encoding="utf-8"))
103109
except json.JSONDecodeError as exc:
104-
raise AgentTokenError("AI-agent grant store is malformed", status_code=500) from exc
110+
raise AgentTokenError(
111+
"AI-agent grant store is malformed", status_code=500
112+
) from exc
105113
if not isinstance(payload, dict) or not isinstance(payload.get("tokens"), list):
106-
raise AgentTokenError("AI-agent grant store must contain a tokens list", status_code=500)
114+
raise AgentTokenError(
115+
"AI-agent grant store must contain a tokens list", status_code=500
116+
)
107117
return [record for record in payload["tokens"] if isinstance(record, dict)]
108118

109119

@@ -119,7 +129,9 @@ def validate_ai_agent_request(request: Request, token: str) -> ValidatedAgentAcc
119129
endpoint_id = _endpoint_id_for_request(request.method, request.url.path)
120130
token_digest = hashlib.sha256(token.encode("utf-8")).hexdigest()
121131
for record in _load_grants(Path(raw_path)):
122-
if not secrets.compare_digest(str(record.get("token_hash") or ""), token_digest):
132+
if not secrets.compare_digest(
133+
str(record.get("token_hash") or ""), token_digest
134+
):
123135
continue
124136
if record.get("revoked_at"):
125137
raise AgentTokenError("AI-agent token has been revoked")
@@ -128,7 +140,9 @@ def validate_ai_agent_request(request: Request, token: str) -> ValidatedAgentAcc
128140
raise AgentTokenError("AI-agent token has expired")
129141
endpoint_ids = [str(item) for item in record.get("endpoint_ids") or []]
130142
if endpoint_id not in endpoint_ids:
131-
raise AgentTokenError("AI-agent token is not authorized for this endpoint", status_code=403)
143+
raise AgentTokenError(
144+
"AI-agent token is not authorized for this endpoint", status_code=403
145+
)
132146
validated = ValidatedAgentAccess(
133147
token_id=str(record.get("token_id") or ""),
134148
agent_id=str(record.get("agent_id") or ""),

bloom_lims/api/v1/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
from .auth import router as auth_router
2121
from .batch import router as batch_router
2222
from .beta_lab import router as beta_lab_router
23-
from .containers import router as containers_router
2423
from .container_actions import router as container_actions_router
24+
from .containers import router as containers_router
2525
from .content import router as content_router
2626
from .equipment import router as equipment_router
2727
from .execution_queue import router as execution_queue_router

bloom_lims/api/v1/auth.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,13 @@ def _broker_preferences_contract(email: str) -> tuple[str, dict[str, str]]:
2525
token = str(os.environ.get("LSMC_AUTH_BROKER_SERVICE_TOKEN") or "").strip()
2626
service_id = str(os.environ.get("LSMC_AUTH_BROKER_SERVICE_ID") or "bloom").strip()
2727
if not raw_url:
28-
raise HTTPException(status_code=503, detail="Broker user preferences URL is not configured")
28+
raise HTTPException(
29+
status_code=503, detail="Broker user preferences URL is not configured"
30+
)
2931
if not token:
30-
raise HTTPException(status_code=503, detail="Broker service token is not configured")
32+
raise HTTPException(
33+
status_code=503, detail="Broker service token is not configured"
34+
)
3135
if "{email}" not in raw_url:
3236
raise HTTPException(
3337
status_code=503,
@@ -58,7 +62,9 @@ async def get_current_user(user: APIUser = Depends(require_api_auth)):
5862
@preferences_router.get("/me/preferences")
5963
async def current_user_preferences(user: APIUser = Depends(require_api_auth)):
6064
if not user.email:
61-
raise HTTPException(status_code=400, detail="Authenticated user email is required")
65+
raise HTTPException(
66+
status_code=400, detail="Authenticated user email is required"
67+
)
6268
url, headers = _broker_preferences_contract(user.email)
6369
with httpx.Client(timeout=5.0) as client:
6470
response = client.get(url, headers=headers)
@@ -73,17 +79,24 @@ async def update_current_user_preferences(
7379
user: APIUser = Depends(require_api_auth),
7480
):
7581
if not user.email:
76-
raise HTTPException(status_code=400, detail="Authenticated user email is required")
82+
raise HTTPException(
83+
status_code=400, detail="Authenticated user email is required"
84+
)
7785
payload = await request.json()
7886
theme = str(payload.get("theme") or "").strip()
7987
if theme and theme not in THEME_NAMES:
8088
raise HTTPException(status_code=400, detail="Unknown theme")
8189
service_themes = payload.get("service_themes")
8290
if service_themes is not None:
8391
if not isinstance(service_themes, dict):
84-
raise HTTPException(status_code=400, detail="service_themes must be an object")
92+
raise HTTPException(
93+
status_code=400, detail="service_themes must be an object"
94+
)
8595
for service_theme in service_themes.values():
86-
if service_theme is not None and str(service_theme).strip() not in THEME_NAMES:
96+
if (
97+
service_theme is not None
98+
and str(service_theme).strip() not in THEME_NAMES
99+
):
87100
raise HTTPException(status_code=400, detail="Unknown theme")
88101
forward_payload = {}
89102
if "theme" in payload:

bloom_lims/api/v1/container_actions.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from pydantic import BaseModel, Field, ValidationError
1111
from sqlalchemy import select
1212

13-
from bloom_lims.tapdb_adapter import BLOOMdb3, generic_instance
1413
from bloom_lims.domain.lab_actions import LabActionsService
1514
from bloom_lims.schemas.lab_actions import (
1615
ExtractionPlateRequest,
@@ -22,6 +21,7 @@
2221
SeqLibraryPoolRequest,
2322
SeqRunSetRequest,
2423
)
24+
from bloom_lims.tapdb_adapter import BLOOMdb3, generic_instance
2525

2626
from .dependencies import APIUser, require_write
2727

@@ -76,8 +76,12 @@ def _split_source(value: str) -> tuple[str, str | None]:
7676
return euid, well
7777

7878

79-
def _normalize_record(row: dict[str, Any], default_target: str | None) -> dict[str, Any]:
80-
source = row.get("source") or row.get("source_euid") or row.get("euid") or row.get("0")
79+
def _normalize_record(
80+
row: dict[str, Any], default_target: str | None
81+
) -> dict[str, Any]:
82+
source = (
83+
row.get("source") or row.get("source_euid") or row.get("euid") or row.get("0")
84+
)
8185
target = (
8286
row.get("target")
8387
or row.get("target_container_euid")
@@ -159,7 +163,10 @@ async def validate_container_action(
159163
):
160164
try:
161165
planned = _parse_rows(payload)
162-
source_types = {row["source_euid"]: _object_kind(row["source_euid"], user) for row in planned}
166+
source_types = {
167+
row["source_euid"]: _object_kind(row["source_euid"], user)
168+
for row in planned
169+
}
163170
target_types = {
164171
row["target_euid"]: _object_kind(row["target_euid"], user)
165172
for row in planned
@@ -232,9 +239,13 @@ async def execute_container_action(
232239
SeqRunSetRequest.model_validate(operation_payload)
233240
)
234241
elif payload.operation_type == "lab_set":
235-
result = service.create_lab_set(LabSetRequest.model_validate(operation_payload))
242+
result = service.create_lab_set(
243+
LabSetRequest.model_validate(operation_payload)
244+
)
236245
elif payload.operation_type == "print_euids":
237-
result = service.print_euids(PrintEuidRequest.model_validate(operation_payload))
246+
result = service.print_euids(
247+
PrintEuidRequest.model_validate(operation_payload)
248+
)
238249
else: # pragma: no cover - Literal validation should prevent this.
239250
raise HTTPException(status_code=400, detail="Unsupported operation_type")
240251
return {

bloom_lims/api/v1/lab_actions.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,13 @@ async def download_seq_run_sample_sheet(
177177
):
178178
service = _service_for_user(user)
179179
try:
180-
content, filename, media_type = service.sequencing_sample_sheet_download(set_euid)
180+
content, filename, media_type = service.sequencing_sample_sheet_download(
181+
set_euid
182+
)
181183
return PlainTextResponse(
182184
content,
183185
media_type=media_type,
184-
headers={
185-
"Content-Disposition": f'attachment; filename="{filename}"'
186-
},
186+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
187187
)
188188
except Exception as exc:
189189
_raise_http(exc)

bloom_lims/cli/db.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,20 @@ def _retire_obsolete_template_variants(
383383
domain_code: str,
384384
) -> int:
385385
current = _active_template_categories_by_semantic_key(templates)
386+
current_semantic_types = {
387+
(
388+
str((template.get("json_addl") or {}).get("semantic_category") or "")
389+
.strip()
390+
.lower(),
391+
str(template.get("type") or "").strip(),
392+
)
393+
for template in templates
394+
if isinstance(template.get("json_addl"), dict)
395+
and str(
396+
(template.get("json_addl") or {}).get("semantic_category") or ""
397+
).strip()
398+
and str(template.get("type") or "").strip()
399+
}
386400
current_prefixes = {
387401
str(template.get("instance_prefix") or "").strip().upper()
388402
for template in templates
@@ -411,18 +425,16 @@ def _retire_obsolete_template_variants(
411425
)
412426
expected_category = current.get(key)
413427
actual_category = str(row.category or "").strip().upper()
414-
semantic_category = template_semantic_category(row).strip().upper()
415-
if semantic_category and actual_category != semantic_category:
416-
row.is_deleted = True
417-
row.bstatus = "retired"
418-
retired += 1
419-
continue
420428
if actual_category in current_prefixes:
421429
row.is_deleted = True
422430
row.bstatus = "retired"
423431
retired += 1
424432
continue
425433
if expected_category is None:
434+
if (key[0], key[1]) in current_semantic_types:
435+
row.is_deleted = True
436+
row.bstatus = "retired"
437+
retired += 1
426438
continue
427439
if actual_category == expected_category:
428440
continue

bloom_lims/domain/beta_actions.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,7 @@ def record(
187187
return action_record
188188

189189
def _ensure_action_template(self, action_key: str) -> generic_template:
190-
template_code = (
191-
f"{BETA_ACTION_TEMPLATE_PREFIX}/{BETA_ACTION_GROUP}/{action_key}/1.0/"
192-
)
190+
template_code = f"action/{BETA_ACTION_GROUP}/{action_key}/1.0/"
193191
return require_seeded_template(
194192
self.session,
195193
template_code,

bloom_lims/domain/beta_lab_refs.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,7 @@ def _fulfillment_item_refs_for_instance(self, instance) -> list[dict[str, str]]:
172172
).strip()
173173
atlas_tenant_id = str(payload.get("atlas_tenant_id") or "").strip()
174174
atlas_trf_euid = str(
175-
payload.get("atlas_trf_euid")
176-
or payload.get("atlas_order_euid")
177-
or ""
175+
payload.get("atlas_trf_euid") or payload.get("atlas_order_euid") or ""
178176
).strip()
179177
if not (
180178
fulfillment_item_euid
@@ -195,8 +193,7 @@ def _fulfillment_item_refs_for_instance(self, instance) -> list[dict[str, str]]:
195193
payload.get("atlas_order_test_euid") or atlas_test_euid
196194
).strip(),
197195
"atlas_fulfillment_slot_euid": str(
198-
payload.get("atlas_fulfillment_slot_euid")
199-
or fulfillment_item_euid
196+
payload.get("atlas_fulfillment_slot_euid") or fulfillment_item_euid
200197
).strip(),
201198
}
202199
return list(refs.values())
@@ -366,7 +363,9 @@ def _replace_fulfillment_item_references(
366363
atlas_tenant_id = str(atlas_context.get("atlas_tenant_id") or "").strip()
367364
atlas_trf_euid = str(atlas_context.get("atlas_trf_euid") or "").strip()
368365
atlas_order_euid = str(
369-
atlas_context.get("atlas_order_euid") or atlas_context.get("order_euid") or ""
366+
atlas_context.get("atlas_order_euid")
367+
or atlas_context.get("order_euid")
368+
or ""
370369
).strip()
371370
fulfillment_items = list(atlas_context.get("fulfillment_items") or [])
372371
for fulfillment_slot in list(atlas_context.get("fulfillment_slots") or []):
@@ -464,8 +463,7 @@ def _replace_fulfillment_item_references(
464463
object_evidence(ref_obj.euid, role="atlas_reference_link"),
465464
],
466465
correlation_id=(
467-
f"SLOT_SATISFIED_BY:{atlas_fulfillment_slot_euid}:"
468-
f"{instance.euid}"
466+
f"SLOT_SATISFIED_BY:{atlas_fulfillment_slot_euid}:{instance.euid}"
469467
),
470468
causation_id=(
471469
f"bloom:atlas_fulfillment_reference:"
@@ -484,7 +482,9 @@ def _replace_container_entity_references(
484482
) -> None:
485483
atlas_tenant_id = str(atlas_context.get("atlas_tenant_id") or "").strip()
486484
atlas_order_euid = str(
487-
atlas_context.get("atlas_order_euid") or atlas_context.get("order_euid") or ""
485+
atlas_context.get("atlas_order_euid")
486+
or atlas_context.get("order_euid")
487+
or ""
488488
).strip()
489489
atlas_order_test_euid = str(
490490
atlas_context.get("atlas_order_test_euid")

bloom_lims/domain/beta_lab_stages.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,9 +409,7 @@ def record_post_extract_qc(
409409
"idempotency_key": idempotency_key or "",
410410
"occurred_at": self._timestamp(),
411411
},
412-
template_code=self.DATA_TEMPLATE_BY_BETA_KIND.get(
413-
"post_extract_qc_result"
414-
),
412+
template_code=self.DATA_TEMPLATE_BY_BETA_KIND.get("post_extract_qc_result"),
415413
)
416414
if payload.quant_artifact_euid:
417415
qc_props = self._props(qc_record)

0 commit comments

Comments
 (0)