Skip to content

Commit fd18d39

Browse files
authored
Merge pull request #167 from fgcz/main
bfabric_app_runner 0.0.20
2 parents ddcb3ea + cd825b3 commit fd18d39

77 files changed

Lines changed: 2453 additions & 852 deletions

File tree

Some content is hidden

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

bfabric/docs/changelog.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ Versioning currently follows `X.Y.Z` where
1010

1111
## \[Unreleased\]
1212

13+
## \[1.13.23\] - 2025-03-25
14+
15+
### Fixed
16+
17+
- Unsuccessful deletions are detected by checking the B-Fabric response.
18+
- Handle problematic characters in `Workunit.store_output_folder`.
19+
- `BfabricRequestError` did not properly subclass RuntimeError.
20+
21+
### Changed
22+
23+
- `WorkunitDefinition` uses `PathSafeStr` to normalize app and workunit names.
24+
- Internal: `ResultContainer` has no optional constructor arguments anymore to avoid confusion.
25+
26+
### Added
27+
28+
- Generic functionality in `bfabric.utils.path_safe_name` to validate names for use in paths.
29+
- `bfabric.entities.Dataset.{write_parquet, get_parquet}` methods for writing parquet
30+
1331
## \[1.13.22\] - 2025-02-19
1432

1533
### Fixed

bfabric/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
55
[project]
66
name = "bfabric"
77
description = "Python client for the B-Fabric API"
8-
version = "1.13.22"
8+
version = "1.13.23"
99
license = { text = "GPL-3.0" }
1010
authors = [
1111
{ name = "Christian Panse", email = "cp@fgcz.ethz.ch" },
@@ -46,7 +46,7 @@ dev = [
4646
]
4747
doc = ["mkdocs", "mkdocs-material", "mkdocstrings[python]"]
4848
test = ["pytest", "pytest-mock", "logot[pytest,loguru]"]
49-
typing = ["mypy", "types-requests", "lxml-stubs", "pandas-stubs", "types-python-dateutil"]
49+
typing = ["mypy", "types-requests", "lxml-stubs", "pandas-stubs", "types-python-dateutil", "types-PyYAML"]
5050

5151
[project.urls]
5252
Homepage = "https://github.com/fgcz/bfabricPy"

bfabric/src/bfabric/engine/engine_suds.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99

1010
from bfabric.engine.response_format_suds import suds_asdict_recursive
1111
from bfabric.errors import BfabricRequestError, get_response_errors
12-
from bfabric.results.result_container import ResultContainer
12+
from bfabric.results.response_delete import ResponseDelete
1313
from bfabric.results.response_format_dict import clean_result
14+
from bfabric.results.result_container import ResultContainer
1415

1516
if TYPE_CHECKING:
1617
from suds.serviceproxy import ServiceProxy
@@ -46,13 +47,13 @@ def read(
4647
query = copy.deepcopy(obj)
4748
query["includedeletableupdateable"] = include_deletable_and_updatable_fields
4849

49-
full_query = dict(
50-
login=auth.login,
51-
page=page,
52-
password=auth.password.get_secret_value(),
53-
query=query,
54-
idonly=return_id_only,
55-
)
50+
full_query = {
51+
"login": auth.login,
52+
"page": page,
53+
"password": auth.password.get_secret_value(),
54+
"query": query,
55+
"idonly": return_id_only,
56+
}
5657
service = self._get_suds_service(endpoint)
5758
response = service.read(full_query)
5859
return self._convert_results(response=response, endpoint=endpoint)
@@ -80,14 +81,11 @@ def delete(self, endpoint: str, id: int | list[int], auth: BfabricAuth) -> Resul
8081
:param auth: the authentication handle of the user performing the request
8182
"""
8283
if isinstance(id, list) and len(id) == 0:
83-
print("Warning, attempted to delete an empty list, ignoring")
84-
# TODO maybe use error here (and make sure it's consistent)
85-
return ResultContainer([], total_pages_api=0)
86-
84+
return ResponseDelete.from_empty_request()
8785
query = {"login": auth.login, "password": auth.password.get_secret_value(), "id": id}
8886
service = self._get_suds_service(endpoint)
8987
response = service.delete(query)
90-
return self._convert_results(response=response, endpoint=endpoint)
88+
return ResponseDelete.from_suds(suds_response=response, endpoint=endpoint)
9189

9290
def _get_suds_service(self, endpoint: str) -> ServiceProxy:
9391
"""Returns a SUDS service for the given endpoint. Reuses existing instances when possible."""

bfabric/src/bfabric/engine/engine_zeep.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from zeep.helpers import serialize_object
88

99
from bfabric.errors import BfabricRequestError, get_response_errors
10+
from bfabric.results.response_delete import ResponseDelete
1011
from bfabric.results.result_container import ResultContainer
1112
from bfabric.results.response_format_dict import clean_result
1213

@@ -104,15 +105,12 @@ def delete(self, endpoint: str, id: int | list[int], auth: BfabricAuth) -> Resul
104105
:param auth: the authentication handle of the user performing the request
105106
"""
106107
if isinstance(id, list) and len(id) == 0:
107-
print("Warning, attempted to delete an empty list, ignoring")
108-
# TODO maybe use error here (and make sure it's consistent)
109-
return ResultContainer([], total_pages_api=0)
108+
return ResponseDelete.from_empty_request()
110109

111110
query = {"login": auth.login, "password": auth.password.get_secret_value(), "id": id}
112-
113111
client = self._get_client(endpoint)
114112
response = client.service.delete(query)
115-
return self._convert_results(response=response, endpoint=endpoint)
113+
return ResponseDelete.from_zeep(zeep_response=response, endpoint=endpoint)
116114

117115
def _get_client(self, endpoint: str) -> zeep.Client:
118116
if endpoint not in self._cl:

bfabric/src/bfabric/entities/dataset.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from __future__ import annotations
22

3-
import tempfile
4-
from pathlib import Path
5-
from typing import Any, TYPE_CHECKING
3+
import io
4+
from typing import TYPE_CHECKING, Any
65

76
from polars import DataFrame
87

98
from bfabric.entities.core.entity import Entity
109

1110
if TYPE_CHECKING:
11+
from pathlib import Path
1212
from bfabric import Bfabric
1313

1414

@@ -49,8 +49,14 @@ def write_csv(self, path: Path, separator: str = ",") -> None:
4949

5050
def get_csv(self, separator: str = ",") -> str:
5151
"""Returns the dataset as a csv string, using the specified column `separator`."""
52-
with tempfile.NamedTemporaryFile() as tmp_file:
53-
self.write_csv(Path(tmp_file.name), separator=separator)
54-
tmp_file.flush()
55-
tmp_file.seek(0)
56-
return tmp_file.read().decode()
52+
return self.to_polars().write_csv(separator=separator)
53+
54+
def write_parquet(self, path: Path) -> None:
55+
"""Writes the dataset to a parquet file at `path`."""
56+
self.to_polars().write_parquet(path)
57+
58+
def get_parquet(self) -> bytes:
59+
"""Returns the dataset as a parquet bytes object."""
60+
bytes_io = io.BytesIO()
61+
self.to_polars().write_parquet(bytes_io)
62+
return bytes_io.getvalue()

bfabric/src/bfabric/entities/workunit.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from bfabric.entities.core.has_container_mixin import HasContainerMixin
1111
from bfabric.entities.core.has_many import HasMany
1212
from bfabric.entities.core.has_one import HasOne
13+
from bfabric.utils.path_safe_name import path_safe_name
1314

1415
if TYPE_CHECKING:
1516
from bfabric import Bfabric
@@ -52,8 +53,8 @@ def store_output_folder(self) -> Path:
5253
return Path(
5354
f"{self.application.storage['projectfolderprefix']}{self.container.id}",
5455
"bfabric",
55-
self.application["technology"].replace(" ", "_"),
56-
self.application["name"].replace(" ", "_"),
56+
path_safe_name(self.application["technology"]),
57+
path_safe_name(self.application["name"]),
5758
date.strftime("%Y/%Y-%m/%Y-%m-%d/"),
5859
f"workunit_{self.id}",
5960
)

bfabric/src/bfabric/errors.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,20 @@ class BfabricRequestError(RuntimeError):
77
"""An error that is returned by the server in response to a full request."""
88

99
def __init__(self, message: str) -> None:
10+
# Call parent class constructor to properly initialize RuntimeError
11+
super().__init__(message)
1012
self.message = message
1113

1214
def __repr__(self) -> str:
13-
return f"RequestError(message={repr(self.message)})"
15+
return f"BfabricRequestError(message={repr(self.message)})"
16+
17+
def __str__(self) -> str:
18+
return self.message
19+
20+
def __eq__(self, other: Any) -> bool:
21+
if not isinstance(other, BfabricRequestError):
22+
return False
23+
return self.message == other.message
1424

1525

1626
class BfabricConfigError(RuntimeError):

bfabric/src/bfabric/experimental/multi_query.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def read_multi(
4141
NOTE: It is assumed that there is only 1 response for each value.
4242
"""
4343
# TODO add `check` parameter
44-
response_tot = ResultContainer([], total_pages_api=0)
44+
response_tot = ResultContainer([], total_pages_api=0, errors=[])
4545
obj_extended = deepcopy(obj) # Make a copy of the query, not to make edits to the argument
4646

4747
# Iterate over request chunks that fit into a single API page
@@ -79,7 +79,7 @@ def delete_multi(self, endpoint: str, id_list: list[int]) -> ResultContainer:
7979
"""Deletes multiple objects from `endpoint` by their ids."""
8080
# TODO document and test error handling
8181
# TODO add `check` parameter
82-
response_tot = ResultContainer([], total_pages_api=0)
82+
response_tot = ResultContainer([], total_pages_api=0, errors=[])
8383

8484
if not id_list:
8585
print("Warning, empty list provided for deletion, ignoring")

bfabric/src/bfabric/experimental/workunit_definition.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import yaml
77
from bfabric.entities import Workunit
88
from pydantic import BaseModel, model_validator
9+
from bfabric.utils.path_safe_name import PathSafeStr # noqa: TC001
910

1011
if TYPE_CHECKING:
1112
from bfabric import Bfabric
@@ -61,11 +62,11 @@ class WorkunitRegistrationDefinition(BaseModel):
6162

6263
application_id: int
6364
"""The ID of the executing application."""
64-
application_name: str
65+
application_name: PathSafeStr
6566
"""The name of the executing application."""
6667
workunit_id: int
6768
"""The ID of the workunit."""
68-
workunit_name: str
69+
workunit_name: PathSafeStr
6970
"""The name of the workunit."""
7071
container_id: int
7172
"""The ID of the container."""

bfabric/src/bfabric/rest/token_data.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from datetime import datetime
4-
from typing import TYPE_CHECKING
4+
from typing import TYPE_CHECKING, Any
55

66
import requests
77
from pydantic import BaseModel, Field, SecretStr, ConfigDict
@@ -13,9 +13,7 @@
1313
class TokenData(BaseModel):
1414
"""Parsed token data from the B-Fabric token validation endpoint."""
1515

16-
model_config = ConfigDict(
17-
populate_by_name=True, str_strip_whitespace=True, json_encoders={datetime: lambda v: v.isoformat()}
18-
)
16+
model_config = ConfigDict(populate_by_name=True, str_strip_whitespace=True)
1917

2018
job_id: int = Field(alias="jobId")
2119
application_id: int = Field(alias="applicationId")
@@ -29,6 +27,14 @@ class TokenData(BaseModel):
2927
token_expires: datetime = Field(alias="expiryDateTime")
3028
environment: str
3129

30+
# Define a custom serializer method for model_dump
31+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
32+
data = super().model_dump(**kwargs)
33+
# Convert datetime to ISO format
34+
if "token_expires" in data and isinstance(data["token_expires"], datetime):
35+
data["token_expires"] = data["token_expires"].isoformat()
36+
return data
37+
3238

3339
def get_token_data(client_config: BfabricClientConfig, token: str) -> TokenData:
3440
"""Returns the token data for the provided token.

0 commit comments

Comments
 (0)