Metadata-Version: 2.5
Name: privacyfixed
Version: 0.1.1
Summary: Official Python SDK for the PrivacyFixed public Developer API.
Project-URL: Homepage, https://privacyfixed.com
Project-URL: Documentation, https://privacyfixed.com/developers
Author: PrivacyFixed
License: MIT
License-File: LICENSE
Keywords: compliance,gdpr,privacy,scanner,sdk,security
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# PrivacyFixed Python SDK

The official Python client for the [PrivacyFixed](https://privacyfixed.com)
public Developer API. Submit a URL, and get back a full privacy, compliance,
and security scan of the page.

- **Zero runtime dependencies** — built entirely on the Python standard
  library (`urllib`, `json`, `time`). Nothing to compile, nothing to audit.
- **Python 3.9+**
- Both a one-call high-level helper (`scan`) and low-level primitives
  (`submit` / `get_job`) for when you want to manage polling yourself.

## Install

```bash
pip install privacyfixed
```

## Get an API key

1. Sign in at [privacyfixed.com](https://privacyfixed.com).
2. The Developer API is part of the **Business plan** — on a lower plan the key
   page is unavailable, and a key from a downgraded account stops working.
3. Go to [privacyfixed.com/developers](https://privacyfixed.com/developers) and
   create a key. It is shown **once**; store it then.

Keys look like `pfk_live_…`. One scan runs at a time per account: while a scan
is in flight, another submit returns HTTP 429 (`RateLimitedError`).

## Quickstart

```python
from privacyfixed import PrivacyFixed

client = PrivacyFixed(api_key="pfk_live_...")
result = client.scan("https://example.com")
print(result["compliance"], result["security_headers"])
```

`scan()` submits the URL and blocks until the scan finishes (a full scan
usually takes ~30-45 seconds), then returns the complete scan `result` dict.

## Authentication

Every request is authenticated with your API key, sent as a bearer token.
Pass it when you construct the client:

```python
client = PrivacyFixed(api_key="pfk_live_...")
```

Keys look like `pfk_live_...`. Keep them secret — treat them like a password
and load them from an environment variable or secrets manager rather than
hard-coding them:

```python
import os
from privacyfixed import PrivacyFixed

client = PrivacyFixed(api_key=os.environ["PRIVACYFIXED_API_KEY"])
```

### Configuration

```python
client = PrivacyFixed(
    api_key="pfk_live_...",
    base_url="https://api.privacyfixed.com",  # override for self-hosting/testing
    timeout=30.0,                              # per-request socket timeout (seconds)
)
```

`base_url` must use **https** — a plain `http://` URL would send your API key
across the network in clear text, so the client raises `ValueError` rather than
doing it. `http://localhost` and `http://127.0.0.1` stay allowed so you can run
against a local API.

### How your key is protected

- It is never written to logs, and `repr(client)` prints `api_key='***'`.
- If the API ever redirects to another host, the `Authorization` header is
  **dropped** before the redirect is followed, so the key cannot be handed to a
  host you did not authenticate to. (Python's `urllib` forwards headers across
  redirects by default; this client overrides that.)
- Job ids are percent-encoded into the URL, so an id taken from user input
  cannot rewrite the request path.

## The high-level call: `scan()`

```python
result = client.scan(
    "https://example.com",
    poll_interval=3.0,   # seconds between poll requests
    max_wait=150.0,      # give up after this many seconds
)
```

- Returns the scan `result` dict when the job completes.
- Raises `ScanFailed` if the job finishes in a `failed` state.
- Raises `ScanTimeout` if the job does not finish within `max_wait` seconds.

## Low-level: submit and poll yourself

When you would rather not block — for example, in an async worker, a web
request handler, or a job queue — submit the scan and poll on your own
schedule:

```python
job_id = client.submit("https://example.com")

# ...later, or on a timer...
job = client.get_job(job_id)
print(job["status"])   # "pending" | "running" | "completed" | "failed"

if job["status"] == "completed":
    result = job["result"]
elif job["status"] == "failed":
    print(job["error"])   # {"status": int, "detail": str}
```

### API shapes

`submit(url)` performs `POST /v1/public/scans` with body `{"url": "..."}` and
returns the `job_id` from the `202 Accepted` response.

`get_job(job_id)` performs `GET /v1/public/scans/{job_id}` and returns the
full job dict:

```json
{
  "job_id": "…",
  "status": "pending | running | completed | failed",
  "result": { "…full scan JSON…" },
  "error": { "status": 500, "detail": "…" }
}
```

`result` is `null` until the scan completes; `error` is `null` unless it fails.

## Error handling

All exceptions subclass `PrivacyFixedError`, so you can catch everything with a
single `except` if you prefer:

| Exception           | When it is raised                                            |
| ------------------- | ------------------------------------------------------------ |
| `AuthError`         | HTTP 401 or 403 — missing, invalid, or unentitled API key.   |
| `RateLimitedError`  | HTTP 429 — a scan is already running for your key.           |
| `APIError`          | Any other non-2xx response (e.g. 503 when the scanner is paused). Carries `.status` and `.message`. |
| `ScanFailed`        | The job finished with `status == "failed"`. Carries `.status`, `.message`, and the full `.job`. |
| `ScanTimeout`       | The scan did not finish within `max_wait`. Carries `.job_id` and `.waited`. |

`AuthError` and `RateLimitedError` are themselves subclasses of `APIError`, so
they also carry `.status` and `.message`.

```python
from privacyfixed import (
    PrivacyFixed,
    AuthError,
    RateLimitedError,
    ScanFailed,
    ScanTimeout,
    APIError,
)

client = PrivacyFixed(api_key="pfk_live_...")

try:
    result = client.scan("https://example.com")
except AuthError:
    print("Check your API key.")
except RateLimitedError:
    print("A scan is already running — try again shortly.")
except ScanTimeout as exc:
    print(f"Scan {exc.job_id} took too long.")
except ScanFailed as exc:
    print(f"Scan failed: {exc.message}")
except APIError as exc:
    print(f"API returned {exc.status}: {exc.message}")
```

## Development

```bash
pip install -e ".[dev]"
python -m pytest -q
```

The behaviour tests monkeypatch the transport and `time.sleep`, so they are
deterministic and never wait. The security tests run real HTTP servers on
loopback — a mocked transport cannot show whether a header survives a redirect,
which is the defect they exist to catch. Nothing leaves the machine.

## License

MIT
