diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index c1e4216..adc641c 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -3,9 +3,9 @@ name: python-redsys on: push: - branches: [ master ] + branches: [ master, release/1.x ] pull_request: - branches: [ master ] + branches: [ master, release/1.x ] jobs: build: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..abdc4c0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,61 @@ +name: Release + +# Build and publish to PyPI when a version tag (vX.Y.Z) is pushed. +on: + push: + tags: + - "v*" + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + # Guard: the tag must match [tool.poetry].version, so a mistagged release + # can never reach PyPI. + - name: Tag matches package version + run: | + version=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['tool']['poetry']['version'])") + tag="${GITHUB_REF_NAME#v}" + if [ "$version" != "$tag" ]; then + echo "::error::tag v$tag does not match pyproject version $version" + exit 1 + fi + + - name: Build sdist and wheel + run: | + python -m pip install --upgrade build + python -m build + + - name: Check distribution metadata + run: | + python -m pip install --upgrade twine + twine check dist/* + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/python-redsys + permissions: + id-token: write # OIDC token for PyPI Trusted Publishing (no stored secret) + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 671983b..f642f66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ +## v1.2.1 (2026-10-04) + +## Fixed + +- `Request` and `Response` instances no longer share their parameters within a process: a parsed response could carry fields of a previously parsed one (for example its amount), and a request could carry fields of a previous request (for example the Bizum mobile number). Known parameters that are absent now read as `None`. + ## v1.2.0 (2023-10-28) diff --git a/pyproject.toml b/pyproject.toml index d31f00f..fa5d9d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [tool.poetry] name = "python-redsys" -version = "1.2.0" +version = "1.2.1" description = "A simple, clean and less dependant client to handle payments through Redsys." -authors = ["Andrés Reverón Molina ", "David Díaz "] +authors = ["David Díaz ", "Andrés Reverón Molina "] license = "MIT" readme = "README.md" -homepage = "https://github.com/systemallica/python-redsys" +homepage = "https://github.com/ddiazpinto/python-redsys" classifiers = [ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', diff --git a/redsys/__init__.py b/redsys/__init__.py index a2aab01..ac8962e 100644 --- a/redsys/__init__.py +++ b/redsys/__init__.py @@ -1,2 +1,2 @@ -__version__ = "1.2.0" +__version__ = "1.2.1" __version_info__ = tuple(int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")) diff --git a/redsys/request.py b/redsys/request.py index 31d275e..e47773f 100644 --- a/redsys/request.py +++ b/redsys/request.py @@ -77,9 +77,11 @@ class Request: their values according to the platform specifications """ - _parameters: Dict[str, Any] = {} + _parameters: Dict[str, Any] def __init__(self, parameters: Dict[str, Any]) -> None: + # __setattr__ only handles merchant parameters, so the per-instance store is set directly. + object.__setattr__(self, "_parameters", {}) for key, value in parameters.items(): if key in MERCHANT_PARAMETERS_MAP: if check := getattr(self, f"check_{str(key)}", None): @@ -90,7 +92,7 @@ def __init__(self, parameters: Dict[str, Any]) -> None: def __getattr__(self, item: str) -> Any: if item in MERCHANT_PARAMETERS_MAP: - return self._parameters[item] + return self._parameters.get(item) def __setattr__(self, key, value): if key in MERCHANT_PARAMETERS_MAP: diff --git a/redsys/response.py b/redsys/response.py index 0d7cb5e..ccb3dff 100644 --- a/redsys/response.py +++ b/redsys/response.py @@ -93,9 +93,11 @@ class Response: Defines a response """ - _parameters: Dict[str, Any] = {} + _parameters: Dict[str, Any] def __init__(self, parameters: Dict[str, Any]): + # __setattr__ only handles merchant parameters, so the per-instance store is set directly. + object.__setattr__(self, "_parameters", {}) MERCHANT_PARAMETERS_MAP_REVERSE = {value: key for key, value in MERCHANT_PARAMETERS_MAP.items()} for key, value in parameters.items(): reversed_parameter = MERCHANT_PARAMETERS_MAP_REVERSE.get(key, key) @@ -104,7 +106,7 @@ def __init__(self, parameters: Dict[str, Any]): def __getattr__(self, item: str) -> Any: if item in MERCHANT_PARAMETERS_MAP: - return self._parameters[item] + return self._parameters.get(item) def __setattr__(self, key: str, value: Any): if key in MERCHANT_PARAMETERS_MAP: diff --git a/redsys/tests/test_request.py b/redsys/tests/test_request.py index 50b37be..90abfea 100644 --- a/redsys/tests/test_request.py +++ b/redsys/tests/test_request.py @@ -63,3 +63,12 @@ def test_super_long_merchant_url_throws_error(self): merchant_url = "".join(choice("abcdefghijklmnopqrtsuvwxyz-0123456789") for _ in range(251)) with pytest.raises(ValueError): assert Request.check_merchant_url(merchant_url) + + def test_requests_do_not_share_parameters(self): + first = Request( + {"order": "000000000001", "amount": D("1.00"), "currency": EUR, "mobile_number": "+34700000000"} + ) + second = Request({"order": "000000000002", "amount": D("2.00"), "currency": EUR}) + assert "Ds_Merchant_Bizum_MobileNumber" not in second.prepare_parameters() + assert second.mobile_number is None + assert first.prepare_parameters()["Ds_Merchant_Bizum_MobileNumber"] == "+34700000000" diff --git a/redsys/tests/test_response.py b/redsys/tests/test_response.py index 36c7c1d..8548093 100644 --- a/redsys/tests/test_response.py +++ b/redsys/tests/test_response.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from redsys.response import Response @@ -21,3 +23,10 @@ def test_create_response(self): assert response.is_paid is True assert response.is_canceled is False assert response.is_refunded is False + + def test_responses_do_not_share_parameters(self): + first = Response({"Ds_Response": "0000", "Ds_Order": "000000000001", "Ds_Amount": "100"}) + second = Response({"Ds_Response": "0190", "Ds_Order": "000000000002"}) + assert "amount" not in second.parameters + assert second.amount is None + assert first.amount == Decimal("1.00")