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
74 changes: 74 additions & 0 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
name: Publish PyPI package

on:
push:
tags:
- "v*.*.*"

jobs:
publish:
runs-on: ubuntu-latest
environment:
name: pypi-release
url: https://pypi.org/p/supertab-connect-sdk
permissions:
contents: read
id-token: write

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Validate release tag format
run: |
if [[ ! "${GITHUB_REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then
echo "Tag '${GITHUB_REF_NAME}' is not valid semver"
exit 1
fi

- name: Verify tag version matches pyproject.toml
run: |
TAG_VERSION="${GITHUB_REF_NAME#v}"
PACKAGE_VERSION="$(python - <<'PY'
import tomllib
with open("pyproject.toml", "rb") as f:
print(tomllib.load(f)["project"]["version"])
PY
)"
if [ "${TAG_VERSION}" != "${PACKAGE_VERSION}" ]; then
echo "Tag version (${TAG_VERSION}) does not match pyproject.toml version (${PACKAGE_VERSION})"
exit 1
fi

- name: Sync dependencies
run: uv sync --locked --extra dev

- name: Check formatting
run: uv run ruff format --check .

- name: Run linting
run: uv run ruff check .

- name: Run type checking
run: uv run ty check

- name: Run tests
run: uv run pytest

- name: Build package
run: |
rm -rf dist
uv run python -m build --no-isolation

- name: Check package metadata
run: uv run twine check dist/*

- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ __pycache__/

.venv/
venv/
.uv-cache/

build/
dist/
Expand Down
120 changes: 104 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,116 @@
# Supertab Connect SDK

Python SDK for Supertab Connect.
Python SDK for [Supertab Connect](https://www.supertab.co/supertab-connect).

## Usage
Use this package to obtain Supertab license tokens on the customer side and verify
or enforce them on the merchant side.

Check the `examples` folder to get the idea of how to use the provided functionality.
## Installation

Dedicated SDK docs portal is coming soon.
```bash
pip install supertab-connect-sdk
```

Requires Python 3.12 or newer.

## Customer Usage

Obtain a license token for a resource URL:

```python
import asyncio

from supertab_connect import obtain_license_token


async def main() -> None:
token = await obtain_license_token(
client_id="your_client_id",
client_secret="your_client_secret",
resource_url="https://example.com/premium/article",
)

if token is None:
print("No token required for this usage")
return

print(token)


asyncio.run(main())
```

The SDK fetches `license.xml` from the resource origin, finds the best matching
`<content>` entry, and exchanges the client credentials for a license token.

## Merchant Usage

Verify and record license-token usage:

```python
import asyncio

from supertab_connect import SupertabConnect, SupertabConnectConfig


async def main() -> None:
client = SupertabConnect(
SupertabConnectConfig(
api_key="your_api_key",
)
)

async with client:
result = await client.verify_and_record(
token="your.jwt.token",
resource_url="https://example.com/premium/article",
user_agent="Mozilla/5.0",
request_headers={"Accept": "text/html"},
)

if not result.valid:
print(f"DENY access: {result.error}")
return

print("ALLOW access")


asyncio.run(main())
```

For request-level enforcement, use `SupertabConnect.handle_request()` with an
`httpx.Request`. See the `examples` directory for complete merchant and customer
examples.

## Error Handling

Customer-side token retrieval raises `SupertabConnectError` when `license.xml`
cannot be fetched or parsed, no matching content block exists, or the token
endpoint fails.

Merchant-side token verification returns typed result objects instead of raising
for normal invalid-token cases. Invalid tokens include a reason and a human
readable error.

## Typing

This package ships inline type hints and includes a `py.typed` marker for type
checkers.

## Documentation

See the [Supertab Connect Python SDK docs](https://supertab-connect.mintlify.app/reference/sdk/python)
for the full API reference.

## Development

This project uses `hatchling` as the build backend.

See [DEVELOPMENT.md](DEVELOPMENT.md) for local setup, Git hooks, and CI-aligned development commands.

## Package Layout

```text
.
├── connect
│ ├── customer
│ └── merchant
├── examples
│ ├── obtain_license_token.py
│ └── obtain_and_verify_license_token.py
└── tests
```
## Links

- [Documentation](https://supertab-connect.mintlify.app/reference/sdk/python)
- [Repository](https://github.com/getsupertab/connect-sdk-python)
- [Issues](https://github.com/getsupertab/connect-sdk-python/issues)
- [License](LICENSE)
6 changes: 0 additions & 6 deletions connect/customer/__init__.py

This file was deleted.

2 changes: 1 addition & 1 deletion examples/merchant_handle_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import httpx

from connect import EnforcementMode, HandlerAction, SupertabConnect, SupertabConnectConfig
from supertab_connect import EnforcementMode, HandlerAction, SupertabConnect, SupertabConnectConfig

logging.basicConfig(level=logging.DEBUG)

Expand Down
2 changes: 1 addition & 1 deletion examples/merchant_verify_and_record_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import asyncio
import logging

from connect import EnforcementMode, SupertabConnect, SupertabConnectConfig
from supertab_connect import EnforcementMode, SupertabConnect, SupertabConnectConfig

logging.basicConfig(level=logging.DEBUG)

Expand Down
4 changes: 2 additions & 2 deletions examples/obtain_and_verify_license_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import asyncio
import logging

from connect import obtain_license_token, verify_license_token
from connect.types import InvalidLicenseToken, ValidLicenseToken
from supertab_connect import obtain_license_token, verify_license_token
from supertab_connect.types import InvalidLicenseToken, ValidLicenseToken

logging.basicConfig(level=logging.DEBUG)

Expand Down
2 changes: 1 addition & 1 deletion examples/obtain_license_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import asyncio
import logging

from connect import obtain_license_token
from supertab_connect import obtain_license_token

logging.basicConfig(level=logging.DEBUG)

Expand Down
30 changes: 26 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@ build-backend = "hatchling.build"

[project]
name = "supertab-connect-sdk"
version = "0.0.1"
version = "0.1.0"
authors = [
{ name = "Supertab", email = "hello@supertab.co" },
]
description = "Supertab Connect SDK"
readme = "README.md"
requires-python = ">=3.12"
classifiers = [
"License :: OSI Approved :: MIT License",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
]
Comment thread
nick434434 marked this conversation as resolved.
license = "MIT"
license-files = ["LICEN[CS]E*"]
Expand All @@ -28,18 +31,21 @@ dependencies = [
[project.optional-dependencies]
dev = [
"build>=1.4.2",
"hatchling>=1.26",
"prek>=0.3.8",
"pytest>=8,<9",
"pytest-asyncio>=0.24",
"respx>=0.22",
"ruff>=0.15.7",
"twine>=6",
"ty>=0.0.25",
]

[project.urls]
Homepage = "https://www.supertab.co/supertab-connect"
Repository = "https://github.com/getsupertab/connect-sdk-python"
Issues = "https://github.com/getsupertab/connect-sdk-python/issues"
Documentation = "https://supertab-connect.mintlify.app/reference/sdk/python"

[tool.pytest.ini_options]
asyncio_mode = "auto"
Expand All @@ -48,4 +54,20 @@ asyncio_mode = "auto"
line-length = 119

[tool.hatch.build.targets.wheel]
packages = ["connect"]
packages = ["supertab_connect"]

[tool.hatch.build.targets.sdist]
exclude = [
"/.github",
"/.idea",
"/.pytest_cache",
"/.ruff_cache",
"/.uv-cache",
"/.venv",
"/build",
"/dist",
"/uv.lock",
"/prek.toml",
"/DEVELOPMENT.md",
]

12 changes: 6 additions & 6 deletions connect/__init__.py → supertab_connect/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Supertab Connect SDK."""

from connect.customer.token import obtain_license_token
from connect.exceptions import SupertabConnectError
from connect.merchant.bots import default_bot_detector
from connect.merchant.client import SupertabConnect
from connect.merchant.license import verify_license_token
from connect.types import (
from supertab_connect.customer.token import obtain_license_token
from supertab_connect.exceptions import SupertabConnectError
from supertab_connect.merchant.bots import default_bot_detector
from supertab_connect.merchant.client import SupertabConnect
from supertab_connect.merchant.license import verify_license_token
from supertab_connect.types import (
EnforcementMode,
HandlerAction,
HandlerResult,
Expand Down
File renamed without changes.
File renamed without changes.
6 changes: 6 additions & 0 deletions supertab_connect/customer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Customer functionality for Supertab Connect."""

from supertab_connect.customer.token import obtain_license_token
from supertab_connect.types import UsageType

__all__ = ["UsageType", "obtain_license_token"]
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import urllib.parse

from connect.common import debug_log
from connect.url_pattern import score_path_pattern
from connect.customer.content_parser import _ContentBlock
from supertab_connect.common import debug_log
from supertab_connect.url_pattern import score_path_pattern
from supertab_connect.customer.content_parser import _ContentBlock


def _find_best_matching_content(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from dataclasses import dataclass
from xml.etree import ElementTree

from connect.common import debug_log
from supertab_connect.common import debug_log

_RSL_NAMESPACE = "https://rslstandard.org/rsl"
_NS = {"rsl": _RSL_NAMESPACE}
Expand Down
12 changes: 6 additions & 6 deletions connect/customer/token.py → supertab_connect/customer/token.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@
from cryptography.hazmat.primitives.asymmetric import ec, rsa
from cryptography.hazmat.primitives.serialization import load_pem_private_key

from connect.common import debug_log, error_log
from connect.exceptions import SupertabConnectError
from connect.customer.content_matcher import _find_best_matching_content
from connect.customer.content_parser import _ContentBlock
from connect.customer.content_parser import _parse_content_elements
from connect.types import UsageType
from supertab_connect.common import debug_log, error_log
from supertab_connect.exceptions import SupertabConnectError
from supertab_connect.customer.content_matcher import _find_best_matching_content
from supertab_connect.customer.content_parser import _ContentBlock
from supertab_connect.customer.content_parser import _parse_content_elements
from supertab_connect.types import UsageType

_SUPPORTED_ALGS = ("ES256", "RS256")
_DEFAULT_HTTP_TIMEOUT_SECONDS = 10.0
Expand Down
File renamed without changes.
Loading
Loading