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
8 changes: 7 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,15 @@ jobs:
- name: Install dependencies
run: pip install -e ".[test]"

- name: Lint
run: |
pip install ruff
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
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
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
62 changes: 62 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 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_*.py`: response models, enums, and options.
- `tests/zero_bounce_test_case.py`: scripted unit tests. They patch `zerobouncesdk.zerobouncesdk.requests`.
- `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

- **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 (`first_name_column`, and the S3 email 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.
- **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` and `validate_s3file` only.** Scoring `send_file` and `score_s3file` do not send it. `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.
- **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.
140 changes: 140 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- `validate` takes `timeout` (3–60 seconds), `activity_data`, `verify_plus`, and `use_post` (form-encoded `POST /validate`).
- `validate_batch` takes `timeout` (10–120 seconds), `activity_data`, and `verify_plus`.
- `ZBValidateResponse` reads `active_in_days` and `active_first_seen`.
- `score_email` (`GET /scoring`) and `score_email(..., use_post=True)` (form-encoded `POST /scoring`).
- Bulk email finder (`send_email_finder_file`, `email_finder_file_status`, `email_finder_get_file`, `email_finder_delete_file`, plus stream upload).
- Bulk domain search (`send_domain_search_file`, `domain_search_file_status`, `domain_search_get_file`, `domain_search_delete_file`, plus stream upload).
- List evaluator (`send_list_evaluator_file`, `get_list_evaluator_result`, `send_evaluated_file`).
- Filters (`add_filter`, `list_filters`, `delete_filter`).
- S3 bulk (`validate_s3file`, `score_s3file`, `export_s3file`, `list_s3files`).

### Changed

- A custom `base_url` must be an `https://` URL.
- API requests time out after 120 seconds unless the caller passes `timeout`.
- API requests do not follow redirects.

### Fixed

- A 403 HTML "Access denied" page (about 1.4 MB from the live API on `get_file`) is raised as a short `ZBApiException`. An HTML page with status 200 is an error as well, including `get_file`, `email_finder_get_file`, and `domain_search_get_file`, instead of a saved CSV.
- Empty and non-JSON bodies are a short `ZBApiException`. The message is no longer a chained `JSONDecodeError`, and a doubled `Error: Error:` / `[Error]: [Error]:` prefix is collapsed to one.
- `validate_batch` raises `ZBApiException` for an account-level failure (HTTP 200, error for address `all`, such as an invalid API key or no credits). An error for one address stays on the successful result.
- `send_file`, `scoring_send_file`, bulk email finder, domain search, and S3 uploads send `has_header_row`, `remove_duplicate`, and `remove_duplicates` as `true` or `false`. `has_header_row=False` is sent.
- `send_email_finder_file`, `send_domain_search_file`, and `send_list_evaluator_file` send column `0`. They previously treated `0` as empty and rejected it before the request.
- `find_email_format` raises `ZBApiException` when the API returns a `Message` error, the same way `find_domain` and `guess_format` already did.

### Security

- Error messages no longer include the raw HTML access-denied page. The live `get_file` page is about 1.4 MB and contains the API key.
- Redirects are not followed, so a response cannot send the client, or the API key, to another host.

## [2.2.3] - 2026-06-18

### Fixed

- `validate_batch` posts to `api.zerobounce.net/v2/validatebatch` instead of `bulkapi.zerobounce.net`.

## [2.2.2] - 2026-06-10

### Added

- `ZBValidateResponse.catchall_domain`, read from `catchall_domain`.

## [2.2.0] - 2026-04-05

### Added

- `send_file` / `send_file_stream` `allow_phase_2`, sent as `allow_phase_2` on validation bulk sendfile only.
- `ZBGetFileOptions` for `get_file` and `scoring_get_file`, with `download_type` (`phase_1`, `phase_2`, `combined`) and `activity_data` (validation `get_file` only).
- `ZBFileStatusResponse.file_phase_2_status`.

### Fixed

- `get_file` raises `ZBApiException` when the body is a JSON error, including HTTP 200 with `"success": false`, instead of saving that body as the results file.

## [2.1.10] - 2026-03-03

### Changed

- License the project under MIT.

## [2.1.0] - 2026-03-03

### Added

- `ZeroBounce(..., timeout=timedelta)` to set the HTTP timeout.
- `send_file_stream` and `scoring_send_file_stream`.

### Fixed

- `get_credits` and `validate_batch` tolerate a response that omits `Credits`, `email_batch`, or `errors`.

## [2.0.1] - 2026-01-26

### Fixed

- `ZBValidateSubStatus.gold` and unknown status values are recognized again. The 2.0.0 tag did not include them.

## [2.0.0] - 2026-01-26

### Removed

- `ZBValidateSubStatus.gold` and the fallback that kept an unknown status from raising. Restored in 2.0.1.

## [1.1.3] - 2026-01-26

### Added

- `find_email_format` and `find_domain`.
- `ZeroBounce(api_key, base_url)` with `ZBApiUrl` (`API_DEFAULT_URL`, `API_USA_URL`, `API_EU_URL`) or an https URL string.
- `ZBValidateSubStatus.gold` and `ZBValidateSubStatus.role_based_accept_all`.

### Changed

- An unknown status, sub-status, or confidence value warns and becomes `None` instead of raising.

### Deprecated

- `guess_format`. Use `find_email_format` or `find_domain`.

## [1.1.2] - 2025-08-07

### Added

- `ZBValidateSubStatus.accept_all`.

## [1.1.1] - 2024-12-20

### Fixed

- API error payloads raise `ZBApiException` with the error text.

## [1.1.0] - 2023-09-12

### Added

- Initial release: `get_credits`, `get_api_usage`, `get_activity`, `validate`, `validate_batch`, bulk and AI-scoring file methods, and `guess_format`.

[Unreleased]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.2.4...HEAD
[2.2.3]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.2.2...v2.2.3
[2.2.2]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.2.0...v2.2.2
[2.2.0]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.1.10...v2.2.0
[2.1.10]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.1.0...v2.1.10
[2.1.0]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.0.1...v2.1.0
[2.0.1]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v1.1.3...v2.0.0
[1.1.3]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v1.1.2...v1.1.3
[1.1.2]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v1.1.1...v1.1.2
[1.1.1]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/zerobounce/zero-bounce-python-sdk-setup/releases/tag/v1.1.0
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ test = ["pytest>=7.0.0", "pytest-cov>=4.0.0"]
[project.urls]
Homepage = "https://github.com/zerobounce/zero-bounce-python-sdk-setup"

[tool.ruff.lint]
select = ["E9", "F63", "F7", "F82"]

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py", "zero_bounce_test_case.py"]
python_files = ["test_*.py", "zero_bounce_test_case.py"]
python_functions = ["test_*"]
addopts = "-v --tb=short"
filterwarnings = ["ignore::DeprecationWarning"]
Expand Down
1 change: 1 addition & 0 deletions src/zerobouncesdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .zb_get_credits_response import ZBGetCreditsResponse
from .zb_get_api_usage_response import ZBGetApiUsageResponse
from .zb_get_activity_response import ZBGetActivityResponse
from .zb_score_email_response import ZBScoreEmailResponse
from .zb_validate_status import ZBValidateStatus
from .zb_validate_sub_status import ZBValidateSubStatus
from .zb_validate_response import ZBValidateResponse
Expand Down
5 changes: 4 additions & 1 deletion src/zerobouncesdk/zb_find_email_format_response.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from . import ZBConfidence
from . import ZBApiException, ZBConfidence
from ._zb_response import ZBResponse


Expand All @@ -25,6 +25,9 @@ class ZBFindEmailFormatResponse(ZBResponse):

def __init__(self, data):
super().__init__(data)
if "Message" in data or "message" in data:
message = data.get("message", data.get("Message"))
raise ZBApiException(message)
if self.email_confidence and isinstance(self.email_confidence, str):
self.email_confidence = ZBConfidence(self.email_confidence.lower())

8 changes: 8 additions & 0 deletions src/zerobouncesdk/zb_score_email_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from ._zb_response import ZBResponse


class ZBScoreEmailResponse(ZBResponse):
"""Response for GET/POST /scoring (real-time AI scoring)."""

email: str = None
score: int = None
6 changes: 6 additions & 0 deletions src/zerobouncesdk/zb_validate_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ class ZBValidateResponse(ZBResponse):
domain_age_days: str = None
"""Age of the email domain in days or [null]."""

active_in_days: str = None
"""Last activity bucket when activity_data was requested (e.g. 30, 365+)."""

active_first_seen: str = None
"""First date activity was detected (YYYY-MM-DD)."""

free_email: bool = False
"""[true/false] If the email comes from a free provider."""

Expand Down
Loading
Loading