From fd416b6aeefbf25b8caa17f4b6dbb815e9f61b85 Mon Sep 17 00:00:00 2001 From: Leonardo Schwarz Date: Thu, 8 Aug 2024 15:30:51 +0200 Subject: [PATCH] make Bfabric pickleable --- bfabric/bfabric.py | 48 ++++++++++++++++++++---------- bfabric/tests/unit/test_bfabric.py | 20 ++++++------- docs/changelog.md | 4 +++ 3 files changed, 47 insertions(+), 25 deletions(-) mode change 100755 => 100644 bfabric/bfabric.py diff --git a/bfabric/bfabric.py b/bfabric/bfabric.py old mode 100755 new mode 100644 index d0be4b1a..4b7f68a2 --- a/bfabric/bfabric.py +++ b/bfabric/bfabric.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 """B-Fabric Application Interface using WSDL Copyright (C) 2014 - 2024 Functional Genomics Center Zurich ETHZ|UZH. All rights reserved. @@ -20,6 +19,7 @@ from contextlib import contextmanager from datetime import datetime from enum import Enum +from functools import cached_property from pathlib import Path from pprint import pprint from typing import Literal, Any @@ -49,7 +49,7 @@ class Bfabric: Use `Bfabric.from_config` to create a new instance. :param config: Configuration object :param auth: Authentication object (if `None`, it has to be provided using the `with_auth` context manager) - :param engine: Engine to use for the API. Default is SUDS. + :param engine: Engine type to use for the API. Default is `BfabricAPIEngineType.SUDS`. """ def __init__( @@ -61,15 +61,17 @@ def __init__( self.query_counter = 0 self._config = config self._auth = auth + self._engine_type = engine + self._log_version_message() - if engine == BfabricAPIEngineType.SUDS: - self.engine = EngineSUDS(base_url=config.base_url) - elif engine == BfabricAPIEngineType.ZEEP: - self.engine = EngineZeep(base_url=config.base_url) + @cached_property + def _engine(self) -> EngineSUDS | EngineZeep: + if self._engine_type == BfabricAPIEngineType.SUDS: + return EngineSUDS(base_url=self._config.base_url) + elif self._engine_type == BfabricAPIEngineType.ZEEP: + return EngineZeep(base_url=self._config.base_url) else: - raise ValueError(f"Unexpected engine: {engine}") - - self._log_version_message() + raise ValueError(f"Unexpected engine type: {self._engine_type}") @classmethod def from_config( @@ -147,7 +149,7 @@ def read( """ # Get the first page. logger.debug(f"Reading from endpoint {repr(endpoint)} with query {repr(obj)}") - results = self.engine.read(endpoint=endpoint, obj=obj, auth=self.auth, page=1, return_id_only=return_id_only) + results = self._engine.read(endpoint=endpoint, obj=obj, auth=self.auth, page=1, return_id_only=return_id_only) n_available_pages = results.total_pages_api if not n_available_pages: if check: @@ -170,7 +172,7 @@ def read( for i_iter, i_page in enumerate(requested_pages): if not (i_iter == 0 and i_page == 1): logger.debug(f"Reading page {i_page} of {n_available_pages}") - results = self.engine.read( + results = self._engine.read( endpoint=endpoint, obj=obj, auth=self.auth, page=i_page, return_id_only=return_id_only ) errors += results.errors @@ -192,7 +194,7 @@ def save(self, endpoint: str, obj: dict[str, Any], check: bool = True, method: s appropriate to be used instead. :return a ResultContainer describing the saved object if successful """ - results = self.engine.save(endpoint=endpoint, obj=obj, auth=self.auth, method=method) + results = self._engine.save(endpoint=endpoint, obj=obj, auth=self.auth, method=method) if check: results.assert_success() return results @@ -204,7 +206,7 @@ def delete(self, endpoint: str, id: int | list[int], check: bool = True) -> Resu :param check: whether to raise an error if the response is not successful :return a ResultContainer describing the deleted object if successful """ - results = self.engine.delete(endpoint=endpoint, id=id, auth=self.auth) + results = self._engine.delete(endpoint=endpoint, id=id, auth=self.auth) if check: results.assert_success() return results @@ -252,7 +254,7 @@ def _get_version_message(self) -> tuple[str, str]: """Returns the version message as a string.""" package_version = importlib.metadata.version("bfabric") year = datetime.now().year - engine_name = self.engine.__class__.__name__ + engine_name = self._engine.__class__.__name__ base_url = self.config.base_url user_name = f"U={self._auth.login if self._auth else None}" return ( @@ -269,7 +271,23 @@ def _log_version_message(self) -> None: logger.info(capture.get()) def __repr__(self) -> str: - return f"Bfabric(config={repr(self.config)}, auth={repr(self.auth)}, engine={self.engine})" + return f"Bfabric(config={repr(self.config)}, auth={repr(self.auth)}, engine={self._engine})" + + __str__ = __repr__ + + def __getstate__(self): + return { + "config": self._config, + "auth": self._auth, + "engine_type": self._engine_type, + "query_counter": self.query_counter, + } + + def __setstate__(self, state): + self._config = state["config"] + self._auth = state["auth"] + self._engine_type = state["engine_type"] + self.query_counter = state["query_counter"] def get_system_auth( diff --git a/bfabric/tests/unit/test_bfabric.py b/bfabric/tests/unit/test_bfabric.py index 8e30c043..c52d6ff0 100644 --- a/bfabric/tests/unit/test_bfabric.py +++ b/bfabric/tests/unit/test_bfabric.py @@ -88,7 +88,7 @@ def test_from_config_when_engine_suds(mocker): assert isinstance(client, Bfabric) assert client.config == mock_config assert client.auth == mock_auth - assert client.engine == mock_engine_suds.return_value + assert client._engine == mock_engine_suds.return_value mock_get_system_auth.assert_called_once_with(config_env=None, config_path=None) mock_engine_suds.assert_called_once_with(base_url=mock_config.base_url) @@ -106,7 +106,7 @@ def test_from_config_when_engine_zeep(mocker): assert isinstance(client, Bfabric) assert client.config == mock_config assert client.auth == mock_auth - assert client.engine == mock_engine_zeep.return_value + assert client._engine == mock_engine_zeep.return_value mock_get_system_auth.assert_called_once_with(config_env=None, config_path=None) mock_engine_zeep.assert_called_once_with(base_url=mock_config.base_url) @@ -157,7 +157,7 @@ def test_read_when_no_pages_available_and_check(bfabric_instance, mocker): mock_auth = MagicMock(name="mock_auth") bfabric_instance._auth = mock_auth - mock_engine = mocker.patch.object(bfabric_instance, "engine") + mock_engine = mocker.patch.object(bfabric_instance, "_engine") mock_result = MagicMock(name="mock_result", total_pages_api=0, assert_success=MagicMock()) mock_engine.read.return_value = mock_result @@ -176,7 +176,7 @@ def test_read_when_pages_available_and_check(bfabric_instance, mocker): bfabric_instance._auth = mock_auth mock_compute_requested_pages = mocker.patch("bfabric.bfabric.compute_requested_pages") - mock_engine = mocker.patch.object(bfabric_instance, "engine") + mock_engine = mocker.patch.object(bfabric_instance, "_engine") mock_page_results = [ MagicMock(name=f"mock_page_result_{i}", assert_success=MagicMock(), total_pages_api=3, errors=[]) @@ -205,7 +205,7 @@ def test_read_when_pages_available_and_check(bfabric_instance, mocker): def test_save_when_no_auth(bfabric_instance, mocker): - mock_engine = mocker.patch.object(bfabric_instance, "engine") + mock_engine = mocker.patch.object(bfabric_instance, "_engine") with pytest.raises(ValueError, match="Authentication not available"): bfabric_instance.save("test_endpoint", {"key": "value"}) @@ -217,7 +217,7 @@ def test_save_when_auth_and_check_false(bfabric_instance, mocker): mock_auth = MagicMock(name="mock_auth") bfabric_instance._auth = mock_auth - mock_engine = mocker.patch.object(bfabric_instance, "engine") + mock_engine = mocker.patch.object(bfabric_instance, "_engine") method_assert_success = MagicMock(name="method_assert_success") mock_engine.save.return_value.assert_success = method_assert_success @@ -234,7 +234,7 @@ def test_save_when_auth_and_check_true(bfabric_instance, mocker): mock_auth = MagicMock(name="mock_auth") bfabric_instance._auth = mock_auth - mock_engine = mocker.patch.object(bfabric_instance, "engine") + mock_engine = mocker.patch.object(bfabric_instance, "_engine") method_assert_success = MagicMock(name="method_assert_success") mock_engine.save.return_value.assert_success = method_assert_success @@ -248,7 +248,7 @@ def test_save_when_auth_and_check_true(bfabric_instance, mocker): def test_delete_when_no_auth(bfabric_instance, mocker): - mock_engine = mocker.patch.object(bfabric_instance, "engine") + mock_engine = mocker.patch.object(bfabric_instance, "_engine") with pytest.raises(ValueError, match="Authentication not available"): bfabric_instance.delete("test_endpoint", {"key": "value"}) @@ -260,7 +260,7 @@ def test_delete_when_auth_and_check_false(bfabric_instance, mocker): mock_auth = MagicMock(name="mock_auth") bfabric_instance._auth = mock_auth - mock_engine = mocker.patch.object(bfabric_instance, "engine") + mock_engine = mocker.patch.object(bfabric_instance, "_engine") method_assert_success = MagicMock(name="method_assert_success") mock_engine.delete.return_value.assert_success = method_assert_success @@ -275,7 +275,7 @@ def test_delete_when_auth_and_check_true(bfabric_instance, mocker): mock_auth = MagicMock(name="mock_auth") bfabric_instance._auth = mock_auth - mock_engine = mocker.patch.object(bfabric_instance, "engine") + mock_engine = mocker.patch.object(bfabric_instance, "_engine") method_assert_success = MagicMock(name="method_assert_success") mock_engine.delete.return_value.assert_success = method_assert_success diff --git a/docs/changelog.md b/docs/changelog.md index 30f501d1..71f2ab0b 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -10,6 +10,10 @@ Versioning currently follows `X.Y.Z` where ## \[tba\] - tba +### Added + +- The `Bfabric` instance is now pickleable. + ## \[1.13.4\] - 2024-08-05 ### Added