Skip to content
Open
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
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,22 @@ jobs:

- name: Run tests
run: pytest

lint:
name: Lint (mypy)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"

- name: Run mypy
run: mypy
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ after every change and leave them green:
```bash
pip install -e ".[dev]" # ci.setup
pytest # ci.test
mypy # ci.lint
```

What "done" means (PR audit, testing tiers, UAT proof) is defined in the KB:
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Typed taxonomy exports so integrations use SDK enums instead of magic
strings: `EventSlug` (built-in ingestion slugs) and the status vocabularies
`ConversionStatus`, `TransactionStatus`, `ObligationStatus`, `PayoutStatus`,
`FulfillmentStatus`, `OpportunityStatus`, `ApiKeyStatus`, and
`WebhookSubscriptionStatus`.
- `WebhookEventType`: added the missing `CREDIT_ISSUED`, `CREDIT_REDEEMED`,
`CURRENCY_CREATED`, and `CURRENCY_DELETED` events (also added to
`openapi.yaml`), matching the full set Siren dispatches.
- mypy strict type checking wired as the repo's lint command (`mypy`),
declared in `navigator.yaml` `ci.lint` and enforced by a CI lint job.

## [0.1.0] - 2026-07-11

### Added
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@ client.webhooks.subscriptions.delete(subscription.id)
- **Automatic retries** — exponential backoff on network errors and 429/5xx for
idempotent reads and event ingestion (never for secret-minting writes).
- **Fully typed** — ships `py.typed`; works with mypy and Pyright out of the box.
- **Typed taxonomy** — Siren's domain vocabulary as enums, so no magic strings
cross the boundary: `WebhookEventType`, `EventSlug`, and the status
vocabularies (`ConversionStatus`, `TransactionStatus`, `ObligationStatus`,
`PayoutStatus`, `FulfillmentStatus`, `OpportunityStatus`, `ApiKeyStatus`,
`WebhookSubscriptionStatus`).

### API keys

Expand All @@ -184,7 +189,8 @@ print(page.total)

client.transactions.list()
client.obligations.list()
client.payouts.list(status="paid") # extra filters pass through as query args
# Extra filters pass through as query args; use the typed taxonomy for values.
client.payouts.list(status=siren.PayoutStatus.PAID)
```

### Errors
Expand Down
1 change: 1 addition & 0 deletions navigator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ initiative: siren
ci:
setup: pip install -e ".[dev]"
test: pytest
lint: mypy
knowledgeDependencies:
- initiative: phpnomad
description: "Org-wide engineering doctrine"
Expand Down
4 changes: 4 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,10 @@ components:
- conversion.rejected
- conversion.renewed
- coupon.applied
- credit.issued
- credit.redeemed
- currency.created
- currency.deleted
- distribution.completed
- engagement.awarded
- engagement.completed
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Documentation = "https://sirenaffiliates.com"
Issues = "https://github.com/Novatorius/siren-python/issues"

[project.optional-dependencies]
dev = ["pytest>=7.0", "respx>=0.21", "build>=1.0"]
dev = ["pytest>=7.0", "respx>=0.21", "build>=1.0", "mypy>=1.8"]

[tool.hatch.build.targets.wheel]
packages = ["src/siren"]
Expand All @@ -64,3 +64,7 @@ include = [
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-q"

[tool.mypy]
strict = true
files = ["src/siren"]
19 changes: 19 additions & 0 deletions src/siren/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,20 @@
)
from .types import (
ApiKey,
ApiKeyStatus,
ConversionStatus,
EventResult,
EventSlug,
FulfillmentStatus,
ListPage,
ObligationStatus,
OpportunityStatus,
PayoutStatus,
TransactionStatus,
WebhookEvent,
WebhookEventType,
WebhookSubscription,
WebhookSubscriptionStatus,
)

__all__ = [
Expand All @@ -59,4 +68,14 @@
"WebhookEvent",
"WebhookEventType",
"WebhookSubscription",
# Taxonomy
"ApiKeyStatus",
"ConversionStatus",
"EventSlug",
"FulfillmentStatus",
"ObligationStatus",
"OpportunityStatus",
"PayoutStatus",
"TransactionStatus",
"WebhookSubscriptionStatus",
]
2 changes: 1 addition & 1 deletion src/siren/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def request(
def _backoff_delay(self, attempt: int, error: SirenError) -> float:
if isinstance(error, RateLimitError) and error.retry_after is not None:
return float(error.retry_after)
return min(_INITIAL_BACKOFF * (2**attempt), _MAX_BACKOFF)
return float(min(_INITIAL_BACKOFF * (2**attempt), _MAX_BACKOFF))


def parse_json(response: httpx.Response) -> Any:
Expand Down
132 changes: 131 additions & 1 deletion src/siren/types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
"""Typed result objects and the webhook event-type catalog."""
"""Typed result objects and Siren's domain taxonomy (event and status catalogs).

Siren owns these vocabularies; this module is the typed source for them so
integrations never hand-roll magic strings.
"""

from __future__ import annotations

Expand All @@ -19,6 +23,10 @@ class WebhookEventType(str, Enum):
CONVERSION_REJECTED = "conversion.rejected"
CONVERSION_RENEWED = "conversion.renewed"
COUPON_APPLIED = "coupon.applied"
CREDIT_ISSUED = "credit.issued"
CREDIT_REDEEMED = "credit.redeemed"
CURRENCY_CREATED = "currency.created"
CURRENCY_DELETED = "currency.deleted"
DISTRIBUTION_COMPLETED = "distribution.completed"
ENGAGEMENT_AWARDED = "engagement.awarded"
ENGAGEMENT_COMPLETED = "engagement.completed"
Expand All @@ -43,6 +51,128 @@ def __str__(self) -> str:
return self.value


class EventSlug(str, Enum):
"""URL slugs for the built-in ingestion event types (``POST /event/{slug}``).

``client.events.sale()`` / ``.refund()`` / ``.site_visited()`` already use
these internally; the members exist for code that routes slugs dynamically
(e.g. wrapping ``client.events.ingest``).
"""

SALE = "sale"
REFUND = "refund"
SITE_VISITED = "site-visited"

def __str__(self) -> str:
return self.value


class ConversionStatus(str, Enum):
"""Statuses a conversion can hold (``client.conversions`` records and
``conversion.*`` webhook payloads)."""

PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
EXPIRED = "expired"
#: Soft-delete bucket written by the bulk delete action.
DELETED = "deleted"

def __str__(self) -> str:
return self.value


class TransactionStatus(str, Enum):
"""Statuses a transaction can hold (``client.transactions`` records and
``transaction.*`` webhook payloads)."""

COMPLETE = "complete"
CANCELLED = "cancelled"
REFUNDED = "refunded"

def __str__(self) -> str:
return self.value


class ObligationStatus(str, Enum):
"""Statuses an obligation can hold (``client.obligations`` records and
``obligation.*`` webhook payloads).

Note: Siren's machine paths (fulfillment generation, bulk actions) write
``complete``, while its management REST surface accepts ``fulfilled`` —
both appear in the wild, so both are listed here.
"""

PENDING = "pending"
COMPLETE = "complete"
FULFILLED = "fulfilled"
CANCELLED = "cancelled"

def __str__(self) -> str:
return self.value


class PayoutStatus(str, Enum):
"""Statuses a payout can hold (``client.payouts`` records and
``payout.*`` webhook payloads)."""

UNPAID = "unpaid"
PROCESSING = "processing"
PAID = "paid"
FAILED = "failed"

def __str__(self) -> str:
return self.value


class FulfillmentStatus(str, Enum):
"""Statuses a fulfillment can hold (``fulfillment.created`` /
``fulfillment.updated`` webhook payloads)."""

PENDING = "pending"
PROCESSING = "processing"
COMPLETE = "complete"
FAILED = "failed"

def __str__(self) -> str:
return self.value


class OpportunityStatus(str, Enum):
"""Statuses an opportunity can hold (``opportunity.created`` /
``opportunity.invalidated`` webhook payloads; the ``tracking_id`` on a
sale refers to an opportunity)."""

ACTIVE = "active"
INACTIVE = "inactive"
#: Set by Siren's invalidation service; never operator-settable.
INVALID = "invalid"

def __str__(self) -> str:
return self.value


class ApiKeyStatus(str, Enum):
"""Statuses an API key can hold (``client.api_keys`` records)."""

ACTIVE = "active"
REVOKED = "revoked"

def __str__(self) -> str:
return self.value


class WebhookSubscriptionStatus(str, Enum):
"""Statuses a webhook subscription can hold
(``client.webhooks.subscriptions`` records)."""

ACTIVE = "active"
PAUSED = "paused"

def __str__(self) -> str:
return self.value


@dataclass(frozen=True)
class EventResult:
"""Result of an event ingestion call.
Expand Down
4 changes: 2 additions & 2 deletions src/siren/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import hmac
import json
from enum import Enum
from typing import Any, List, Optional, Sequence, Union
from typing import Any, Dict, List, Optional, Sequence, Union

from ._client import HttpClient, parse_json
from .errors import SignatureVerificationError
Expand Down Expand Up @@ -48,7 +48,7 @@ def create(
store it; it cannot be retrieved later. Pass
``events=[WebhookEventType.ALL]`` (``["*"]``) to subscribe to everything.
"""
payload: dict = {
payload: Dict[str, Any] = {
"targetUrl": target_url,
"events": [_event_value(event) for event in events],
}
Expand Down
Loading
Loading