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
168 changes: 168 additions & 0 deletions .github/workflows/regen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
name: Regenerate SDK

# Canonical SDK-regen workflow. Copy this file into each SDK repo at
# `.github/workflows/regen.yml`; the only per-SDK customization needed is
# inserting a language toolchain step (e.g. setup-python / setup-go / etc.)
# below the "Toolchain (language SDK)" marker.
#
# Triggers:
# workflow_dispatch — manual run with optional inputs
# repository_dispatch — fired from goodsender-web after spec changes
# (event type: regen-sdk;
# client_payload: spec_url, spec_tag, clean, generator_version)

on:
workflow_dispatch:
inputs:
spec_url:
description: "URL to download bundled openapi spec from. If empty, regenerate from openapi/goodsender.yaml in repo."
required: false
type: string
clean:
description: "Pass --clean to scripts/regen.sh (full wipe before regen)."
required: false
type: boolean
default: false
generator_version:
description: "Override openapi-generator JAR version (default 7.21.0 from scripts/regen.sh)."
required: false
type: string
repository_dispatch:
types: [regen-sdk]
# Test trigger: pushes to any `ci/**` branch run this workflow on the branch's code,
# so regen + tests + PR creation can be smoke-tested before merging to main.
push:
branches:
- 'ci/**'

permissions:
contents: write
pull-requests: write

jobs:
regen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

# --- Toolchain (regen driver): Node runs the openapi-generator-cli wrapper
- uses: actions/setup-node@v4
with:
node-version: "20"

# --- Toolchain (regen driver): Java runs the openapi-generator JAR
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"

# --- Toolchain (language SDK): inserted per repo (Python / Go / Ruby / etc.)
# See per-SDK task in docs/superpowers/plans/2026-05-19-sdk-regen-automation.md
- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Cache openapi-generator JAR + npm
uses: actions/cache@v4
with:
path: |
~/.openapi-generator
~/.npm
key: openapi-gen-${{ runner.os }}-${{ hashFiles('openapitools.json') }}

- name: Resolve inputs
id: in
env:
DISPATCH_SPEC_URL: ${{ github.event.inputs.spec_url }}
DISPATCH_CLEAN: ${{ github.event.inputs.clean }}
DISPATCH_GEN_VER: ${{ github.event.inputs.generator_version }}
PAYLOAD_SPEC_URL: ${{ github.event.client_payload.spec_url }}
PAYLOAD_SPEC_TAG: ${{ github.event.client_payload.spec_tag }}
PAYLOAD_CLEAN: ${{ github.event.client_payload.clean }}
PAYLOAD_GEN_VER: ${{ github.event.client_payload.generator_version }}
run: |
set -euo pipefail
spec_url="${DISPATCH_SPEC_URL:-${PAYLOAD_SPEC_URL:-}}"
spec_tag="${PAYLOAD_SPEC_TAG:-}"
clean="${DISPATCH_CLEAN:-${PAYLOAD_CLEAN:-false}}"
gen_ver="${DISPATCH_GEN_VER:-${PAYLOAD_GEN_VER:-}}"
[[ "$clean" != "true" ]] && clean="false"
{
echo "spec_url<<EOF"; printf '%s\n' "$spec_url"; echo "EOF"
echo "spec_tag=$spec_tag"
echo "clean=$clean"
echo "gen_ver<<EOF"; printf '%s\n' "$gen_ver"; echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Update spec from URL
if: steps.in.outputs.spec_url != ''
env:
SPEC_URL: ${{ steps.in.outputs.spec_url }}
run: |
set -euo pipefail
curl --fail --location --show-error --silent "$SPEC_URL" -o openapi/goodsender.yaml

- name: Regenerate
env:
OPENAPI_GENERATOR_VERSION: ${{ steps.in.outputs.gen_ver }}
CLEAN: ${{ steps.in.outputs.clean }}
run: |
set -euo pipefail
if [[ "$CLEAN" == "true" ]]; then
bash scripts/regen.sh --clean
else
bash scripts/regen.sh
fi

- name: Run conformance tests (mock)
id: tests
continue-on-error: true
run: bash tests/run.sh mock

- name: Detect changes
id: diff
run: |
if [[ -z "$(git status --porcelain)" ]]; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
git --no-pager diff --stat | tee -a "$GITHUB_STEP_SUMMARY"
fi

- name: Mint PR-creation token from GitHub App
if: steps.diff.outputs.changed == 'true'
id: pr_token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.SDK_DISPATCH_APP_ID }}
private-key: ${{ secrets.SDK_DISPATCH_APP_PRIVATE_KEY }}

- name: Open PR
if: steps.diff.outputs.changed == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ steps.pr_token.outputs.token }}
commit-message: "chore: regenerate SDK from openapi spec ${{ steps.in.outputs.spec_tag || 'in-repo' }}"
title: "chore: regenerate SDK from openapi spec ${{ steps.in.outputs.spec_tag || 'in-repo' }}"
body: |
Automated regeneration via `.github/workflows/regen.yml`.

- Trigger: `${{ github.event_name }}`
- Spec tag: `${{ steps.in.outputs.spec_tag || '(none — in-repo spec used)' }}`
- Spec URL: `${{ steps.in.outputs.spec_url || '(none)' }}`
- Clean: `${{ steps.in.outputs.clean }}`
- Generator version: `${{ steps.in.outputs.gen_ver || '(default from scripts/regen.sh)' }}`
- Conformance tests (`tests/run.sh mock`): **${{ steps.tests.outcome }}**
branch: auto/regen
delete-branch: true
labels: automated, regen

- name: Fail workflow if conformance tests failed
if: always() && steps.tests.outcome == 'failure'
run: |
echo "Conformance tests failed — see the 'Run conformance tests (mock)' step above."
echo "The regen PR (if any diff existed) has still been opened so the change is visible for review."
echo "This workflow run is marked failed so the failure surfaces on the PR's checks list."
exit 1
2 changes: 2 additions & 0 deletions .openapi-generator-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ tests/**
scripts/**
openapi/**
openapitools.json
.github/**
.regen-ignore
24 changes: 0 additions & 24 deletions .openapi-generator/FILES
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
.github/workflows/python.yml
.gitignore
.gitlab-ci.yml
.travis.yml
Expand Down Expand Up @@ -64,27 +63,4 @@ setup.cfg
setup.py
test-requirements.txt
test/__init__.py
test/test_address.py
test/test_attachment.py
test/test_consent_email_entry.py
test/test_consent_email_recipient.py
test/test_consent_email_request.py
test/test_consent_email_result.py
test/test_domain.py
test/test_domain_list_response.py
test/test_domain_verification.py
test/test_domains_api.py
test/test_email_account.py
test/test_email_list_response.py
test/test_emails_api.py
test/test_error_response.py
test/test_quota_exceeded_error.py
test/test_send_email.py
test/test_send_email_error_response.py
test/test_send_email_request.py
test/test_send_email_response.py
test/test_template_email_request.py
test/test_template_email_request_template.py
test/test_template_email_response.py
test/test_tracking_settings.py
tox.ini
21 changes: 21 additions & 0 deletions .regen-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Files preserved across regenerations.
# scripts/regen.sh consumes this file in two ways:
# 1. Filtered (comments + blank lines stripped) and written to
# .openapi-generator-ignore, which openapi-generator reads to know
# which paths it must NOT overwrite.
# 2. The first path component of every pattern is added to --clean's
# preserve list, so `regen.sh --clean` will not wipe these top-level
# directories or files.
#
# Patterns are gitignore-style. Lines starting with `#` and blank lines are
# treated as comments.

LICENSE
CHANGELOG.md
README.md
tests/**
scripts/**
openapi/**
openapitools.json
.github/**
.regen-ignore
91 changes: 91 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# GoodSender SDK for Python

Official client library for the GoodSender email API. Package: `goodsender`

## Installation

```bash
pip install goodsender
```

## Quick start

```python
import goodsender
from goodsender.api.emails_api import EmailsApi
from goodsender.api.domains_api import DomainsApi

cfg = goodsender.Configuration(
host="https://api.goodsender.com",
access_token="YOUR_API_KEY",
)
api = goodsender.ApiClient(cfg)
emails = EmailsApi(api)
domains = DomainsApi(api)

req = goodsender.SendEmailRequest(
emails=[
goodsender.SendEmail(
var_from=goodsender.Address(email="sender@example.com"),
to=[goodsender.Address(email="recipient@example.com")],
subject="Hello",
text_content="Body",
)
]
)
res = emails.send_email(send_email_request=req)
print(f"sent={res.sent} declined={res.declined}")
```

## Examples

### Send via a template

```python
req = goodsender.TemplateEmailRequest(
var_from=goodsender.Address(email="sender@example.com"),
to=goodsender.Address(email="recipient@example.com"),
subject="Your OTP",
template=goodsender.TemplateEmailRequestTemplate(
template_id="otp_code", variables={"code": "123456"}
),
)
res = emails.send_template_email(template_email_request=req)
print(f"status={res.status}")
```

### List domains

```python
res = domains.list_domains(limit=50)
for d in res.domains:
print(d)
```

### Check consent status

```python
# Get all consent records for an address
res = emails.get_email_consent_status(email="user@example.com", domain="example.com")
print(f"entries={len(res)}")

# List all consents for a domain
res = emails.list_email_consents(domain="example.com", limit=50)
print(f"emails={len(res.emails or [])}")
```

## Documentation

- API reference: <https://api.goodsender.com/docs>
- OpenAPI spec: `openapi/goodsender.yaml` in this repo
- Conformance tests: `tests/`

## Development

- Regenerate from spec: `scripts/regen.sh` (preserves `tests/`, `.github/`, and hand-curated files per `.regen-ignore`)
- Run conformance tests against local mock: `tests/run.sh mock`
- Run conformance against real dev API: `tests/run.sh dev` (requires `tests/.env.dev`)

## License

MIT — see [LICENSE](LICENSE).
42 changes: 19 additions & 23 deletions scripts/regen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,27 @@ export OPENAPI_GENERATOR_VERSION="${OPENAPI_GENERATOR_VERSION:-7.21.0}"
[[ -f "$SPEC" ]] || { echo "!! spec not found at $SPEC" >&2; exit 1; }
[[ -f "$CONFIG" ]] || { echo "!! generator config not found at $CONFIG" >&2; exit 1; }

# --- .openapi-generator-ignore: protect hand-curated files from being overwritten
cat > "$SDK_DIR/.openapi-generator-ignore" <<'IGNORE_EOF'
LICENSE
CHANGELOG.md
README.md
tests/**
scripts/**
openapi/**
openapitools.json
IGNORE_EOF
# --- Sync .openapi-generator-ignore from .regen-ignore
# .regen-ignore is the single source of truth for preserved paths. We filter
# comments + blanks and write the result to .openapi-generator-ignore (which
# openapi-generator reads). The same patterns drive --clean's preserve list.
REGEN_IGNORE="$SDK_DIR/.regen-ignore"
[[ -f "$REGEN_IGNORE" ]] || { echo "!! .regen-ignore not found at $REGEN_IGNORE" >&2; exit 1; }
grep -v '^[[:space:]]*#' "$REGEN_IGNORE" | grep -v '^[[:space:]]*$' > "$SDK_DIR/.openapi-generator-ignore"

# --- Optional clean step: wipe SDK output (preserves hand-curated files only)
# --- Optional clean step: wipe SDK output (preserves entries in .regen-ignore)
if (( CLEAN == 1 )); then
echo ">> --clean: wiping $SDK_DIR (preserving LICENSE, CHANGELOG.md, README.md, tests/, scripts/, openapi/, openapitools.json)"
find "$SDK_DIR" -mindepth 1 -maxdepth 1 \
! -name LICENSE \
! -name CHANGELOG.md \
! -name README.md \
! -name tests \
! -name scripts \
! -name openapi \
! -name openapitools.json \
! -name .git \
! -name .gitignore \
-exec rm -rf {} +
preserve_args=()
while IFS= read -r line; do
line="${line%%#*}"
line="$(echo "$line" | xargs)"
[[ -z "$line" ]] && continue
top="${line%%/*}"
preserve_args+=(! -name "$top")
done < "$REGEN_IGNORE"
preserve_args+=(! -name .git ! -name .gitignore)
echo ">> --clean: wiping $SDK_DIR (preserving entries from .regen-ignore)"
find "$SDK_DIR" -mindepth 1 -maxdepth 1 "${preserve_args[@]}" -exec rm -rf {} +
fi

# --- Regen via openapi-generator-cli
Expand Down
Loading