Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/python-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Python Checks

on:
push:
branches: ["main"]
pull_request:
branches: ["main"]

jobs:
check-python-app:
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: "0.8.0"

- name: Set up Python (latest)
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"

- name: Run tests
run: uv run pytest tests --cov-fail-under=90

- name: Run mypy type check
run: uv run mypy . --config-file=pyproject.toml

- name: Run ruff linting
run: uv run ruff check --config=pyproject.toml
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,28 @@
# pardner

Python library for authorizing access and fetching personal data from portability APIs and services

## Developer set-up

> **tl;dr**:
>
> - [install `uv`](https://docs.astral.sh/uv/getting-started/installation/).
> - add [`uv run`](https://docs.astral.sh/uv/reference/cli/#uv-run) to beginning of a command to run it with dependencies and in a virtual environment.
> - `uv run pytest tests` to run tests.
> - [`uv add`](https://docs.astral.sh/uv/reference/cli/#uv-add)/[`uv remove`](https://docs.astral.sh/uv/reference/cli/#uv-remove) to install or uninstall packages.
> - `uv run ruff check --config=pyproject.toml` to run the linter.
> - `uv run mypy . --config-file=pyproject.toml` to run the type checker.
> - [`uv build`](https://docs.astral.sh/uv/reference/cli/#uv-build) to build package.

Package and project management is done using [`uv`](https://docs.astral.sh/uv/) ([install here](https://docs.astral.sh/uv/getting-started/installation/)).

Rather than managing your virtual environment and dependencies yourself, `uv` does that for you.
When you're running a command related to the project, you can usually just prepend it with `uv run` to run it within a virtual environment that will automatically install all necessary dependencies.
To build the project, for example, you can run [`uv build`](https://docs.astral.sh/uv/reference/cli/#uv-build) (or `uv run build`) and to run tests you can run `uv run pytest tests`.

Rather than using `requirements.txt` to manage dependencies, `uv` uses its own `uv.lock` file.
Every time you add a new dependency ([`uv add`](https://docs.astral.sh/uv/reference/cli/#uv-add)), it'll automatically update `pyproject.toml` and `uv.lock`.
Unfortunately, `uv.lock` is specific to `uv` (there are still some features the tool provides that can't be stored in `pylock.toml` at the moment).
But [`pylock.toml` (the new standard for listing dependencies)](https://peps.python.org/pep-0751/) isn't and can be used by most package managers available.
We don't store both in this project to prevent them from getting out of sync.
If you need a different lockfile format, you can use [`uv export`](https://docs.astral.sh/uv/reference/cli/#uv-export) to generate it.
236 changes: 0 additions & 236 deletions pylock.toml

This file was deleted.

22 changes: 18 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,29 @@ classifiers = [
]
license = "Apache-2.0"
license-files = ["LICENSE"]
dependencies = [
"oauthlib>=3.3.1",
"requests-oauthlib>=2.0.0",
]
dependencies = ["oauthlib>=3.3.1", "requests-oauthlib>=2.0.0"]

[dependency-groups]
dev = [
"mypy>=1.17.0",
"pytest>=8.4.1",
"pytest-cov>=6.2.1",
"ruff>=0.12.3",
"types-requests-oauthlib>=2.0.0.20250516",
]

[tool.mypy]
show_error_codes = true
warn_return_any = true
strict_optional = true
disallow_incomplete_defs = true
exclude_gitignore = true
exclude = ["tests"]

[tool.ruff]
line-length = 88

[tool.ruff.format]
skip-magic-trailing-comma = true
quote-style = "single"
docstring-code-format = true
8 changes: 3 additions & 5 deletions src/pardner/services/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from pardner.services.base import (
BaseTransferService,
InsufficientScopeException,
UnsupportedVerticalException,
)
from pardner.services.base import BaseTransferService as BaseTransferService
from pardner.services.base import InsufficientScopeException as InsufficientScopeException
from pardner.services.base import UnsupportedVerticalException as UnsupportedVerticalException
24 changes: 18 additions & 6 deletions src/pardner/services/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@


class InsufficientScopeException(Exception):
def __init__(self, unsupported_verticals: Iterable[Vertical], service_name: str) -> None:
def __init__(
self, unsupported_verticals: Iterable[Vertical], service_name: str
) -> None:
combined_verticals = ' '.join(unsupported_verticals)
super().__init__(f'Cannot add {combined_verticals} to {service_name} with current scope.')
super().__init__(
f'Cannot add {combined_verticals} to {service_name} with current scope.'
)


class UnsupportedVerticalException(Exception):
def __init__(self, unsupported_verticals: Iterable[Vertical], service_name: str) -> None:
def __init__(
self, unsupported_verticals: Iterable[Vertical], service_name: str
) -> None:
combined_verticals = ' '.join(unsupported_verticals)
super().__init__(
f'Cannot add {combined_verticals} to {service_name} because they are not supported.'
Expand Down Expand Up @@ -48,7 +54,9 @@ def __init__(
) -> None:
background_application_client = BackendApplicationClient(client_id)
super().__init__(
client_id=client_id, client=background_application_client, redirect_uri=redirect_uri
client_id=client_id,
client=background_application_client,
redirect_uri=redirect_uri,
)
self._client_secret = client_secret
self._supported_verticals = supported_verticals
Expand All @@ -70,13 +78,17 @@ def verticals(self, verticals: Iterable[Vertical]) -> None:
`verticals` are not supported by the service.
"""
unsupported_verticals = [
vertical for vertical in verticals if vertical not in self._supported_verticals
vertical
for vertical in verticals
if vertical not in self._supported_verticals
]
if len(unsupported_verticals) > 0:
raise UnsupportedVerticalException(unsupported_verticals, self.name)
self._verticals = set(verticals)

def add_verticals(self, verticals: Iterable[Vertical], should_reauth: bool = False) -> bool:
def add_verticals(
self, verticals: Iterable[Vertical], should_reauth: bool = False
) -> bool:
"""
Adds to the verticals being requested.

Expand Down
2 changes: 1 addition & 1 deletion src/pardner/verticals/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from pardner.verticals.base import Vertical
from pardner.verticals.base import Vertical as Vertical
19 changes: 15 additions & 4 deletions tests/test_transfer_services/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ def test_set_verticals_raises_exception(mock_vertical, blank_transfer_service):
@pytest.fixture
def transfer_service(mock_vertical):
mock_transfer_service = FakeTransferService(
[Vertical.FAKE_VERTICAL, Vertical.NEW_VERTICAL, Vertical.NEW_VERTICAL_EXTRA_SCOPE],
[
Vertical.FAKE_VERTICAL,
Vertical.NEW_VERTICAL,
Vertical.NEW_VERTICAL_EXTRA_SCOPE,
],
[Vertical.FAKE_VERTICAL],
)
mock_transfer_service.scope = sample_scope
Expand All @@ -62,17 +66,24 @@ def test_add_supported_verticals(mock_vertical, transfer_service):
assert transfer_service.verticals == {Vertical.FAKE_VERTICAL, Vertical.NEW_VERTICAL}


def test_add_unsupported_vertical_new_scope_required(monkeypatch, mock_vertical, transfer_service):
def test_add_unsupported_vertical_new_scope_required(
monkeypatch, mock_vertical, transfer_service
):
def _mock_scope_for_verticals(verticals):
if Vertical.NEW_VERTICAL_EXTRA_SCOPE in verticals:
return {'new_scope'}
return sample_scope

transfer_service.access_token = 'access_token'
monkeypatch.setattr(transfer_service, 'scope_for_verticals', _mock_scope_for_verticals)
monkeypatch.setattr(
transfer_service, 'scope_for_verticals', _mock_scope_for_verticals
)
assert not transfer_service.add_verticals(
[Vertical.NEW_VERTICAL_EXTRA_SCOPE], should_reauth=True
)
assert not transfer_service.access_token
assert transfer_service.scope == {'fake', 'scope', 'new_scope'}
assert transfer_service.verticals == {Vertical.FAKE_VERTICAL, Vertical.NEW_VERTICAL_EXTRA_SCOPE}
assert transfer_service.verticals == {
Vertical.FAKE_VERTICAL,
Vertical.NEW_VERTICAL_EXTRA_SCOPE,
}
Loading