Metadata-Version: 2.4
Name: raipii
Version: 0.1.0
Summary: Python SDK for the raipii PII detection and sanitization API
Project-URL: Homepage, https://raipii.com
Project-URL: Documentation, https://docs.raipii.com
Project-URL: Repository, https://github.com/mynridost-afk/raipii-python
Project-URL: Bug Tracker, https://github.com/mynridost-afk/raipii-python/issues
License: MIT
Keywords: llm,pii,privacy,redact,sanitize
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.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: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Requires-Dist: requests>=2.28.0
Provides-Extra: dev
Requires-Dist: pytest-mock>=3.10; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: responses>=0.23; extra == 'dev'
Description-Content-Type: text/markdown

# raipii Python SDK

Detect and sanitize PII before it reaches your LLM. Replace real data with tokens or realistic fakes. Restore original values after the model responds.

[![PyPI version](https://img.shields.io/pypi/v/raipii)](https://pypi.org/project/raipii/)
[![Python 3.8+](https://img.shields.io/pypi/pyversions/raipii)](https://pypi.org/project/raipii/)

---

## Install

```bash
pip install raipii
```

Requires Python 3.8+. The only dependency is `requests`.

---

## Quick start

```python
import raipii

ps = raipii.Raipii(api_key="ps_live_...")

# 1. Sanitize — strip PII before sending to your LLM
result = ps.sanitize(
    "Hi, I'm John Smith — john@acme.com, SSN 392-45-7810",
    mode="fake_substitute",
)
print(result.sanitized_text)
# "Hi, I'm Michael Torres — m.torres@email.net, SSN 847-23-1956"

# 2. Call your LLM with the sanitized prompt
llm_response = your_llm(result.sanitized_text)

# 3. Restore — put original values back in the response
original = ps.restore(llm_response, result.session_id)
print(original.restored_text)
# "Hi, I'm John Smith — john@acme.com, SSN 392-45-7810"
```

Set `RAIPII_API_KEY` in your environment to avoid passing the key in code:

```bash
export RAIPII_API_KEY=ps_live_...
```

```python
ps = raipii.Raipii()  # reads RAIPII_API_KEY automatically
```

Get a free API key (2M chars/month) at [raipii.com](https://raipii.com).

---

## Sanitize modes

### `token` (default)

Replaces PII with labelled placeholder tokens. Safe, lossless, fully reversible.

```python
result = ps.sanitize(
    "Schedule a call with Jane Doe at jane@corp.com on 555-867-5309",
    mode="token",
)
print(result.sanitized_text)
# "Schedule a call with [PERSON_1] at [EMAIL_1] on [PHONE_1]"

# Each token maps back to its original value on restore
restored = ps.restore(llm_response, result.session_id)
```

### `fake_substitute`

Replaces PII with realistic Faker-generated values. The LLM sees natural data and produces better output. All substitutions are reversed on restore.

```python
result = ps.sanitize(
    "Write a summary for John Smith, DOB 1985-03-12, SSN 392-45-7810",
    mode="fake_substitute",
)
print(result.sanitized_text)
# "Write a summary for Michael Torres, DOB 1991-07-24, SSN 847-23-1956"

restored = ps.restore(llm_response, result.session_id)
# LLM response has fake values swapped back to real ones
```

### `redact`

Replaces PII with `[REDACTED]`. One-way — there is nothing to restore. Use when the LLM response must never reference PII at all.

```python
result = ps.sanitize(
    "Patient John Smith, MRN 00123456, DOB 1985-03-12",
    mode="redact",
)
print(result.sanitized_text)
# "Patient [REDACTED], MRN [REDACTED], DOB [REDACTED]"
```

---

## Detect only

Scan text for PII without modifying it. Useful for logging, compliance auditing, or deciding whether to sanitize.

```python
result = ps.detect("My SSN is 392-45-7810 and email is john@acme.com")

print(result.pii_detected)   # True
print(result.risk_level)     # "HIGH"

for entity in result.entities_found:
    print(f"  {entity.type}: {entity.value!r} at {entity.position} ({entity.confidence:.0%} confidence)")
# US_SSN: '392-45-7810' at (10, 21) (100% confidence)
# EMAIL:  'john@acme.com' at (35, 48) (100% confidence)
```

**Risk levels:** `NONE` → `LOW` → `MEDIUM` → `HIGH`

| Risk | Triggered by |
|---|---|
| `HIGH` | SSN, credit card, MRN, bank account, tax ID |
| `MEDIUM` | Person name, email, date of birth, address |
| `LOW` | Any other detected entity |
| `NONE` | No PII found |

---

## Detected entity types

| Type | Example | Tier |
|---|---|---|
| `PERSON` | John Smith | All tiers |
| `EMAIL` | john@acme.com | All tiers |
| `PHONE` | 555-867-5309 | All tiers |
| `US_SSN` | 392-45-7810 | All tiers |
| `CREDIT_CARD` | 4111 1111 1111 1111 | All tiers |
| `DATE_OF_BIRTH` | 1985-03-12 | All tiers |
| `ADDRESS` | 123 Main St, Austin TX | Growth+ |
| `IP_ADDRESS` | 192.168.1.1 | All tiers |
| `MEDICAL_RECORD_NUMBER` | MRN 00123456 | All tiers |
| `BANK_ACCOUNT` | 12345678 | All tiers |
| `TAX_ID` | 12-3456789 | All tiers |
| `IBAN` | GB29 NWBK 6016 1331 9268 19 | All tiers |
| `JWT` | eyJhbGci... | All tiers |
| `AWS_KEY` | AKIA... | All tiers |

> **Starter tier** detects structured PII reliably. Growth and Business tiers add enhanced contextual detection with higher accuracy for unstructured entities such as names and addresses.

---

## Multi-turn conversations

Keep consistent fake substitutions across all turns of a conversation. The same entity always maps to the same fake value within a session.

```python
conv = ps.conversations.create(ttl=86400)  # 24hr TTL

# Turn 1
turn1 = ps.sanitize(
    "My name is John Smith. What should I know about my account?",
    mode="fake_substitute",
    conversation_id=conv.conversation_id,
)
llm_reply_1 = your_llm(turn1.sanitized_text)
response1 = ps.restore(llm_reply_1, turn1.session_id)

# Turn 2 — "John Smith" maps to the SAME fake name as turn 1
turn2 = ps.sanitize(
    "What were you saying about John Smith earlier?",
    mode="fake_substitute",
    conversation_id=conv.conversation_id,
)
llm_reply_2 = your_llm(turn2.sanitized_text)
response2 = ps.restore(llm_reply_2, turn2.session_id)
```

---

## Error handling

```python
from raipii import (
    AuthenticationError,
    QuotaExceededError,
    NotFoundError,
    ValidationError,
    RateLimitError,
    ServiceUnavailableError,
)

try:
    result = ps.sanitize(text)
except AuthenticationError:
    # Invalid or missing API key
    print("Check your API key at raipii.com")
except QuotaExceededError:
    # Monthly character limit reached
    print("Upgrade your plan at raipii.com")
except NotFoundError:
    # Session expired or not found
    print("Session expired — re-sanitize the original text")
except RateLimitError:
    # Too many requests — SDK retries automatically, this means retries exhausted
    print("Rate limit hit")
except ServiceUnavailableError:
    # Detection backend temporarily unavailable — SDK retried 3 times
    print("Service unavailable, try again shortly")
except ValidationError as e:
    print(f"Bad request: {e}")
```

All exceptions inherit from `raipii.RaipiiError` and expose `.status_code` and `.response`.

---

## Retry behaviour

The SDK automatically retries on `429 Too Many Requests` and `503 Service Unavailable` with exponential backoff:

| Attempt | Delay |
|---|---|
| 1st retry | 1s |
| 2nd retry | 2s |
| 3rd retry | 4s |

Default `max_retries=3`. Override:

```python
ps = raipii.Raipii(api_key="...", max_retries=5)
ps_no_retry = raipii.Raipii(api_key="...", max_retries=0)
```

---

## Client options

```python
ps = raipii.Raipii(
    api_key="ps_live_...",    # or RAIPII_API_KEY env var
    base_url="https://api.raipii.com",  # override for testing
    timeout=30,               # request timeout in seconds
    max_retries=3,            # retries on 429/503
)
```

---

## Return types

### `SanitizeResult`
| Field | Type | Description |
|---|---|---|
| `session_id` | `str` | Pass to `restore()` |
| `sanitized_text` | `str` | Text with PII replaced |
| `entities_found` | `list[EntityFound]` | Each detected entity |
| `char_count` | `int` | Characters processed |
| `usage.chars_billed` | `int` | Characters billed |
| `conversation_id` | `str \| None` | Echo of the conversation_id passed in |

### `RestoreResult`
| Field | Type | Description |
|---|---|---|
| `restored_text` | `str` | Text with original PII restored |
| `substitutions_reversed` | `int` | Number of tokens replaced |
| `usage.chars_billed` | `int` | Characters billed |

### `DetectResult`
| Field | Type | Description |
|---|---|---|
| `entities_found` | `list[DetectedEntity]` | Detected PII entities |
| `pii_detected` | `bool` | True if any PII found |
| `risk_level` | `str` | `NONE` / `LOW` / `MEDIUM` / `HIGH` |
| `usage.chars_billed` | `int` | Characters billed |

---

## Caveats

- **Session TTL** — sessions expire after `session_ttl` seconds (default 1hr). Calling `restore()` after expiry raises `NotFoundError`. Store the `session_id` and call restore promptly after getting the LLM response.
- **`redact` mode has no restore** — `[REDACTED]` tokens contain no reversible information. Calling `restore()` on a redact session returns the text unchanged.
- **Conversation TTL** — conversation sessions expire after their TTL (default 24hr). After expiry, new turns start a fresh mapping.
- **Characters billed** — both `sanitize` and `restore` bill by character count of the input text. `detect` also bills. Free tier: 2M chars/month.
- **Starter tier** detects structured PII reliably. Upgrade to Growth for enhanced contextual detection of names and addresses in free-form text.

---

## Get an API key

Free tier — 2M characters/month, no credit card required: [raipii.com](https://raipii.com)
