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/scripts/release-dry-run.sh
Original file line number Diff line number Diff line change
@@ -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 <version>}"
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")
12 changes: 10 additions & 2 deletions .github/workflows/craftgate-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand All @@ -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."
6 changes: 3 additions & 3 deletions .github/workflows/craftgate-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand All @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion craftgate/adapter/base_adapter.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 3 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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.*"]
Expand All @@ -54,4 +53,4 @@ exclude = ["tests", "tests.*"]
craftgate = ["py.typed"]

[tool.setuptools.dynamic]
version = { attr = "_version.VERSION" }
version = { attr = "craftgate._version.VERSION" }
9 changes: 5 additions & 4 deletions scripts/update_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,11 +20,11 @@

VERSION_PATTERN = re.compile(r"^v?(?P<num>\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).",
Expand Down
26 changes: 26 additions & 0 deletions tests/test_client_version.py
Original file line number Diff line number Diff line change
@@ -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()