From daa2a3f781642a80ef9ce3ee9c9e5a72479ae4f0 Mon Sep 17 00:00:00 2001 From: Alex Konstantinov Date: Thu, 18 Jun 2026 14:51:32 +0100 Subject: [PATCH 1/2] Add public OneDep session metadata API --- src/onedep_lib/__init__.py | 15 +++++- src/onedep_lib/dsp.py | 74 +++++++++++++++++++++++++--- src/onedep_lib/session/models.py | 14 ++++++ tests/unit/test_deposition_facade.py | 58 +++++++++++++++++++++- 4 files changed, 152 insertions(+), 9 deletions(-) diff --git a/src/onedep_lib/__init__.py b/src/onedep_lib/__init__.py index 06aa4ff..b3b4fee 100644 --- a/src/onedep_lib/__init__.py +++ b/src/onedep_lib/__init__.py @@ -6,15 +6,28 @@ from onedep_lib.auths.token import TokenStore from onedep_lib.auths.types import AuthProvider from onedep_lib.checks.report import CheckIssue, CheckReport, CheckSeverity, CifLocation -from onedep_lib.dsp import Deposition, deposit_init, deposit_resume, list_sessions +from onedep_lib.dsp import ( + Deposition, + deposit_init, + deposit_resume, + get_session, + get_session_metadata, + list_session_metadata, + list_sessions, +) from onedep_lib.enums import Country, EMSubType, ExperimentType, FileType from onedep_lib.exceptions import ApiError, DepositApiException, OneDepError +from onedep_lib.session.models import SessionSummary __all__ = [ # factories / facade "deposit_init", "deposit_resume", + "get_session", + "get_session_metadata", + "list_session_metadata", "list_sessions", + "SessionSummary", "Deposition", # check result types "CheckReport", diff --git a/src/onedep_lib/dsp.py b/src/onedep_lib/dsp.py index e55d9a8..cba8017 100644 --- a/src/onedep_lib/dsp.py +++ b/src/onedep_lib/dsp.py @@ -8,6 +8,7 @@ from onedep_lib.apis.deposit.client import HttpApiClient from onedep_lib.apis.deposit.models import DepositError, DepositStatus, Experiment from onedep_lib.apis.deposit.types import ApiClient +from onedep_lib.auths.token import TokenStore from onedep_lib.checks.report import CheckReport from onedep_lib.checks.runner import CheckRunner from onedep_lib.checks.types import CheckRunner as CheckRunnerProtocol @@ -16,9 +17,8 @@ from onedep_lib.schemas.local import LocalSchemaProvider from onedep_lib.schemas.remote import RemoteSchemaProvider from onedep_lib.session.json_store import JsonSessionStore -from onedep_lib.session.models import LocalFile, LocalSession +from onedep_lib.session.models import LocalFile, LocalSession, SessionSummary from onedep_lib.session.types import SessionStore -from onedep_lib.auths.token import TokenStore def _md5_of_file(path: Path, chunk_size: int = 1 << 20) -> str: @@ -29,6 +29,27 @@ def _md5_of_file(path: Path, chunk_size: int = 1 << 20) -> str: return h.hexdigest() +def _session_base_dir(config: DepositConfig, base_dir: Path | None = None, _base_dir: Path | None = None) -> Path: + if base_dir is not None and _base_dir is not None: + raise ValueError("Specify either base_dir or _base_dir, not both.") + return base_dir or _base_dir or config.session_dir + + +def _summarize_session(session: LocalSession, files: list[LocalFile]) -> SessionSummary: + return SessionSummary( + session_id=session.session_id, + email=session.email, + users=session.users, + country=session.country, + experiment_type=session.experiment_type, + created_at=session.created_at, + remote_dep_id=session.remote_dep_id, + em_subtype=session.em_subtype, + coordinates=session.coordinates, + file_count=len(files), + ) + + def list_sessions(base_dir: Path | None = None) -> list[tuple[LocalSession, list[LocalFile]]]: """Return all local sessions with their registered files, newest first.""" config = DepositConfig.load() @@ -53,6 +74,41 @@ def list_sessions(base_dir: Path | None = None) -> list[tuple[LocalSession, list return results +def list_session_metadata(base_dir: Path | None = None) -> list[SessionSummary]: + """Return metadata summaries for all local sessions, newest first.""" + return [_summarize_session(session, files) for session, files in list_sessions(base_dir=base_dir)] + + +def get_session(session_id: str, base_dir: Path | None = None) -> tuple[LocalSession, list[LocalFile]]: + """Return one local session and its registered files. + + Args: + session_id: Local session UUID. + base_dir: Optional session storage directory. Defaults to the configured + OneDep Lib session directory. + + Raises: + KeyError: If no local session with the given session_id exists. + """ + config = DepositConfig.load() + _base = base_dir or config.session_dir + json_path = _base / session_id / "session.json" + if not json_path.exists(): + raise KeyError(f"No session found for {session_id!r}") + + store = JsonSessionStore(session_id, base_dir=_base) + try: + return store.get_session(), store.get_all_files() + finally: + store.close() + + +def get_session_metadata(session_id: str, base_dir: Path | None = None) -> SessionSummary: + """Return a metadata summary for one local session.""" + session, files = get_session(session_id, base_dir=base_dir) + return _summarize_session(session, files) + + def deposit_init( email: str, users: list[str], @@ -61,6 +117,7 @@ def deposit_init( em_subtype: EMSubType | None = None, coordinates: bool | None = None, config: DepositConfig | None = None, + base_dir: Path | None = None, _base_dir: Path | None = None, _api_client: ApiClient | None = None, _check_runner: CheckRunnerProtocol | None = None, @@ -75,6 +132,8 @@ def deposit_init( em_subtype: EM experiment subtype (can be set later via set_em_params). coordinates: Whether coordinates are being deposited (can be set later). config: Optional pre-built DepositConfig; loaded from default sources if None. + base_dir: Optional session storage directory. Defaults to the configured + OneDep Lib session directory. _base_dir: Override session storage directory (for testing only). _api_client: Override API client (for testing only). _check_runner: Override check runner (for testing only). @@ -84,8 +143,8 @@ def deposit_init( """ config = config or DepositConfig.load() session_id = str(uuid.uuid4()) - base_dir = _base_dir or config.session_dir - store: SessionStore = JsonSessionStore(session_id, base_dir=base_dir) + session_base_dir = _session_base_dir(config, base_dir=base_dir, _base_dir=_base_dir) + store: SessionStore = JsonSessionStore(session_id, base_dir=session_base_dir) api_client: ApiClient = _api_client or HttpApiClient(config, auth_provider=TokenStore(config)) check_runner: CheckRunnerProtocol = _check_runner or CheckRunner( LocalSchemaProvider(config.local_schema_cache_dir) @@ -109,6 +168,7 @@ def deposit_init( def deposit_resume( session_id: str, config: DepositConfig | None = None, + base_dir: Path | None = None, _base_dir: Path | None = None, _api_client: ApiClient | None = None, _check_runner: CheckRunnerProtocol | None = None, @@ -118,6 +178,8 @@ def deposit_resume( Args: session_id: The session_id returned by a previous deposit_init() call. config: Optional pre-built DepositConfig; loaded from default sources if None. + base_dir: Optional session storage directory. Defaults to the configured + OneDep Lib session directory. _base_dir: Override session storage directory (for testing only). _api_client: Override API client (for testing only). _check_runner: Override check runner (for testing only). @@ -129,8 +191,8 @@ def deposit_resume( KeyError: If no session with the given session_id exists. """ config = config or DepositConfig.load() - base_dir = _base_dir or config.session_dir - store: SessionStore = JsonSessionStore(session_id, base_dir=base_dir) + session_base_dir = _session_base_dir(config, base_dir=base_dir, _base_dir=_base_dir) + store: SessionStore = JsonSessionStore(session_id, base_dir=session_base_dir) store.get_session() # raises KeyError if not found api_client: ApiClient = _api_client or HttpApiClient(config, auth_provider=TokenStore(config)) check_runner: CheckRunnerProtocol = _check_runner or CheckRunner( diff --git a/src/onedep_lib/session/models.py b/src/onedep_lib/session/models.py index 5a102c1..3350c71 100644 --- a/src/onedep_lib/session/models.py +++ b/src/onedep_lib/session/models.py @@ -28,3 +28,17 @@ class LocalSession: remote_dep_id: str | None = None em_subtype: EMSubType | None = None coordinates: bool | None = None + + +@dataclass +class SessionSummary: + session_id: str + email: str + users: list[str] + country: Country + experiment_type: ExperimentType | None + created_at: datetime + remote_dep_id: str | None = None + em_subtype: EMSubType | None = None + coordinates: bool | None = None + file_count: int = 0 diff --git a/tests/unit/test_deposition_facade.py b/tests/unit/test_deposition_facade.py index 6b3b12a..3d182b1 100644 --- a/tests/unit/test_deposition_facade.py +++ b/tests/unit/test_deposition_facade.py @@ -1,7 +1,13 @@ import pytest -from pathlib import Path + from onedep_lib.checks.report import CheckReport -from onedep_lib.dsp import deposit_init, deposit_resume +from onedep_lib.dsp import ( + deposit_init, + deposit_resume, + get_session, + get_session_metadata, + list_session_metadata, +) from onedep_lib.enums import Country, ExperimentType, FileType from tests.unit.apis.deposit.test_stub_api_client import StubApiClient @@ -117,6 +123,54 @@ def test_deposit_resume_restores_session(dep, tmp_path, stub_api): resumed.close() +def test_deposit_resume_accepts_public_base_dir(tmp_path, stub_api): + dep = deposit_init( + email="test@example.com", + users=[], + country=Country.USA, + experiment_type=ExperimentType.XRAY, + base_dir=tmp_path, + _api_client=stub_api, + _check_runner=StubCheckRunner(), + ) + session_id = dep.session_id + dep.close() + + resumed = deposit_resume( + session_id, + base_dir=tmp_path, + _api_client=stub_api, + _check_runner=StubCheckRunner(), + ) + assert resumed.session_id == session_id + resumed.close() + + +def test_session_metadata_entry_points(dep, tmp_path): + test_file = tmp_path / "coords.cif" + test_file.write_text("data_test") + file_id = dep.add_file(str(test_file), FileType.MMCIF_COORD) + session_id = dep.session_id + dep.close() + + session, files = get_session(session_id, base_dir=tmp_path) + assert session.session_id == session_id + assert session.email == "test@example.com" + assert [file.file_id for file in files] == [file_id] + + summary = get_session_metadata(session_id, base_dir=tmp_path) + assert summary.session_id == session_id + assert summary.email == "test@example.com" + assert summary.country == Country.USA + assert summary.experiment_type == ExperimentType.XRAY + assert summary.file_count == 1 + + summaries = list_session_metadata(base_dir=tmp_path) + assert len(summaries) == 1 + assert summaries[0].session_id == session_id + assert summaries[0].file_count == 1 + + def test_context_manager(tmp_path, stub_api): with deposit_init( email="test@example.com", From 7c051cadea41029579cddffc631a8f831ed28786 Mon Sep 17 00:00:00 2001 From: Alex Konstantinov Date: Thu, 18 Jun 2026 17:48:54 +0100 Subject: [PATCH 2/2] Expose session base_dir API --- src/onedep_lib/dsp.py | 29 ++++++++++++---------------- src/onedep_lib/session/models.py | 11 ++++------- tests/unit/test_deposition_facade.py | 12 ++++++------ 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/onedep_lib/dsp.py b/src/onedep_lib/dsp.py index cba8017..2885eb5 100644 --- a/src/onedep_lib/dsp.py +++ b/src/onedep_lib/dsp.py @@ -29,23 +29,17 @@ def _md5_of_file(path: Path, chunk_size: int = 1 << 20) -> str: return h.hexdigest() -def _session_base_dir(config: DepositConfig, base_dir: Path | None = None, _base_dir: Path | None = None) -> Path: - if base_dir is not None and _base_dir is not None: - raise ValueError("Specify either base_dir or _base_dir, not both.") - return base_dir or _base_dir or config.session_dir +def _session_base_dir(config: DepositConfig, base_dir: Path | None = None) -> Path: + return Path(base_dir or config.session_dir) def _summarize_session(session: LocalSession, files: list[LocalFile]) -> SessionSummary: return SessionSummary( session_id=session.session_id, - email=session.email, - users=session.users, - country=session.country, + remote_dep_id=session.remote_dep_id, experiment_type=session.experiment_type, created_at=session.created_at, - remote_dep_id=session.remote_dep_id, - em_subtype=session.em_subtype, - coordinates=session.coordinates, + updated_at=session.updated_at or session.created_at, file_count=len(files), ) @@ -118,7 +112,6 @@ def deposit_init( coordinates: bool | None = None, config: DepositConfig | None = None, base_dir: Path | None = None, - _base_dir: Path | None = None, _api_client: ApiClient | None = None, _check_runner: CheckRunnerProtocol | None = None, ) -> Deposition: @@ -134,7 +127,6 @@ def deposit_init( config: Optional pre-built DepositConfig; loaded from default sources if None. base_dir: Optional session storage directory. Defaults to the configured OneDep Lib session directory. - _base_dir: Override session storage directory (for testing only). _api_client: Override API client (for testing only). _check_runner: Override check runner (for testing only). @@ -143,7 +135,7 @@ def deposit_init( """ config = config or DepositConfig.load() session_id = str(uuid.uuid4()) - session_base_dir = _session_base_dir(config, base_dir=base_dir, _base_dir=_base_dir) + session_base_dir = _session_base_dir(config, base_dir=base_dir) store: SessionStore = JsonSessionStore(session_id, base_dir=session_base_dir) api_client: ApiClient = _api_client or HttpApiClient(config, auth_provider=TokenStore(config)) check_runner: CheckRunnerProtocol = _check_runner or CheckRunner( @@ -158,6 +150,7 @@ def deposit_init( country=country, experiment_type=experiment_type, created_at=datetime.now(), + updated_at=datetime.now(), em_subtype=em_subtype, coordinates=coordinates, ) @@ -169,7 +162,6 @@ def deposit_resume( session_id: str, config: DepositConfig | None = None, base_dir: Path | None = None, - _base_dir: Path | None = None, _api_client: ApiClient | None = None, _check_runner: CheckRunnerProtocol | None = None, ) -> Deposition: @@ -180,7 +172,6 @@ def deposit_resume( config: Optional pre-built DepositConfig; loaded from default sources if None. base_dir: Optional session storage directory. Defaults to the configured OneDep Lib session directory. - _base_dir: Override session storage directory (for testing only). _api_client: Override API client (for testing only). _check_runner: Override check runner (for testing only). @@ -191,7 +182,7 @@ def deposit_resume( KeyError: If no session with the given session_id exists. """ config = config or DepositConfig.load() - session_base_dir = _session_base_dir(config, base_dir=base_dir, _base_dir=_base_dir) + session_base_dir = _session_base_dir(config, base_dir=base_dir) store: SessionStore = JsonSessionStore(session_id, base_dir=session_base_dir) store.get_session() # raises KeyError if not found api_client: ApiClient = _api_client or HttpApiClient(config, auth_provider=TokenStore(config)) @@ -200,7 +191,11 @@ def deposit_resume( if config.fetch_local_schema else RemoteSchemaProvider(config.schema_base_url, config.schema_cache_dir) ) - return Deposition(store=store, api_client=api_client, check_runner=check_runner) + return Deposition( + store=store, + api_client=api_client, + check_runner=check_runner, + ) class Deposition: diff --git a/src/onedep_lib/session/models.py b/src/onedep_lib/session/models.py index 3350c71..b01675b 100644 --- a/src/onedep_lib/session/models.py +++ b/src/onedep_lib/session/models.py @@ -25,6 +25,7 @@ class LocalSession: country: Country experiment_type: ExperimentType | None created_at: datetime + updated_at: datetime | None = None remote_dep_id: str | None = None em_subtype: EMSubType | None = None coordinates: bool | None = None @@ -33,12 +34,8 @@ class LocalSession: @dataclass class SessionSummary: session_id: str - email: str - users: list[str] - country: Country - experiment_type: ExperimentType | None - created_at: datetime remote_dep_id: str | None = None - em_subtype: EMSubType | None = None - coordinates: bool | None = None + experiment_type: ExperimentType | None = None + created_at: datetime | None = None + updated_at: datetime | None = None file_count: int = 0 diff --git a/tests/unit/test_deposition_facade.py b/tests/unit/test_deposition_facade.py index 3d182b1..75f56a3 100644 --- a/tests/unit/test_deposition_facade.py +++ b/tests/unit/test_deposition_facade.py @@ -43,7 +43,7 @@ def dep(tmp_path, stub_api): users=["0000-0001-2345-6789"], country=Country.USA, experiment_type=ExperimentType.XRAY, - _base_dir=tmp_path, + base_dir=tmp_path, _api_client=stub_api, _check_runner=StubCheckRunner(), ) @@ -84,7 +84,7 @@ def test_deposit_without_experiment_type_raises(tmp_path): email="test@example.com", users=[], country=Country.USA, - _base_dir=tmp_path, + base_dir=tmp_path, _api_client=StubApiClient(), _check_runner=StubCheckRunner(), ) @@ -115,7 +115,7 @@ def test_deposit_resume_restores_session(dep, tmp_path, stub_api): dep.close() resumed = deposit_resume( session_id, - _base_dir=tmp_path, + base_dir=tmp_path, _api_client=stub_api, _check_runner=StubCheckRunner(), ) @@ -160,10 +160,10 @@ def test_session_metadata_entry_points(dep, tmp_path): summary = get_session_metadata(session_id, base_dir=tmp_path) assert summary.session_id == session_id - assert summary.email == "test@example.com" - assert summary.country == Country.USA assert summary.experiment_type == ExperimentType.XRAY assert summary.file_count == 1 + assert summary.created_at is not None + assert summary.updated_at is not None summaries = list_session_metadata(base_dir=tmp_path) assert len(summaries) == 1 @@ -177,7 +177,7 @@ def test_context_manager(tmp_path, stub_api): users=[], country=Country.USA, experiment_type=ExperimentType.XRAY, - _base_dir=tmp_path, + base_dir=tmp_path, _api_client=stub_api, _check_runner=StubCheckRunner(), ) as dep: