From 982bf7ff3d5b17196aa94d45472d5ca9a509ba02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sabri=20Onur=20T=C3=BCz=C3=BCn?= Date: Tue, 13 Oct 2026 22:31:43 +0300 Subject: [PATCH] Move version module into the craftgate package --- .github/scripts/release-dry-run.sh | 34 +++++++++++++++++++++++++ .github/workflows/craftgate-build.yml | 12 +++++++-- .github/workflows/craftgate-release.yml | 6 ++--- README.md | 2 +- _version.py => craftgate/_version.py | 0 craftgate/adapter/base_adapter.py | 2 +- pyproject.toml | 7 +++-- scripts/update_version.py | 9 ++++--- tests/test_client_version.py | 26 +++++++++++++++++++ 9 files changed, 83 insertions(+), 15 deletions(-) create mode 100755 .github/scripts/release-dry-run.sh rename _version.py => craftgate/_version.py (100%) create mode 100644 tests/test_client_version.py diff --git a/.github/scripts/release-dry-run.sh b/.github/scripts/release-dry-run.sh new file mode 100755 index 0000000..2d67502 --- /dev/null +++ b/.github/scripts/release-dry-run.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Builds the package the way the release workflow does for version $1, without committing, tagging or publishing, +# installs the wheel into a fresh virtualenv and checks the x-client-version header. The check runs next to an +# unrelated _version.py, like an application that has its own, to make sure the client reads its own version. +set -euo pipefail +version="${1:?usage: release-dry-run.sh }" +repo_dir="$(cd "$(dirname "$0")/../.." && pwd)" +work_dir="$(mktemp -d)" +trap 'rm -rf "$work_dir"' EXIT + +mkdir "$work_dir/source" +git -C "$repo_dir" archive HEAD | tar -x -C "$work_dir/source" +expected="$(python "$work_dir/source/scripts/update_version.py" "$version")" +python -m build --wheel --outdir "$work_dir/dist" "$work_dir/source" + +python -m venv "$work_dir/venv" +"$work_dir/venv/bin/pip" install --quiet "$work_dir"/dist/*.whl + +mkdir "$work_dir/app" +echo 'VERSION = "application-version"' > "$work_dir/app/_version.py" +cat > "$work_dir/app/check.py" <<'PY' +import sys + +from craftgate import RequestOptions +from craftgate.adapter.base_adapter import BaseAdapter + +adapter = BaseAdapter(RequestOptions(api_key="api-key", secret_key="secret-key", base_url="http://localhost")) +actual = adapter._create_headers(None, "/payment/v1/card-payments")["x-client-version"] +expected = "craftgate-python-client:" + sys.argv[1] +if actual != expected: + sys.exit("Expected header %r, got %r" % (expected, actual)) +print("OK x-client-version: " + actual) +PY +(cd "$work_dir/app" && "$work_dir/venv/bin/python" check.py "$expected") diff --git a/.github/workflows/craftgate-build.yml b/.github/workflows/craftgate-build.yml index f8a397d..1f4fb6b 100644 --- a/.github/workflows/craftgate-build.yml +++ b/.github/workflows/craftgate-build.yml @@ -15,10 +15,12 @@ jobs: - "3.10" - "3.11" - "3.12" + - "3.13" + - "3.14" steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 @@ -39,7 +41,7 @@ jobs: run: python -m compileall craftgate - name: Run unit tests - run: python -m unittest tests.test_idempotency -v + run: python -m unittest tests.test_idempotency tests.test_client_version -v - name: Build distribution and verify metadata if: matrix.python-version == '3.12' @@ -49,5 +51,11 @@ jobs: python -m build twine check dist/* + # Builds and installs the package as the release workflow would for v9.9.9, without publishing anything. + - name: Release dry run + run: | + python -m pip install build + .github/scripts/release-dry-run.sh v9.9.9 + - name: Skip integration tests run: echo "Integration tests rely on live credentials and are intentionally skipped in CI." diff --git a/.github/workflows/craftgate-release.yml b/.github/workflows/craftgate-release.yml index bc50901..4704848 100644 --- a/.github/workflows/craftgate-release.yml +++ b/.github/workflows/craftgate-release.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 ssh-key: ${{ secrets.SSH_KEY }} @@ -60,7 +60,7 @@ jobs: run: | git config --global user.name "craftgate-sdlc" git config --global user.email "sdlc@craftgate.io" - git add _version.py + git add craftgate/_version.py if git diff --cached --quiet; then echo "::error::No changes detected. Is the requested version already released?" exit 1 @@ -86,7 +86,7 @@ jobs: run: twine upload dist/* - name: Create GitHub release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v3 with: tag_name: v${{ steps.bump_version.outputs.version }} name: v${{ steps.bump_version.outputs.version }} diff --git a/README.md b/README.md index 485a457..8dea3b4 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ A variety of end-to-end samples (3DS, Checkout, APM, refunds, stored cards, mark Run a single test: ~~~bash -python -m unittest tests/test_payment_sample.py::PaymentSample::test_create_payment +python -m unittest tests.test_payment_sample.PaymentSample.test_create_payment ~~~ ## Contributions diff --git a/_version.py b/craftgate/_version.py similarity index 100% rename from _version.py rename to craftgate/_version.py diff --git a/craftgate/adapter/base_adapter.py b/craftgate/adapter/base_adapter.py index 7590aae..5969792 100644 --- a/craftgate/adapter/base_adapter.py +++ b/craftgate/adapter/base_adapter.py @@ -1,7 +1,7 @@ import uuid from typing import Dict, Optional -from _version import VERSION +from craftgate._version import VERSION from craftgate.request.common.base_request import BaseRequest from craftgate.request.common.header_options import HeaderOptions from craftgate.request_options import RequestOptions diff --git a/pyproject.toml b/pyproject.toml index 29f13e0..b3e4e21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: PyPy", "Typing :: Typed", "Topic :: Software Development :: Libraries :: Python Modules", @@ -43,9 +45,6 @@ Documentation = "https://developer.craftgate.io" Source = "https://github.com/craftgate/craftgate-python-client" Issues = "https://github.com/craftgate/craftgate-python-client/issues" -[tool.setuptools] -py-modules = ["_version"] - [tool.setuptools.packages.find] include = ["craftgate*"] exclude = ["tests", "tests.*"] @@ -54,4 +53,4 @@ exclude = ["tests", "tests.*"] craftgate = ["py.typed"] [tool.setuptools.dynamic] -version = { attr = "_version.VERSION" } +version = { attr = "craftgate._version.VERSION" } diff --git a/scripts/update_version.py b/scripts/update_version.py index 47073f5..d6f3f7e 100644 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -6,8 +6,9 @@ The script normalizes the provided version (strips a leading "v" if present), validates it loosely against SemVer rules and writes the value into the -_version.py file that is consumed by setuptools. It prints the normalized -version to stdout for downstream tooling. +craftgate/_version.py file that is consumed by setuptools and sent in the +x-client-version header. It prints the normalized version to stdout for +downstream tooling. """ from __future__ import annotations @@ -19,11 +20,11 @@ VERSION_PATTERN = re.compile(r"^v?(?P\d+\.\d+\.\d+(?:[0-9A-Za-z.\-+_]*)?)$") PROJECT_ROOT = Path(__file__).resolve().parent.parent -VERSION_FILE = PROJECT_ROOT / "_version.py" +VERSION_FILE = PROJECT_ROOT / "craftgate" / "_version.py" def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Update _version.py") + parser = argparse.ArgumentParser(description="Update craftgate/_version.py") parser.add_argument( "version", help="New version string (e.g. 1.2.3 or v1.2.3).", diff --git a/tests/test_client_version.py b/tests/test_client_version.py new file mode 100644 index 0000000..5b682fd --- /dev/null +++ b/tests/test_client_version.py @@ -0,0 +1,26 @@ +import os +import unittest + +import craftgate +from craftgate import RequestOptions +from craftgate._version import VERSION +from craftgate.adapter.base_adapter import BaseAdapter + + +class ClientVersionTest(unittest.TestCase): + + def test_version_module_is_part_of_the_package(self): + version_file = os.path.join(os.path.dirname(craftgate.__file__), "_version.py") + self.assertTrue(os.path.isfile(version_file)) + + def test_should_send_client_version_header(self): + adapter = BaseAdapter(RequestOptions(api_key="api-key", secret_key="secret-key", + base_url="https://sandbox-api.craftgate.io")) + + headers = adapter._create_headers(None, "/payment/v1/card-payments") + + self.assertEqual("craftgate-python-client:" + VERSION, headers["x-client-version"]) + + +if __name__ == "__main__": + unittest.main()