Metadata-Version: 2.4
Name: primeguardia
Version: 1.0.3
Summary: Official PrimeGuardia Sanctions Screening SDK for Python
Author-email: PrimeGuardia <support@primeguardia.com>
License-Expression: MIT
Project-URL: Homepage, https://primeguardia.com
Project-URL: Documentation, https://docs.primeguardia.com
Project-URL: Bug Tracker, https://github.com/primeguardia/sanctions-sdk-python/issues
Keywords: sanctions,compliance,screening,aml,kyc,ofac,pep,primeguardia
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
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: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.24.0
Requires-Dist: typing-extensions>=4.0.0; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: respx>=0.20.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: ruff>=0.0.260; extra == "dev"
Dynamic: license-file

# PrimeGuardia Python SDK

Official Python client for PrimeGuardia’s sanctions screening API.

**PyPI package:** [`primeguardia`](https://pypi.org/project/primeguardia/)

[![PyPI version](https://img.shields.io/pypi/v/primeguardia.svg)](https://pypi.org/project/primeguardia/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Install

```bash
pip install primeguardia
```

```python
from primeguardia import PrimeGuardia

client = PrimeGuardia(api_key="your-api-key-here")
```

Default base URL: `https://api.primeguardia.com`. Auth header: `X-API-Key`.

## What production actually does

- Screening is **warn, not hard-block**. Use **`should_block`** and **`review_required`**; `blocked` has been removed.
- Live `POST /api/screen` does **not** send `matches[]` or `risk_assessment`. `ScreeningResult.from_api` fills `matches` from `data` and derives `risk_assessment`.
- The API uses `name`, `email`, and `birth_date` / `dob`. **`country` and `metadata` are sent by the SDK but ignored by the API.**
- Bulk screens **names only**. Passing `emails=` does not screen those emails.
- Search is a name/email `ILIKE` lookup. **`limit` and `sources` are ignored.** A miss is HTTP 404; the SDK returns an empty `SearchResponse` instead of raising.
- Datasets are list-file rows (`fileName` → `dataset.name`). The API does not send record counts; `record_count` is `0`.

## Screen (the method customers use)

```python
result = client.screen(name="Vladimir Putin")

if result.should_block:
    print("SHOULD BE BLOCKED — review required")
    print(result.match_category)          # e.g. "sanctions"
    print(result.risk_assessment)         # "HIGH" (derived)
    print(result.matches[0].name)         # from data.name
    print(result.data["matched_sources"])
else:
    print("Clear")
```

Optional DOB (the engine compares year):

```python
client.screen(name="John Smith", date_of_birth="1980-01-01")
```

Context manager closes the HTTP client:

```python
with PrimeGuardia(api_key="your-key") as client:
    result = client.screen(name="John Doe")
    print(result.should_block, result.risk_assessment)
```

## Bulk screen

```python
results = client.bulk_screen(names=["Vladimir Putin", "Zorblax Quennerthwaite"])

print(results.processed, results.processing_time_ms)
print(results.high_risk_count, results.matches_count)
for row in results.results:
    if row.should_block:
        print(row.name, row.risk_level)  # "Vladimir Putin" "HIGH"
```

## Search and entity

```python
found = client.search(query="Vladimir Putin")
print(found.total, found.results[0].name, found.results[0].source_dataset)

miss = client.search(query="ZxqqqUniqueNobody918273")
# miss.total == 0, miss.results == []  (prod 404, not an exception)

entity = client.get_entity(4242)
print(entity.name, entity.source_dataset)
```

Search is **not** the same engine as `screen()`. It can return PEP/crime clones for a name that `screen()` ranks as OFAC.

`has_more` is only meaningful if the API sent pagination; live search does not, so it stays false.

## Account

```python
profile = client.get_profile()
print(profile.client_name, profile.tier, profile.subscription_status)
print(profile.usage_percentage, profile.is_active)

quota = client.get_quota_status()
print(quota.used, quota.limit, quota.remaining, quota.percentage)
```

`get_quota_status()` calls `GET /api/status` (`usage` / `quota` on the wire). The SDK maps those to `used` / `limit`.

## Monitoring

```python
monitored = client.add_monitoring(
    name="Acme Holdings Ltd",
    frequency=24,  # sent as check_frequency="daily"
)
print(monitored.id, monitored.name)

entities = client.get_monitored_entities()  # list (unwraps { "entities": [...] })
```

Prod add expects `entity_name`. Pass SDK `name`; the client maps it.

The sync client does **not** wrap `getAlerts` / `getMonitoringStats`. Use the HTTP API directly if you need those.

## Datasets

```python
datasets = client.get_datasets()
print(datasets[0].name)  # e.g. "eu_sanctions_2026_08_27.csv"
```

## Async client

`AsyncPrimeGuardia` only implements **`screen`**, **`bulk_screen`**, **`get_profile`**, and **`test_connection`**. Other methods are sync-only.

```python
import asyncio
from primeguardia import AsyncPrimeGuardia

async def main():
    async with AsyncPrimeGuardia(api_key="your-key") as client:
        result = await client.screen(name="Vladimir Putin")
        print(result.should_block, result.risk_assessment)

asyncio.run(main())
```

## Errors

```python
from primeguardia import (
    PrimeGuardia,
    AuthenticationError,
    QuotaExceededError,
    RateLimitError,
    ValidationError,
    PrimeGuardiaError,
)

client = PrimeGuardia(api_key="your-key")

try:
    client.screen(name="John Doe")
except AuthenticationError:
    print("401 — missing/invalid key on some paths")
except QuotaExceededError:
    print("429 with quota in the message")
except RateLimitError as e:
    print("other 429", e.retry_after)
except ValidationError as e:
    print("400", e)
except PrimeGuardiaError as e:
    print(e.status_code, e)
```

Prod returns **403** `INVALID_API_KEY` for a key that is not in the database (not 401). That becomes `PrimeGuardiaError`, not `AuthenticationError`. An empty `api_key=` to the constructor raises `ValidationError` before any request.

## Config

```python
PrimeGuardia(
    api_key="your-api-key",
    base_url="https://api.primeguardia.com",
    timeout=30.0,      # seconds
    max_retries=3,     # httpx transport retries
)
```

## Methods that exist (sync)

| Method | Prod path | Notes |
|---|---|---|
| `screen` | `POST /api/screen` | Use `should_block` |
| `bulk_screen` | `POST /api/sanctions/bulk-search` | Names only |
| `search` | `GET /api/sanctions/search` | 404 miss → empty |
| `get_entity` | `GET /api/sanctions/entity/:id` | Raw entity row |
| `get_datasets` | `GET /api/sanctions/datasets` | Filename list |
| `get_profile` | `GET /api/settings/profile` | Unwraps `{ "client": … }` |
| `get_quota_status` | `GET /api/status` | Maps `usage`/`quota` |
| `add_monitoring` / `get_monitored_entities` | `/api/monitoring/entities` | Maps `name` → `entity_name` |
| `test_connection` | profile GET | |

## Tests

```bash
cd sdks/python
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
```

## Support

- Email: support@primeguardia.com
- Site: https://primeguardia.com

MIT © PrimeGuardia

## Screening safety and compatibility

See [the shared SDK contract](../README.md#screening-contract). `birth_date` is supported directly; `date_of_birth` maps to it. Unknown matches remain null/None, with UNKNOWN risk and `is_clear` false. Use `is_clear` rather than negating `match`. Generic API errors retain the full response in `details`. Bulk email inputs are rejected because the backend screens names only.
