# abuseipdb-python-sdk

Typed, synchronous Python client for AbuseIPDB API v2. This is a personal, unofficial port created by William Friend for Python packaging and release practice. It follows architecture and behavior of official [`AbuseIPDB/laravel` 2.0.1](https://github.com/AbuseIPDB/laravel/tree/2.0.1), but is not produced, endorsed, or supported by AbuseIPDB.

Distribution name is `abuseipdb-python-sdk`; import namespace remains future-friendly `abuseipdb`.

## Installation

```bash
python -m pip install abuseipdb-python-sdk
```

Python 3.10 or newer is required.

## Authentication and configuration

```bash
export ABUSEIPDB_API_KEY="your-key"
```

```python
from abuseipdb import AbuseIPDB

client = AbuseIPDB()  # reads ABUSEIPDB_API_KEY
client = AbuseIPDB(
    api_key="your-key",  # takes precedence over environment
    timeout=10.0,
)
```

Use client as context manager, or call `client.close()` when done. Every request sends `Key`, correct `Accept`, and an `X-Request-Source` identifying Python and SDK versions.

## Seven API operations

### Check

```python
with AbuseIPDB() as client:
    result = client.check("8.8.8.8", max_age_in_days=90, verbose=True)
    print(result.ip_address, result.abuse_confidence_score)
    for report in result.reports:
        print(report.reported_at, report.categories)
```

### Report

Reporting changes production data. Verify input and category choice.

```python
from abuseipdb import AbuseCategory, AbuseIPDB

with AbuseIPDB() as client:
    result = client.report(
        "203.0.113.10",
        categories=[AbuseCategory.BRUTE_FORCE, AbuseCategory.SSH],
        comment="Repeated SSH authentication attempts",
    )
```

One raw ID (`categories=18`) or iterable of enum/raw IDs works. Optional `timestamp` must be `datetime.datetime` and serializes using ISO 8601.

### Reports

```python
with AbuseIPDB() as client:
    page = client.reports("203.0.113.10", max_age_in_days=30, page=1, per_page=25)
    print(page.total, page.count, page.last_page)
    for report in page.results:
        print(report.reported_at)
```

### Blacklist

```python
with AbuseIPDB() as client:
    result = client.blacklist(
        confidence_minimum=90,
        limit=10000,
        only_countries=["US", "CA"],
        except_countries=None,
        ip_version=6,
    )
    for entry in result.blacklisted_ips:
        print(entry.ip_address, entry.last_reported_at)
```

Plaintext is first-class and does not attempt JSON decoding:

```python
with AbuseIPDB() as client:
    result = client.blacklist(confidence_minimum=90, plaintext=True)
    for ip in result.addresses:
        print(ip)
```

### Check block

IPv4 and IPv6 CIDR strings pass through URL encoding correctly.

```python
with AbuseIPDB() as client:
    result = client.check_block("2001:db8::/64", max_age_in_days=30)
    for address in result.reported_addresses:
        print(address.ip_address, address.num_reports)
```

### Bulk report

`bulk_report` accepts CSV contents—not a path—matching Laravel 2.0.1's actual method contract. SDK uploads them as multipart field `csv`, filename `report.csv`.

```python
csv_contents = "IP,Categories,ReportDate,Comment\n203.0.113.10,18,,Brute force\n"
with AbuseIPDB() as client:
    result = client.bulk_report(csv_contents)
    print(result.saved_reports, result.invalid_reports)
```

### Clear address

This deletes only your account's reports for given address.

```python
with AbuseIPDB() as client:
    result = client.clear_address("2001:db8::10")
    print(result.num_reports_deleted)
```

## Categories

`AbuseCategory` contains all IDs 1–23 from Laravel 2.0.1. `CATEGORIES` exposes Laravel-compatible display-name mapping.

```python
from abuseipdb import AbuseCategory, CATEGORIES

assert AbuseCategory.PORT_SCAN == 14
assert CATEGORIES["SSH"] == 22
```

## Typed responses

API camelCase becomes public snake_case: `abuseConfidenceScore` becomes `abuse_confidence_score`. Top-level types are `AbuseResponse`, `CheckResponse`, `ReportResponse`, `ReportsPaginatedResponse`, `BlacklistResponse`, `BlacklistPlaintextResponse`, `BulkReportResponse`, `CheckBlockResponse`, and `ClearAddressResponse`. Nested types are `ReportInfo`, `BlacklistedIP`, `BulkInvalidReport`, and `CheckBlockIP`.

All responses retain `status_code`, selected response headers, and `raw_response`. ISO dates become timezone-aware `datetime` values. Optional fields use `None`; nested collections use immutable tuples.

## Exceptions

All SDK exceptions derive from `AbuseIPDBError`:

- `InvalidParameterError`: Laravel-compatible client validation failure.
- `MissingAPIKeyError`: no explicit or environment API key.
- `PaymentRequiredError`: HTTP 402.
- `UnprocessableContentError`: HTTP 422.
- `TooManyRequestsError`: HTTP 429.
- `UnconventionalAPIError`: any other unsuccessful HTTP status.

HTTP errors expose `status_code`, `detail`, and original `response`. Recognizable `*Exception` aliases are available for Laravel migration.

## Suspicious-operation helper

Laravel's automatic Symfony exception hook has no generic Python equivalent. Optional `abuseipdb.reporters.report_suspicious_operation` accepts address and request parameter mapping supplied by your framework, reports category 21, and suppresses SDK failures like upstream helper. Wire it into your framework explicitly; never include secrets in submitted parameters.

## Development

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
ruff check .
ruff format --check .
pytest --cov
mypy src
python -m build
twine check dist/*
```

Unit tests are offline; mocked write operations never contact production. See [RELEASE.md](RELEASE.md) for personal manual release practice and [PORTING_NOTES.md](PORTING_NOTES.md) for exact Laravel mappings and discrepancies.

### Local installation and automatic tests

From repository root:

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
pytest --cov
```

Editable installation means imports use current `src/` code; reinstall is unnecessary after edits. To test exact built wheel instead:

```bash
rm -rf dist build
python -m build
python -m venv /tmp/abuseipdb-local-wheel
/tmp/abuseipdb-local-wheel/bin/python -m pip install dist/abuseipdb_python_sdk-0.1.0-py3-none-any.whl
cd /tmp
/tmp/abuseipdb-local-wheel/bin/python -c "import abuseipdb; print(abuseipdb.__version__)"
```

VS Code: open Command Palette → **Tasks: Run Task**. Use **Test: pytest** or **Quality: all automatic checks** from [`.vscode/tasks.json`](.vscode/tasks.json).

### Live end-to-end test

Normal pytest never contacts AbuseIPDB. Live runner calls every endpoint, including destructive `report`, `bulk-report`, and `clear-address`. Use two different real abusive public IPs you are authorized to report; cleanup deletes your account's reports for those addresses. Do not use documentation/reserved IPs for writes.

Keep API key in shell environment, never source control:

```bash
export ABUSEIPDB_API_KEY="your-real-key"
export ABUSEIPDB_E2E_CHECK_IP="8.8.8.8"
export ABUSEIPDB_E2E_REPORT_IP="real-abusive-public-ip-1"
export ABUSEIPDB_E2E_BULK_IP="real-abusive-public-ip-2"
export ABUSEIPDB_E2E_NETWORK="8.8.8.0/24"

python tests/manual/e2e.py --allow-writes
```

Production API is automatic. Developers testing against local server can override it explicitly:

```bash
export ABUSEIPDB_URL="http://abuseipdb.localhost/api/v2"
python tests/manual/e2e.py --allow-writes
```

Constructor override also works: `AbuseIPDB(base_url="http://abuseipdb.localhost/api/v2")`. Explicit constructor value wins over developer environment override.

Runner prints `[PASS]` or `[FAIL]`, typed output/error body for every call, and final count. It continues after failures so one run shows whole API state. VS Code task **Manual: live API E2E (WRITES REPORTS)** prompts for key/addresses without saving them.

## Attribution and license

API behavior is documented by [AbuseIPDB API v2](https://docs.abuseipdb.com/). Architecture is based on official [AbuseIPDB Laravel SDK](https://github.com/AbuseIPDB/laravel). This unofficial project uses MIT license consistent with upstream.
