Skip to content

Commit

Permalink
Merge pull request #150 from fgcz/main
Browse files Browse the repository at this point in the history
bfabric-scripts 1.13.23
  • Loading branch information
leoschwarz authored Feb 19, 2025
2 parents e509b80 + 2d2f250 commit af1e6e7
Show file tree
Hide file tree
Showing 66 changed files with 891 additions and 211 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
changelog.md merge=manual
5 changes: 0 additions & 5 deletions .github/workflows/build_app_runner.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
name: Build App Runner
on:
workflow_dispatch:
release:
types: [published]
pull_request:
branches: [stable]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
Expand Down Expand Up @@ -32,7 +28,6 @@ jobs:
# - It's a release with valid tag
if: >
${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || (github.event_name == 'release' && needs.check_release_tag.outputs.is_valid_tag == 'true') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
44 changes: 43 additions & 1 deletion .github/workflows/publish_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write # permission to create tags
contents: write # permission to create tags and releases
steps:
- uses: actions/checkout@v4
# Step: Determine the package that is being built
Expand Down Expand Up @@ -86,6 +86,48 @@ jobs:
TAG_NAME="${{ env.PACKAGE }}/${{ env.VERSION }}"
git tag -a "$TAG_NAME" -m "Release ${{ env.PACKAGE }} version ${{ env.VERSION }}"
git push origin "$TAG_NAME"
- name: Extract changelog and create release
run: |
set -x
CHANGELOG_PATH="${{ env.PACKAGE }}/docs/changelog.md"
TAG_NAME="${{ env.PACKAGE }}/${{ env.VERSION }}"
if [ -f "$CHANGELOG_PATH" ]; then
# Extract the changelog section for the current version
# This assumes the changelog follows the keep-a-changelog format
CHANGELOG_CONTENT=$(awk -v ver="${{ env.VERSION }}" '
BEGIN { found=0; }
$0 ~ "^## \\\\[" ver "\\\\]" { found=1; next }
found && $0 ~ "^## \\\\[" { exit }
found { print }
' "$CHANGELOG_PATH")
if [ -n "$CHANGELOG_CONTENT" ]; then
# Create GitHub release
gh release create "$TAG_NAME" \
--title "Release ${{ env.PACKAGE }} ${{ env.VERSION }}" \
--notes "$CHANGELOG_CONTENT" \
--target ${{ github.sha }} \
--draft
else
# Create release with basic notes if no changelog section is found
gh release create "$TAG_NAME" \
--title "Release ${{ env.PACKAGE }} ${{ env.VERSION }}" \
--notes "Release of ${{ env.PACKAGE }} version ${{ env.VERSION }}" \
--target ${{ github.sha }} \
--draft
fi
else
echo "Warning: Changelog file not found at $CHANGELOG_PATH"
# Create release with basic notes
gh release create "$TAG_NAME" \
--title "Release ${{ env.PACKAGE }} ${{ env.VERSION }}" \
--notes "Release of ${{ env.PACKAGE }} version ${{ env.VERSION }}" \
--target ${{ github.sha }} \
--draft
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Debug package info
run: |
echo "Built and published package: ${{ env.PACKAGE }}"
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/run_unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.9
python-version: 3.11
- name: Install nox
run: pip install nox uv
- name: Run checks
Expand All @@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: 3.9
python-version: 3.11
- name: Install nox
run: pip install nox uv
- name: Check code with ruff
Expand Down
2 changes: 0 additions & 2 deletions ___pytest.ini

This file was deleted.

8 changes: 7 additions & 1 deletion docs/changelog.md → bfabric/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ Versioning currently follows `X.Y.Z` where
- `Y` should be the current bfabric release
- `Z` is increased for feature releases, that should not break the API

## \[Unreleased\]
## \[1.13.21\] - 2025-02-19

### Added

- `Entity.load_yaml` and `Entity.dump_yaml`
- `Bfabric.from_token` to create a `Bfabric` instance from a token
- `bfabric.rest.token_data` to get token data from the REST API, low-level functionality

### Changed

- Internally, the user password is now in a `pydantic.SecretStr` until we construct the API call. This should prevent some logging related accidents.

## \[1.13.20\] - 2025-02-10

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
3 changes: 2 additions & 1 deletion bfabric/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "hatchling.build"
[project]
name = "bfabric"
description = "Python client for the B-Fabric API"
version = "1.13.20"
version = "1.13.21"
license = { text = "GPL-3.0" }
authors = [
{ name = "Christian Panse", email = "[email protected]" },
Expand All @@ -29,6 +29,7 @@ dependencies = [
"eval_type_backport; python_version < '3.10'",
"python-dateutil >= 2.9.0",
"cyclopts >= 2.9.9",
"requests >= 2.26.0",
#"platformdirs >= 4.3",
]

Expand Down
28 changes: 25 additions & 3 deletions bfabric/src/bfabric/bfabric.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

import base64
import importlib.metadata
import sys
from contextlib import contextmanager
from datetime import datetime
from enum import Enum
Expand All @@ -24,16 +23,18 @@
from pprint import pprint
from typing import Literal, Any, TYPE_CHECKING

import sys
from loguru import logger
from rich.console import Console

from bfabric.bfabric_config import read_config
from bfabric.utils.cli_integration import DEFAULT_THEME, HostnameHighlighter
from bfabric.config import BfabricAuth
from bfabric.config import BfabricClientConfig
from bfabric.engine.engine_suds import EngineSUDS
from bfabric.engine.engine_zeep import EngineZeep
from bfabric.rest.token_data import get_token_data
from bfabric.results.result_container import ResultContainer
from bfabric.utils.cli_integration import DEFAULT_THEME, HostnameHighlighter
from bfabric.utils.paginator import compute_requested_pages, BFABRIC_QUERY_LIMIT

if TYPE_CHECKING:
Expand Down Expand Up @@ -93,12 +94,33 @@ def from_config(
:param config_path: Path to the config file, in case it is different from default
:param auth: Authentication to use. If "config" is given, the authentication will be read from the config file.
If it is set to None, no authentication will be used.
:param engine: Engine to use for the API. Default is SUDS.
:param engine: Engine to use for the API.
"""
config, auth_config = get_system_auth(config_env=config_env, config_path=config_path)
auth_used: BfabricAuth | None = auth_config if auth == "config" else auth
return cls(config, auth_used, engine=engine)

@classmethod
def from_token(
cls,
token: str,
config_env: str | None = None,
config_path: str | None = None,
engine: BfabricAPIEngineType = BfabricAPIEngineType.SUDS,
) -> Bfabric:
"""Returns a new Bfabric instance, configured with the user configuration file and the provided token.
Any authentication in the configuration file will be ignored, but it will be used to determine the correct
B-Fabric instance.
:param token: the token to use for authentication
:param config_env: the config environment to use (if not specified, see `from_config`)
:param config_path: the path to the config file (if not specified, see `from_config`)
:param engine: the engine to use for the API.
"""
config, _ = get_system_auth(config_env=config_env, config_path=config_path)
token_data = get_token_data(client_config=config, token=token)
auth = BfabricAuth(login=token_data.user, password=token_data.user_ws_password)
return cls(config, auth, engine=engine)

@property
def config(self) -> BfabricClientConfig:
"""Returns the config object."""
Expand Down
9 changes: 2 additions & 7 deletions bfabric/src/bfabric/config/bfabric_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@

from typing import Annotated

from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, SecretStr


class BfabricAuth(BaseModel):
"""Holds the authentication data for the B-Fabric client."""

login: Annotated[str, Field(min_length=3)]
password: Annotated[str, Field(min_length=32, max_length=32)]

def __repr__(self) -> str:
return f"BfabricAuth(login={repr(self.login)}, password=...)"

__str__ = __repr__
password: Annotated[SecretStr, Field(min_length=32, max_length=32)]
6 changes: 3 additions & 3 deletions bfabric/src/bfabric/engine/engine_suds.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def read(
full_query = dict(
login=auth.login,
page=page,
password=auth.password,
password=auth.password.get_secret_value(),
query=query,
idonly=return_id_only,
)
Expand All @@ -65,7 +65,7 @@ def save(self, endpoint: str, obj: dict, auth: BfabricAuth, method: str = "save"
:param method: the method to use for saving, generally "save", but in some cases e.g. "checkandinsert" is more
appropriate to be used instead.
"""
query = {"login": auth.login, "password": auth.password, endpoint: obj}
query = {"login": auth.login, "password": auth.password.get_secret_value(), endpoint: obj}
service = self._get_suds_service(endpoint)
try:
response = getattr(service, method)(query)
Expand All @@ -84,7 +84,7 @@ def delete(self, endpoint: str, id: int | list[int], auth: BfabricAuth) -> Resul
# TODO maybe use error here (and make sure it's consistent)
return ResultContainer([], total_pages_api=0)

query = {"login": auth.login, "password": auth.password, "id": id}
query = {"login": auth.login, "password": auth.password.get_secret_value(), "id": id}
service = self._get_suds_service(endpoint)
response = service.delete(query)
return self._convert_results(response=response, endpoint=endpoint)
Expand Down
6 changes: 3 additions & 3 deletions bfabric/src/bfabric/engine/engine_zeep.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def read(
full_query = dict(
login=auth.login,
page=page,
password=auth.password,
password=auth.password.get_secret_value(),
query=query,
idonly=return_id_only,
)
Expand All @@ -84,7 +84,7 @@ def save(self, endpoint: str, obj: dict, auth: BfabricAuth, method: str = "save"
excl_keys = ["name", "sampleid", "storageid", "workunitid", "relativepath"]
_zeep_query_append_skipped(query, excl_keys, inplace=True, overwrite=False)

full_query = {"login": auth.login, "password": auth.password, endpoint: query}
full_query = {"login": auth.login, "password": auth.password.get_secret_value(), endpoint: query}

client = self._get_client(endpoint)

Expand All @@ -108,7 +108,7 @@ def delete(self, endpoint: str, id: int | list[int], auth: BfabricAuth) -> Resul
# TODO maybe use error here (and make sure it's consistent)
return ResultContainer([], total_pages_api=0)

query = {"login": auth.login, "password": auth.password, "id": id}
query = {"login": auth.login, "password": auth.password.get_secret_value(), "id": id}

client = self._get_client(endpoint)
response = client.service.delete(query)
Expand Down
5 changes: 5 additions & 0 deletions bfabric/src/bfabric/entities/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ def to_polars(self) -> DataFrame:
data.append(dict(zip(column_names, row_values)))
return DataFrame(data)

@property
def types(self) -> dict[str, str]:
"""Returns a dictionary mapping column names to their data types."""
return {x["name"]: x["type"] for x in self.data_dict["attribute"]}

def write_csv(self, path: Path, separator: str = ",") -> None:
"""Writes the dataset to a csv file at `path`, using the specified column `separator`."""
self.to_polars().write_csv(path, separator=separator)
Expand Down
2 changes: 1 addition & 1 deletion bfabric/src/bfabric/examples/compare_zeep_suds_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def full_query(auth: BfabricAuth, query: dict, includedeletableupdateable: bool
thisQuery = deepcopy(query)
thisQuery["includedeletableupdateable"] = includedeletableupdateable

return {"login": auth.login, "password": auth.password, "query": thisQuery}
return {"login": auth.login, "password": auth.password.get_secret_value(), "query": thisQuery}


def calc_both(
Expand Down
2 changes: 1 addition & 1 deletion bfabric/src/bfabric/examples/zeep_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def full_query(auth: BfabricAuth, query: dict, includedeletableupdateable: bool
thisQuery = deepcopy(query)
thisQuery["includedeletableupdateable"] = includedeletableupdateable

return {"login": auth.login, "password": auth.password, "query": thisQuery}
return {"login": auth.login, "password": auth.password.get_secret_value(), "query": thisQuery}


def read_zeep(wsdl, fullQuery, raw=True):
Expand Down
2 changes: 0 additions & 2 deletions bfabric/src/bfabric/experimental/upload_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ def polars_to_bfabric_type(dtype: pl.DataType) -> str | None:
return "Integer"
elif str(dtype).startswith("String"):
return "String"
elif str(dtype).startswith("Float"):
return "Float"
else:
return "String"

Expand Down
Empty file.
43 changes: 43 additions & 0 deletions bfabric/src/bfabric/rest/token_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

from datetime import datetime
from typing import TYPE_CHECKING

import requests
from pydantic import BaseModel, Field, SecretStr, ConfigDict

if TYPE_CHECKING:
from bfabric import BfabricClientConfig


class TokenData(BaseModel):
"""Parsed token data from the B-Fabric token validation endpoint."""

model_config = ConfigDict(
populate_by_name=True, str_strip_whitespace=True, json_encoders={datetime: lambda v: v.isoformat()}
)

job_id: int = Field(alias="jobId")
application_id: int = Field(alias="applicationId")

entity_class: str = Field(alias="entityClassName")
entity_id: int = Field(alias="entityId")

user: str = Field(alias="user")
user_ws_password: SecretStr = Field(alias="userWsPassword")

token_expires: datetime = Field(alias="expiryDateTime")
environment: str


def get_token_data(client_config: BfabricClientConfig, token: str) -> TokenData:
"""Returns the token data for the provided token.
If the request fails, an exception is raised.
"""
url = f"{client_config.base_url}/rest/token/validate"
response = requests.get(url, params={"token": token})
if not response.ok:
response.raise_for_status()
parsed = response.json()
return TokenData.model_validate(parsed)
6 changes: 6 additions & 0 deletions bfabric_app_runner/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added

- Implement `--force-storage` to pass a yaml to a forced storage instead of the real one.
- A Makefile will be created in the app folder for easier interaction with the app-runner (it uses uv and PyPI).

### Changed

- CopyResourceSpec.update_existing now defaults to `if_exists`.
- Resolve workunit_ref to absolute path if it is a Path instance for CLI.

## \[0.0.15\] - 2025-02-06

Expand Down
7 changes: 7 additions & 0 deletions bfabric_app_runner/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ dependencies = [
"mako",
]

[tool.hatch.build.targets.sdist]
include = [
"src/bfabric_app_runner/**/*.py",
"src/bfabric_app_runner/py.typed",
"src/bfabric_app_runner/resources/*.mk"
]

[project.scripts]
"bfabric-app-runner"="bfabric_app_runner.cli.__main__:app"

Expand Down
Loading

0 comments on commit af1e6e7

Please sign in to comment.