Metadata-Version: 2.4
Name: nss-client
Version: 0.1.0
Summary: Fire-and-forget async Python client for the NSS (Notification Sending Service) HTTP API, with a drop-in Django email backend.
Author: FPT platform team
License: Apache-2.0
Project-URL: Homepage, https://git.fpt.net/fli-backend/platform/nss
Project-URL: Documentation, https://git.fpt.net/fli-backend/platform/nss/-/tree/main/sdk/python
Project-URL: Source, https://git.fpt.net/fli-backend/platform/nss/-/tree/main/sdk/python
Project-URL: Issues, https://git.fpt.net/fli-backend/platform/nss/-/issues
Keywords: nss,notification,email,smtp,django,async,httpx
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Framework :: Django
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
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 :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.27
Provides-Extra: django
Requires-Dist: Django>=4.2; extra == "django"
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Requires-Dist: pytest-asyncio>=0.21; extra == "test"
Requires-Dist: Django>=4.2; extra == "test"

# nss-client

**Fire-and-forget** async Python SDK for the FPT **NSS** notification API.

Design goal: the caller should never block on notification delivery. Every
send is scheduled on a background asyncio event loop (started lazily in a
daemon thread) with a **200ms** per-request timeout. If the enqueue takes
longer than that, the request is dropped and logged — the calling thread
already moved on.

## Install

```bash
pip install /path/to/nss-python-sdk           # local
# or
pip install "nss-client[django]"              # once published
```

## API surface

```python
from nss_client import NSSClient, NSSError, fire, NSSEmailBackend, shutdown
```

- `NSSClient` — plain async client (`await nss.send(...)`). 200ms timeout by default.
- `fire(coro)` — schedule any coroutine on the background loop, return immediately.
- `NSSEmailBackend` — Django backend that wires `fire()` into `send_messages`.
- `shutdown()` — stop the background loop (called automatically at exit).

## Async use — inside asyncio / async Django views

```python
import asyncio
from nss_client import NSSClient

async def main():
    async with NSSClient(
        base_url="http://nss-api:8080",
        token="Bearer …",
        tenant_id="platform-cloud",
        # timeout=0.2 by default (200ms) — override if you need more time
    ) as nss:
        await nss.send(
            to="user@example.com",
            provider_id="prov-uuid",   # or channel="smtp"
            subject="Hi",
            body="Hello world",
        )

asyncio.run(main())
```

Errors are logged and swallowed by default. Opt into exceptions when you
actually care about the outcome:

```python
await nss.send(..., raise_on_error=True)
```

## Fire-and-forget from sync code (Django views, Celery tasks, scripts)

Use the top-level `fire()` — the coroutine runs on the shared background
loop and the call returns instantly:

```python
from nss_client import NSSClient, fire

_client = NSSClient(base_url="…", token="…", tenant_id="…", timeout=0.2)

def user_signed_up(user):
    fire(_client.send(
        to=user.email,
        provider_id="prov-uuid",
        subject="Welcome",
        body=f"Hi {user.name}",
    ))
    # returns in ~microseconds — HTTP happens on the background loop
```

## Django integration

Two ways to wire it. Pick either — SDK reads both.

### A) `settings.py` block (explicit)

```python
EMAIL_BACKEND = "nss_client.NSSEmailBackend"

NSS = {
    "BASE_URL":    "http://nss-api:8080",
    "TOKEN":       os.environ["NSS_TOKEN"],
    "TENANT_ID":   "platform-cloud",
    "PROVIDER_ID": "prov-uuid",   # or "CHANNEL": "smtp"
    "TIMEOUT":     0.2,
}
```

### B) Env vars only (no settings block needed)

Just export the env vars — SDK falls back to `NSS_<KEY>` if `settings.NSS`
is absent:

```bash
export NSS_BASE_URL=http://nss-api:8080
export NSS_TOKEN="Bearer …"
export NSS_TENANT_ID=platform-cloud
export NSS_PROVIDER_ID=prov-uuid
export NSS_TIMEOUT=0.2
```

```python
# settings.py — just this one line is enough
EMAIL_BACKEND = "nss_client.NSSEmailBackend"
```

### Skip mode

If **neither** `settings.NSS` nor `NSS_BASE_URL` env is set, the backend
enters **silent skip mode**: `send_mail()` still returns the message count
(so app code that expects `sent == 1` keeps working) but nothing is sent
and no exception is raised. A one-time WARNING is logged so ops know the
backend is disarmed.

Then Django's stock helpers work — but never block the request:

```python
from django.core.mail import send_mail, EmailMultiAlternatives

def signup(request):
    User.objects.create(...)
    send_mail("Welcome!", "Thanks for joining.", None, [request.POST["email"]])
    return redirect("/thanks")   # HTTP request already dispatched on background loop
```

Bulk sends fan out onto the same loop — `send_messages([...])` returns
the message count instantly:

```python
from django.core.mail import EmailMessage, get_connection
with get_connection() as conn:
    conn.send_messages([EmailMessage("Hi", "body", to=[u.email]) for u in queryset])
```

## Templates

```python
await nss.send(
    to="user@example.com",
    provider_id="prov-uuid",
    template_id="welcome-v1",
    payload={"name": "Ben", "code": "ABC123"},
)
```

## Logging

All background failures land on the `nss_client` logger at `WARNING`. Wire
it into your usual pipeline:

```python
LOGGING = {
    "loggers": {"nss_client": {"level": "WARNING", "handlers": ["console"]}},
}
```
