Skip to content

Commit

Permalink
make Bfabric pickleable
Browse files Browse the repository at this point in the history
  • Loading branch information
leoschwarz committed Aug 8, 2024
1 parent 25d5ccf commit fd416b6
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 25 deletions.
48 changes: 33 additions & 15 deletions bfabric/bfabric.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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__(
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 (
Expand All @@ -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(
Expand Down
20 changes: 10 additions & 10 deletions bfabric/tests/unit/test_bfabric.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -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

Expand All @@ -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=[])
Expand Down Expand Up @@ -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"})
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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"})
Expand All @@ -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

Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit fd416b6

Please sign in to comment.