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

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.11", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[test,dev]
- name: Lint
run: ruff check .
- name: Type check
run: mypy pyhunter
- name: Test with coverage
run: pytest --cov=pyhunter --cov-report=term-missing --cov-fail-under=50
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Changelog

## 2.1.0

- Added `AsyncPyHunter` with initial async support for core endpoints:
- `domain_search`
- `email_finder`
- `email_verifier`
- `email_count`
- `account_information`
- Hardened sync transport and error handling:
- timeout/retry/backoff options
- normalized `HunterApiError` and `HunterTransportError`
- safer per-call base params (no shared mutation across calls)
- Added pytest test suite including async tests and sync/async parity checks.
- Added GitHub Actions CI for lint, type checks, tests, and coverage threshold.
- Migrated project metadata to `pyproject.toml`.
32 changes: 32 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Contributing to PyHunter

## Local setup

```bash
python -m venv .venv
source .venv/bin/activate
pip install -e .[test,dev]
```

## Quality checks

```bash
ruff check .
mypy pyhunter
pytest --cov=pyhunter --cov-report=term-missing
```

## Pull requests

- Keep the public sync API backward compatible unless the PR is explicitly marked breaking.
- Add tests for each bug fix or endpoint behavior change.
- Update `README.md` for user-visible changes.
- Add an entry to `CHANGELOG.md`.

## Release checklist

- Bump version in `pyproject.toml`.
- Ensure CI is green for all supported Python versions.
- Build and verify artifacts:
- `python -m build`
- `python -m twine check dist/*`
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ To install:
pip install pyhunter
```

For async support:

```bash
pip install "pyhunter[test]"
```

### Usage

Import the PyHunter and instantiate it:
Expand All @@ -29,6 +35,36 @@ from pyhunter import PyHunter
hunter = PyHunter('my_hunter_api_key')
```

You can configure transport behavior:

```python
hunter = PyHunter(
'my_hunter_api_key',
timeout=10,
max_retries=2,
retry_backoff=0.5,
)
```

### Async Usage

```python
from pyhunter import AsyncPyHunter

async with AsyncPyHunter('my_hunter_api_key') as hunter:
result = await hunter.domain_search('instagram.com')
```

For long-lived clients:

```python
hunter = AsyncPyHunter('my_hunter_api_key')
try:
data = await hunter.email_count('instagram.com')
finally:
await hunter.aclose()
```

---

### Domain Search
Expand Down Expand Up @@ -110,6 +146,10 @@ PyHunter adds a `calls['left']` field to the response with the number of API cal

**NOTE:** By default, all calls return the `data` element of the JSON response. Pass `raw=True` to get the full HTTP response object, including headers (e.g. `X-RateLimit-Remaining`) and the complete response body including `meta`.

Transport and HTTP failures raise typed exceptions:
- `HunterTransportError` for connectivity/timeouts
- `HunterApiError` for non-2xx and malformed API payloads

---

### Enrichment
Expand Down Expand Up @@ -313,6 +353,25 @@ hunter.start_campaign(42)

---

### Logos

Get a company logo by domain (returns bytes):

```python
logo_bytes = hunter.logo('stripe.com')
```

Async:

```python
from pyhunter import AsyncPyHunter

async with AsyncPyHunter('my_hunter_api_key') as async_hunter:
logo_bytes = await async_hunter.logo('stripe.com')
```

---

### Information

If you find a bug or something is missing, feel free to open an issue or a pull request on [GitHub](https://github.com/VonStruddle/PyHunter).
Expand Down
3 changes: 3 additions & 0 deletions pyhunter/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
from .async_pyhunter import AsyncPyHunter
from .pyhunter import PyHunter

__all__ = ["PyHunter", "AsyncPyHunter"]
18 changes: 18 additions & 0 deletions pyhunter/_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from .exceptions import HunterApiError


def parse_data_payload(response, endpoint, method):
try:
return response.json()["data"]
except (KeyError, ValueError) as exc:
try:
payload_data = response.json()
except ValueError:
payload_data = {"body": response.text}
raise HunterApiError(
message="Hunter API response format is invalid",
status_code=response.status_code,
payload=payload_data,
endpoint=endpoint,
method=method,
) from exc
Loading
Loading