diff --git a/qcfractal/qcfractal/components/gridoptimization/test_record_client.py b/qcfractal/qcfractal/components/gridoptimization/test_record_client.py index a4321ac3a..ecd60f5c9 100644 --- a/qcfractal/qcfractal/components/gridoptimization/test_record_client.py +++ b/qcfractal/qcfractal/components/gridoptimization/test_record_client.py @@ -85,6 +85,9 @@ def test_gridoptimization_client_add_get( assert r.record_type == "gridoptimization" assert compare_gridoptimization_specs(spec, r.specification) + assert r.status == RecordStatusEnum.waiting + assert r.children_status == {} + assert r.service.tag == "tag1" assert r.service.priority == PriorityEnum.low @@ -195,6 +198,8 @@ def test_gridoptimization_client_delete(snowflake: QCATestingSnowflake): child_recs = snowflake_client.get_records(child_ids, missing_ok=True) assert all(x.status == RecordStatusEnum.complete for x in child_recs) + go_rec = snowflake_client.get_records(go_id) + assert go_rec.children_status == {RecordStatusEnum.complete: len(child_ids)} snowflake_client.undelete_records(go_id) @@ -205,6 +210,8 @@ def test_gridoptimization_client_delete(snowflake: QCATestingSnowflake): child_recs = snowflake_client.get_records(child_ids, missing_ok=True) assert all(x.status == RecordStatusEnum.deleted for x in child_recs) + go_rec = snowflake_client.get_records(go_id) + assert go_rec.children_status == {RecordStatusEnum.deleted: len(child_ids)} meta = snowflake_client.delete_records(go_id, soft_delete=False, delete_children=True) assert meta.success diff --git a/qcfractal/qcfractal/components/manybody/test_record_client.py b/qcfractal/qcfractal/components/manybody/test_record_client.py index 458db754d..68816e7e3 100644 --- a/qcfractal/qcfractal/components/manybody/test_record_client.py +++ b/qcfractal/qcfractal/components/manybody/test_record_client.py @@ -69,6 +69,9 @@ def test_manybody_client_add_get( assert r.record_type == "manybody" assert compare_manybody_specs(spec, r.specification) + assert r.status == RecordStatusEnum.waiting + assert r.children_status == {} + assert r.service.tag == "tag1" assert r.service.priority == PriorityEnum.low @@ -177,6 +180,8 @@ def test_manybody_client_delete(snowflake: QCATestingSnowflake): child_recs = snowflake_client.get_records(child_ids, missing_ok=True) assert all(x.status == RecordStatusEnum.complete for x in child_recs) + mb_rec = snowflake_client.get_records(mb_id) + assert mb_rec.children_status == {RecordStatusEnum.complete: len(child_ids)} snowflake_client.undelete_records(mb_id) @@ -187,6 +192,8 @@ def test_manybody_client_delete(snowflake: QCATestingSnowflake): child_recs = snowflake_client.get_records(child_ids, missing_ok=True) assert all(x.status == RecordStatusEnum.deleted for x in child_recs) + mb_rec = snowflake_client.get_records(mb_id) + assert mb_rec.children_status == {RecordStatusEnum.deleted: len(child_ids)} meta = snowflake_client.delete_records(mb_id, soft_delete=False, delete_children=True) assert meta.success diff --git a/qcfractal/qcfractal/components/neb/test_record_client.py b/qcfractal/qcfractal/components/neb/test_record_client.py index 237079af2..02bff7c61 100644 --- a/qcfractal/qcfractal/components/neb/test_record_client.py +++ b/qcfractal/qcfractal/components/neb/test_record_client.py @@ -81,6 +81,9 @@ def test_neb_client_add_get(submitter_client: PortalClient, spec: NEBSpecificati assert r.record_type == "neb" assert compare_neb_specs(spec, r.specification) + assert r.status == RecordStatusEnum.waiting + assert r.children_status == {} + assert r.service.tag == "tag1" assert r.service.priority == PriorityEnum.low @@ -180,15 +183,17 @@ def test_neb_client_delete(snowflake: QCATestingSnowflake): rec = session.get(NEBRecordORM, neb_id) # Children are singlepoints, optimizations, and the trajectory of the optimizations (also singlepoints) - child_ids = [x.singlepoint_id for x in rec.singlepoints] + direct_child_ids = [x.singlepoint_id for x in rec.singlepoints] opt_ids = [x.optimization_id for x in rec.optimizations] - child_ids.extend(opt_ids) + direct_child_ids.extend(opt_ids) + child_ids = direct_child_ids.copy() for opt in rec.optimizations: traj_ids = [x.singlepoint_id for x in opt.optimization_record.trajectory] child_ids.extend(traj_ids) # Some duplicates here + direct_child_ids = list(set(direct_child_ids)) child_ids = list(set(child_ids)) meta = snowflake_client.delete_records(neb_id, soft_delete=True, delete_children=False) @@ -196,9 +201,10 @@ def test_neb_client_delete(snowflake: QCATestingSnowflake): assert meta.deleted_idx == [0] assert meta.n_children_deleted == 0 - for cid in child_ids: - child_rec = snowflake_client.get_records(cid, missing_ok=True) - assert child_rec.status == RecordStatusEnum.complete + child_recs = snowflake_client.get_records(child_ids, missing_ok=True) + assert all(x.status == RecordStatusEnum.complete for x in child_recs) + neb_rec = snowflake_client.get_records(neb_id) + assert neb_rec.children_status == {RecordStatusEnum.complete: len(direct_child_ids)} snowflake_client.undelete_records(neb_id) @@ -207,9 +213,10 @@ def test_neb_client_delete(snowflake: QCATestingSnowflake): assert meta.deleted_idx == [0] assert meta.n_children_deleted == len(child_ids) - for cid in child_ids: - child_rec = snowflake_client.get_records(cid, missing_ok=True) - assert child_rec.status == RecordStatusEnum.deleted + child_recs = snowflake_client.get_records(child_ids, missing_ok=True) + assert all(x.status == RecordStatusEnum.deleted for x in child_recs) + neb_rec = snowflake_client.get_records(neb_id) + assert neb_rec.children_status == {RecordStatusEnum.deleted: len(direct_child_ids)} meta = snowflake_client.delete_records(neb_id, soft_delete=False, delete_children=True) assert meta.success @@ -219,9 +226,8 @@ def test_neb_client_delete(snowflake: QCATestingSnowflake): recs = snowflake_client.get_nebs(neb_id, missing_ok=True) assert recs is None - for cid in child_ids: - child_rec = snowflake_client.get_records(cid, missing_ok=True) - assert child_rec is None + child_recs = snowflake_client.get_records(child_ids, missing_ok=True) + assert all(x is None for x in child_recs) # DB should be pretty empty now query_res = snowflake_client.query_records() diff --git a/qcfractal/qcfractal/components/optimization/test_record_client.py b/qcfractal/qcfractal/components/optimization/test_record_client.py index 26650fce5..88f7bbcfd 100644 --- a/qcfractal/qcfractal/components/optimization/test_record_client.py +++ b/qcfractal/qcfractal/components/optimization/test_record_client.py @@ -83,6 +83,9 @@ def test_optimization_client_add_get( assert r.record_type == "optimization" assert compare_optimization_specs(spec, r.specification) + assert r.status == RecordStatusEnum.waiting + assert r.children_status == {} + assert r.task.function is None assert r.task.tag == "tag1" assert r.task.priority == PriorityEnum.low @@ -198,6 +201,9 @@ def test_optimization_client_delete(snowflake: QCATestingSnowflake, opt_file: st child_recs = snowflake_client.get_records(child_ids, missing_ok=True) assert all(x.status == RecordStatusEnum.complete for x in child_recs) + opt_rec = snowflake_client.get_records(opt_id) + if child_ids: + assert opt_rec.children_status == {RecordStatusEnum.complete: len(child_ids)} # Undo what we just did snowflake_client.undelete_records(opt_id) @@ -209,6 +215,9 @@ def test_optimization_client_delete(snowflake: QCATestingSnowflake, opt_file: st child_recs = snowflake_client.get_records(child_ids, missing_ok=True) assert all(x.status == RecordStatusEnum.deleted for x in child_recs) + opt_rec = snowflake_client.get_records(opt_id) + if child_ids: + assert opt_rec.children_status == {RecordStatusEnum.deleted: len(child_ids)} meta = snowflake_client.delete_records(opt_id, soft_delete=False, delete_children=True) assert meta.success diff --git a/qcfractal/qcfractal/components/reaction/test_record_client.py b/qcfractal/qcfractal/components/reaction/test_record_client.py index a21297457..c47c81188 100644 --- a/qcfractal/qcfractal/components/reaction/test_record_client.py +++ b/qcfractal/qcfractal/components/reaction/test_record_client.py @@ -77,6 +77,9 @@ def test_reaction_client_add_get( assert r.record_type == "reaction" assert compare_reaction_specs(spec, r.specification) + assert r.status == RecordStatusEnum.waiting + assert r.children_status == {} + assert r.service.tag == "tag1" assert r.service.priority == PriorityEnum.low @@ -205,6 +208,8 @@ def test_reaction_client_delete(snowflake: QCATestingSnowflake): child_recs = snowflake_client.get_records(child_ids, missing_ok=True) assert all(x.status == RecordStatusEnum.complete for x in child_recs) + rxn_rec = snowflake_client.get_records(rxn_id) + assert rxn_rec.children_status == {RecordStatusEnum.complete: len(child_ids)} snowflake_client.undelete_records(rxn_id) @@ -215,6 +220,8 @@ def test_reaction_client_delete(snowflake: QCATestingSnowflake): child_recs = snowflake_client.get_records(child_ids, missing_ok=True) assert all(x.status == RecordStatusEnum.deleted for x in child_recs) + rxn_rec = snowflake_client.get_records(rxn_id) + assert rxn_rec.children_status == {RecordStatusEnum.deleted: len(child_ids)} meta = snowflake_client.delete_records(rxn_id, soft_delete=False, delete_children=True) assert meta.success diff --git a/qcfractal/qcfractal/components/record_routes.py b/qcfractal/qcfractal/components/record_routes.py index ec66d796a..303dfa08b 100644 --- a/qcfractal/qcfractal/components/record_routes.py +++ b/qcfractal/qcfractal/components/record_routes.py @@ -197,3 +197,10 @@ def get_record_native_file_single_v1(record_id: int, name: str, record_type: Opt def get_record_native_file_data_v1(record_id: int, name: str, record_type: Optional[str] = None): record_socket = storage_socket.records.get_socket(record_type) return record_socket.get_single_native_file_rawdata(record_id, name) + + +@api_v1.route("/records///children_status", methods=["GET"]) +@wrap_route("READ") +def get_record_children_status_v1(record_id: int, record_type: Optional[str] = None): + record_socket = storage_socket.records.get_socket(record_type) + return record_socket.get_children_status(record_id) diff --git a/qcfractal/qcfractal/components/record_socket.py b/qcfractal/qcfractal/components/record_socket.py index 17e6c773a..edbe8faa2 100644 --- a/qcfractal/qcfractal/components/record_socket.py +++ b/qcfractal/qcfractal/components/record_socket.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING from qcelemental.models import FailedOperation -from sqlalchemy import select, union, or_ +from sqlalchemy import select, union, or_, func from sqlalchemy.orm import ( joinedload, selectinload, @@ -453,6 +453,25 @@ def get_single_native_file_rawdata( return nf_data[0], nf_data[1] + def get_children_status(self, record_id: int, *, session: Optional[Session] = None) -> Dict[RecordStatusEnum, int]: + + # Get the SQL 'select' statements from the handlers + select_stmts = self.get_children_select() + + if not select_stmts: + return {} + + select_cte = union(*select_stmts).cte() + + stmt = select(BaseRecordORM.status, func.count()) + stmt = stmt.join(select_cte, select_cte.c.child_id == BaseRecordORM.id) + stmt = stmt.where(select_cte.c.parent_id == record_id) + stmt = stmt.group_by(BaseRecordORM.status) + + with self.root_socket.optional_session(session, True) as session: + res = session.execute(stmt).all() + return {x: y for x, y in res} + class RecordSocket: """ diff --git a/qcfractal/qcfractal/components/services/test_socket.py b/qcfractal/qcfractal/components/services/test_socket.py index 7dd8a0cd5..fb17f684e 100644 --- a/qcfractal/qcfractal/components/services/test_socket.py +++ b/qcfractal/qcfractal/components/services/test_socket.py @@ -43,6 +43,9 @@ def test_service_socket_error(storage_socket: SQLAlchemySocket, session: Session rec = session.get(BaseRecordORM, id_1) assert rec.status == RecordStatusEnum.error + + child_stat = storage_socket.records.torsiondrive.get_children_status(id_1, session=session) + assert child_stat[RecordStatusEnum.error] == 1 assert len(rec.compute_history) == 1 assert len(rec.compute_history[-1].outputs) == 2 # stdout and error assert rec.compute_history[-1].status == RecordStatusEnum.error diff --git a/qcfractal/qcfractal/components/singlepoint/test_record_client.py b/qcfractal/qcfractal/components/singlepoint/test_record_client.py index c07d23c5a..81894a53d 100644 --- a/qcfractal/qcfractal/components/singlepoint/test_record_client.py +++ b/qcfractal/qcfractal/components/singlepoint/test_record_client.py @@ -7,7 +7,7 @@ import pytest from qcarchivetesting import load_molecule_data -from qcportal.record_models import PriorityEnum +from qcportal.record_models import PriorityEnum, RecordStatusEnum from qcportal.singlepoint import QCSpecification, SinglepointDriver from .testing_helpers import submit_test_data, run_test_data, compare_singlepoint_specs, test_specs @@ -67,6 +67,10 @@ def test_singlepoint_client_add_get(submitter_client: PortalClient, spec: QCSpec assert r.record_type == "singlepoint" assert r.record_type == "singlepoint" assert compare_singlepoint_specs(spec, r.specification) + + assert r.status == RecordStatusEnum.waiting + assert r.children_status == {} + assert r.task.function is None assert r.task.tag == "tag1" assert r.task.priority == PriorityEnum.high diff --git a/qcfractal/qcfractal/components/torsiondrive/test_record_client.py b/qcfractal/qcfractal/components/torsiondrive/test_record_client.py index f8fdacda6..6b3e949d1 100644 --- a/qcfractal/qcfractal/components/torsiondrive/test_record_client.py +++ b/qcfractal/qcfractal/components/torsiondrive/test_record_client.py @@ -77,6 +77,9 @@ def test_torsiondrive_client_add_get( assert r.record_type == "torsiondrive" assert compare_torsiondrive_specs(spec, r.specification) + assert r.status == RecordStatusEnum.waiting + assert r.children_status == {} + assert r.service.tag == "tag1" assert r.service.priority == PriorityEnum.low @@ -195,6 +198,8 @@ def test_torsiondrive_client_delete(snowflake: QCATestingSnowflake): child_recs = snowflake_client.get_records(child_ids, missing_ok=True) assert all(x.status == RecordStatusEnum.complete for x in child_recs) + td_rec = snowflake_client.get_records(td_id) + assert td_rec.children_status == {RecordStatusEnum.complete: len(child_ids)} snowflake_client.undelete_records(td_id) @@ -205,6 +210,8 @@ def test_torsiondrive_client_delete(snowflake: QCATestingSnowflake): child_recs = snowflake_client.get_records(child_ids, missing_ok=True) assert all(x.status == RecordStatusEnum.deleted for x in child_recs) + td_rec = snowflake_client.get_records(td_id) + assert td_rec.children_status == {RecordStatusEnum.deleted: len(child_ids)} meta = snowflake_client.delete_records(td_id, soft_delete=False, delete_children=True) assert meta.success diff --git a/qcportal/qcportal/record_models.py b/qcportal/qcportal/record_models.py index 22d6da826..821f6b338 100644 --- a/qcportal/qcportal/record_models.py +++ b/qcportal/qcportal/record_models.py @@ -525,6 +525,17 @@ def _handle_includes(self, includes: Optional[Iterable[str]]): def offline(self) -> bool: return self._client is None + @property + def children_status(self) -> Dict[RecordStatusEnum, int]: + """Returns a dicionary of the status of all children of this record""" + self._assert_online() + + return self._client.make_request( + "get", + f"{self._base_url}/children_status", + Dict[RecordStatusEnum, int], + ) + @property def compute_history(self) -> List[ComputeHistory]: if self.compute_history_ is None: