Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7632fff
feat: v2 validate parity (verify_plus, activity_data, timeouts)
franciscbalint Oct 17, 2026
f8c546f
feat: add score_email for GET/POST /v2/scoring
franciscbalint Oct 17, 2026
317f7d6
feat: Priority 3 API parity (bulk finder, filters, S3)
franciscbalint Oct 17, 2026
d27ab4a
feat: POST /validate transport parity and listS3files
franciscbalint Oct 17, 2026
4b36baa
fix: mock requests.request in listS3files test
franciscbalint Oct 18, 2026
2000394
fix: return short API errors and encode form booleans
franciscbalint Oct 19, 2026
2a86296
fix: save plain-text file downloads and lint the SDK
franciscbalint Oct 19, 2026
407f0a8
feat: validate WHOIS and greylist options, Inbox Placement Tester, li…
franciscbalint Oct 23, 2026
a6710e5
Remove the S3 methods: they are not in the public API docs
franciscbalint Oct 23, 2026
d16d737
Restore the S3 methods: the S3 bulk API is documented
franciscbalint Oct 23, 2026
b0444f8
feat: S3 email finder / domain search and SFTP/FTP bulk endpoints
franciscbalint Oct 23, 2026
cb415f9
feat: add ai_agent_mailbox sub_status (RD-66)
franciscbalint Oct 20, 2026
bc21887
fix: align the SDK with the public docs and test the edges
franciscbalint Oct 23, 2026
b1babf1
feat: Warmup API client (ZeroBounceWarmup)
franciscbalint Oct 23, 2026
bae3de6
fix(security): keep keys out of errors and URLs, raise dependency floors
franciscbalint Oct 23, 2026
8c494e5
feat: opt-in streamed downloads, regional bulk-host warning, workflow…
franciscbalint Oct 23, 2026
380c50b
ci: publish to PyPI with Trusted Publishing instead of an API token
franciscbalint Oct 23, 2026
59a28a7
docs: document PyPI Trusted Publishing as the only publishing method
franciscbalint Oct 23, 2026
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
33 changes: 25 additions & 8 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
name: Dependabot security auto-merge

# Dependabot here opens security updates only (.github/dependabot.yml). Patch and minor updates are
# approved and merged once checks pass. A major update gets a comment and is left for a person to
# review and merge, because it can change behaviour even when the tests pass.

on:
pull_request:
types: [opened, synchronize, reopened]
Expand All @@ -18,14 +22,26 @@ jobs:
github.event_name == 'pull_request'
&& github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Approve Dependabot pull request
run: gh pr review --approve "$PR_URL"
- name: Read the update type
id: metadata
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Approve and auto-merge a patch or minor update
if: steps.metadata.outputs.update-type != 'version-update:semver-major'
run: |
gh pr review --approve "$PR_URL"
gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Enable auto-merge when checks pass
run: gh pr merge --auto --merge "$PR_URL"
- name: Leave a major update for review
if: steps.metadata.outputs.update-type == 'version-update:semver-major'
run: >-
gh pr comment "$PR_URL" --body "Major version update: not auto-merged.
Review the changelog and merge by hand."
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -34,16 +50,17 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name == 'schedule'
steps:
- name: Approve and queue merge for open Dependabot PRs
- name: Re-queue Dependabot pull requests already cleared for auto-merge
# Only PRs the job above approved (patch or minor); majors are never approved by a workflow.
run: |
set -euo pipefail
mapfile -t urls < <(gh pr list --author app/dependabot --state open --json url --jq '.[].url')
mapfile -t urls < <(gh pr list --repo "$GITHUB_REPOSITORY" --author app/dependabot --state open \
--search "review:approved" --json url --jq '.[].url')
if [ "${#urls[@]}" -eq 0 ]; then
echo "No open Dependabot pull requests."
echo "No approved Dependabot pull requests to queue."
exit 0
fi
for url in "${urls[@]}"; do
gh pr review --approve "$url" || true
gh pr merge --auto --merge "$url" || true
done
env:
Expand Down
19 changes: 14 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ jobs:

persist-credentials: false
- name: Verify tag and package version
# Passed through env, never pasted into the script: an input expanded in run: is shell code.
env:
TAG: ${{ github.event.inputs.tag }}
run: |
TAG="${{ github.event.inputs.tag }}"
if [ -z "$TAG" ]; then
echo "Error: Tag is required."
exit 1
Expand Down Expand Up @@ -86,9 +88,15 @@ jobs:
- name: Install dependencies
run: pip install -e ".[test]"

- name: Lint
run: |
pip install ruff==0.17.2
ruff check src tests
python -m compileall -q src tests

- name: Run tests
run: |
python -m pytest tests/zero_bounce_test_case.py -v \
python -m pytest \
--cov=zerobouncesdk --cov-report=term-missing --cov-fail-under=50

publish:
Expand Down Expand Up @@ -118,9 +126,10 @@ jobs:
run: python -m build

- name: Publish to PyPI
# PyPI Trusted Publishing, the only way this package is published: the PyPI project trusts this
# repository's publish.yml in the "release" environment, and the action exchanges the job's OIDC
# token (id-token: write) for a short-lived upload token. No PyPI token exists or is needed.
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
password: ${{ secrets.PYPI_API_TOKEN }}

release:
name: Create GitHub Release
Expand All @@ -141,8 +150,8 @@ jobs:
- name: Create release if missing
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.event.inputs.tag }}
run: |
TAG="${{ github.event.inputs.tag }}"
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Release $TAG already exists, skipping."
else
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/sdk_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ jobs:
- name: Install dependencies
run: pip install -e ".[test]"

- name: Lint
run: |
pip install ruff==0.17.2
ruff check src tests
python -m compileall -q src tests

- name: Run unit tests with coverage
run: |
python -m pytest tests/zero_bounce_test_case.py -v \
python -m pytest \
--cov=zerobouncesdk --cov-report=term-missing --cov-fail-under=50
66 changes: 66 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# AGENTS.md

Guidance for AI coding agents working in this repository.

## Project

`zero-bounce-python-sdk-setup` is the ZeroBounce Python SDK, published to PyPI as `zerobouncesdk`. Callers construct a `ZeroBounce` with an API key and call methods that return response objects or raise `ZBApiException` / `ZBClientException`. HTTP goes through the `requests` library inside `src/zerobouncesdk/zerobouncesdk.py`.

## Layout

- `src/zerobouncesdk/zerobouncesdk.py`: the client. `_get`, `_post`, `_post_form`, and `_request_json` are the HTTP entry points.
- `src/zerobouncesdk/zb_warmup.py`: `ZeroBounceWarmup`, the Warmup API client. It has its own host and key, authenticates with an `API-Key` header, and reuses the error helpers in `zerobouncesdk.py`. Keep Warmup methods there, not on `ZeroBounce`.
- `src/zerobouncesdk/zb_*.py`: response models, enums, and options.
- `tests/zero_bounce_test_case.py`: scripted unit tests. They patch `zerobouncesdk.zerobouncesdk.requests`.
- `tests/test_offline_edges.py` and `tests/test_warmup_offline.py`: more offline suites on the same principle (error bodies, redaction, every optional field; the Warmup API).
- `tests/test_offline_api.py`: offline suite. It patches `requests.sessions.Session.send`, so the real `requests` client builds the URL and body and the fake answers without the network.
- `tests/zero_bounce_integration_test.py`: live API tests. `pytest` does not collect this file. Run it with `python -m unittest tests.zero_bounce_integration_test` when `ZEROBOUNCE_API_KEY` is set.
- `pyproject.toml`: the only version field, `version = "x.y.z"`. `setup.py` calls `setup()` and reads that file.

## Commands

```bash
pip install -e ".[test]"
pip install ruff
ruff check src tests # undefined names and syntax errors only
python -m compileall -q src tests
python -m pytest # unit tests and tests/test_offline_api.py
```

`ruff check` plus `compileall` is the lint step. There is no typechecker. The Ruff selection in `pyproject.toml` is `E9`, `F63`, `F7`, and `F82`. Do not widen it to a style ruleset that fails the existing tree.

CI (`.github/workflows/sdk_ci.yml`) runs `ruff check`, `compileall`, then `python -m pytest` with coverage.

## Conventions

- **Only endpoints documented at https://www.zerobounce.net/docs/.** Do not add a method for an endpoint that the public docs do not describe, even if it exists on the server. Integration pages count: the S3 bulk API is documented at https://www.zerobounce.net/docs/api-integrations/amazon-s3-bulk-api.
- **One `ZeroBounce` per API key.** The class is not a singleton. Do not reuse one instance across users or requests. The offline concurrency test constructs a separate client per key.
- **Column indexes are 1-based, and the SDK does not add 1.** Pass the API's index. `0` is a real value and is sent. `None` on an optional column (such as `first_name_column`) omits the field. `send_file`'s email column is required and is sent as given, including `0`. `domain_column` and the list-evaluator email column are required: `None` is rejected, `0` is sent. The bulk email finder also needs `first_name_column` or `full_name_column`.
- **Optional parameters are omitted when unset.** `timeout`, `activity_data`, `verify_plus`, and `allow_phase_2` are not sent until the caller sets them. Explicit `False` is sent as `false`. `ip_address=None` is omitted; `""` is sent.
- **Booleans on form and multipart bodies must be the strings `true` and `false`.** A Python `bool` passed to `requests` becomes `True` or `False`, and the API will not treat that as a boolean. `_form_data` converts them. JSON bodies (`validate_batch`) keep JSON booleans.
- **`allow_phase_2` is validation `send_file` only.** Scoring `send_file` and the S3 / SFTP jobs do not send it; those jobs send only their documented `file_name`, `has_header_row`, `remove_duplicates`, and `return_url`. `activity_data` on get-file is validation `get_file` only.
- **`validate_batch` uses `api.zerobounce.net`**, not `bulkapi.zerobounce.net`. It follows `base_url`.
- **Failures raise.** `ZBClientException` means the arguments were rejected and no request was sent. `ZBApiException` means the API failed. The message must be short: no raw HTML, no megabyte body, no `JSONDecodeError` / `Expecting value` text, and no `Error: Error:` prefix. An account-level `validate_batch` error (`email_address` `all`) raises. A per-address error stays on the result's `errors` list. `get_credits` with an invalid key returns `Credits` `-1`; that endpoint does not send an `error` field.
- **Downloads are buffered unless `stream=True`.** The streamed path sniffs the first 64 KB for an error body, writes to a `.part` temporary file next to `download_path`, and moves it into place only when complete. Keep both paths' error checks identical (`_raise_if_download_error`).
- **A file download is not JSON.** `get_file` saves a non-empty CSV body, including `text/csv` and `text/plain`. An empty body, an HTML page (status 200 or 403), and a JSON error are `ZBApiException`. Do not treat every `text/plain` body as an error.
- **Do not follow redirects.** Every `requests` call passes `allow_redirects=False`. A 3xx response is a `ZBApiException`. A redirect must not be fetched, because the `Location` could be another host and the next request can carry the API key.
- **Do not commit `.env`.** It is gitignored and may hold a real key. Tests must not call the live API.
- Match the surrounding style. Do not reformat unrelated code.

## Changelog

`CHANGELOG.md` follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/), and the project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html).

- Add every user-visible change to `## [Unreleased]` in the same commit as the change.
- Group entries under these headings, in this order, omitting empty ones: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
- Write for SDK users: name the method, option, or behaviour, and say what changed. Leave out CI-only changes, internal refactors, and fixes to code that was never released.
- At 2.y.z, a breaking change bumps the major version, a new method or option bumps the minor version, and fixes alone bump the patch version.

## Releasing

Publishing and pushing tags need a maintainer's explicit approval. Do not bump the version, tag, or publish unless they asked.

1. Move `[Unreleased]` entries under `## [x.y.z] - YYYY-MM-DD`, leave an empty `[Unreleased]` section, and update the compare links at the bottom of `CHANGELOG.md`.
2. Set the same version in `pyproject.toml` (`version = "x.y.z"`). The publish workflow fails if it differs from the tag. That is the only version field. There is no runtime `__version__` and no user-agent constant. `setup.py` does not contain a version.
3. `ruff check src tests`, `python -m compileall -q src tests`, and `python -m pytest`.
4. Tag `vx.y.z` on that commit and run the Publish workflow (`.github/workflows/publish.yml`) with that tag. It publishes `zerobouncesdk` to PyPI through PyPI Trusted Publishing: the PyPI project trusts this repository, `publish.yml`, environment `release`. There is no PyPI token, and none should be added. Keep the workflow file name and environment name, or update the trusted publisher on PyPI first.
Loading
Loading