Metadata-Version: 2.4
Name: esper-py-client
Version: 0.1.0
Summary: Python client for Esper bot detection API
Project-URL: Homepage, https://github.com/stonehedgelabs/esper
Project-URL: Documentation, https://docs.esperr.com
Project-URL: Repository, https://github.com/stonehedgelabs/esper
Project-URL: Bug Tracker, https://github.com/stonehedgelabs/esper/issues
Author-email: Esper Team <team@esperr.com>
Maintainer-email: Esper Team <team@esperr.com>
License: MIT
Keywords: api,bot-detection,esper,sdk,security
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.7
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
Classifier: Typing :: Typed
Requires-Python: >=3.7
Requires-Dist: httpx>=0.24.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: typing-extensions>=4.0.0; python_version < '3.8'
Provides-Extra: dev
Requires-Dist: black>=23.0.0; extra == 'dev'
Requires-Dist: isort>=5.12.0; extra == 'dev'
Requires-Dist: mypy>=1.4.0; extra == 'dev'
Requires-Dist: pre-commit>=3.3.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest-httpx>=0.22.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# Esper Python Client

Lightweight Python client library for Esper bot detection API.

## Installation

```bash
pip install esper-py-client
```

## Quick Start

```python
from esper import EsperClient, EsperConfig, MitigationRequest, MitigationAction

# Initialize client
config = EsperConfig(api_key="your-api-key")
client = EsperClient(config)

# Check if request should be mitigated
request = MitigationRequest(
    ip="192.168.1.1",
    user_agent="Mozilla/5.0...",
    path="/api/endpoint",
    method="POST"
)

response = client.check_mitigation(request)

if response.action == MitigationAction.BLOCK:
    # Block the request
    print("Request blocked:", response.reason)
elif response.action == MitigationAction.CHALLENGE:
    # Present challenge to user
    print("Challenge required:", response.challenge)
else:
    # Allow the request
    print("Request allowed")
```

## Async Support

```python
import asyncio
from esper import AsyncEsperClient, EsperConfig, MitigationRequest

async def check_request():
    config = EsperConfig(api_key="your-api-key")

    async with AsyncEsperClient(config) as client:
        request = MitigationRequest(ip="192.168.1.1")
        response = await client.check_mitigation(request)
        return response

# Run async function
response = asyncio.run(check_request())
```

## Configuration

```python
from esper import EsperConfig

config = EsperConfig(
    api_key="your-api-key",              # Required
    api_url="https://api.esperr.com",    # Optional
    beacon_url="https://beacon.esperr.com", # Optional
    timeout=30.0,                         # Optional (seconds)
    max_retries=3,                        # Optional
    retry_delay=1.0                       # Optional (seconds)
)
```

## API Reference

### Check Mitigation

```python
from esper import MitigationRequest

request = MitigationRequest(
    ip="192.168.1.1",
    user_agent="Mozilla/5.0...",
    path="/api/endpoint",
    method="POST",
    headers={"X-Custom": "value"},
    metadata={"user_id": "123"}
)

response = client.check_mitigation(request)
print(f"Action: {response.action}")
print(f"Score: {response.score}")
print(f"Reason: {response.reason}")
```

### Verify Challenge

```python
# Verify user's challenge response
is_valid = client.verify_challenge(
    token="challenge-token",
    response="user-response"
)

if is_valid:
    print("Challenge passed")
else:
    print("Challenge failed")
```

### Send Beacon Event

```python
from esper import BeaconEvent

# Send telemetry event
event = BeaconEvent(
    type="page_view",
    ip="192.168.1.1",
    user_agent="Mozilla/5.0...",
    session_id="session-123",
    data={"page": "/home", "referrer": "/login"}
)

client.send_beacon_event(event)
```

### Get Usage Statistics

```python
# Get API usage
usage = client.get_usage()
print(f"Total requests: {usage.requests}")
print(f"Blocked: {usage.blocked}")
print(f"Challenged: {usage.challenged}")
print(f"Allowed: {usage.allowed}")
```

## Django Integration

```python
# middleware.py
from django.http import JsonResponse
from esper import EsperClient, EsperConfig, MitigationRequest

class EsperMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        config = EsperConfig(api_key=settings.ESPER_API_KEY)
        self.client = EsperClient(config)

    def __call__(self, request):
        # Check mitigation
        mitigation_request = MitigationRequest(
            ip=self.get_client_ip(request),
            user_agent=request.META.get('HTTP_USER_AGENT'),
            path=request.path,
            method=request.method
        )

        try:
            response = self.client.check_mitigation(mitigation_request)

            if response.action == MitigationAction.BLOCK:
                return JsonResponse({"error": "Access denied"}, status=403)
            elif response.action == MitigationAction.CHALLENGE:
                return JsonResponse({"challenge": response.challenge.dict()}, status=429)
        except Exception as e:
            # Log error and allow request on failure
            logger.error(f"Esper error: {e}")

        return self.get_response(request)

    def get_client_ip(self, request):
        x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
        if x_forwarded_for:
            return x_forwarded_for.split(',')[0]
        return request.META.get('REMOTE_ADDR')
```

## FastAPI Integration

```python
from fastapi import FastAPI, Request, HTTPException
from esper import AsyncEsperClient, EsperConfig, MitigationRequest

app = FastAPI()
config = EsperConfig(api_key="your-api-key")
esper = AsyncEsperClient(config)

@app.middleware("http")
async def esper_middleware(request: Request, call_next):
    # Check mitigation
    mitigation_request = MitigationRequest(
        ip=request.client.host,
        user_agent=request.headers.get("user-agent"),
        path=request.url.path,
        method=request.method
    )

    try:
        response = await esper.check_mitigation(mitigation_request)

        if response.action == MitigationAction.BLOCK:
            raise HTTPException(status_code=403, detail="Access denied")
        elif response.action == MitigationAction.CHALLENGE:
            return JSONResponse(
                status_code=429,
                content={"challenge": response.challenge.dict()}
            )
    except Exception as e:
        # Log error and continue
        logger.error(f"Esper error: {e}")

    return await call_next(request)
```

## Error Handling

```python
from esper import (
    EsperAPIError,
    EsperConnectionError,
    EsperTimeoutError
)

try:
    response = client.check_mitigation(request)
except EsperAPIError as e:
    print(f"API error: {e.message}")
    print(f"Status code: {e.status_code}")
    print(f"Error code: {e.error_code}")
except EsperConnectionError as e:
    print(f"Connection error: {e}")
except EsperTimeoutError as e:
    print(f"Timeout error: {e}")
```

## Testing

```bash
# Install development dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Run tests with coverage
pytest --cov=esper

# Type checking
mypy src/esper

# Linting
ruff check src/esper

# Formatting
black src/esper
isort src/esper
```

## License

MIT