Metadata-Version: 2.4
Name: softechlog
Version: 1.3.0
Summary: Server-side SDK for Softechlog — drop-in user activity logging & customer-facing activity feeds
Author-email: FutureGenSystems <hello@softechlog.com>
License: MIT
Project-URL: Homepage, https://softechlog.com
Project-URL: Documentation, https://softechlog.com/docs/quickstart
Project-URL: Repository, https://github.com/ayush-parida/softechlog.com
Project-URL: Issues, https://github.com/ayush-parida/softechlog.com/issues
Keywords: softechlog,activity-log,audit-trail,saas,tracking,activity-feed
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: Framework :: FastAPI
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.24.0
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.100.0; extra == "fastapi"
Provides-Extra: verify
Requires-Dist: cryptography>=41; extra == "verify"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: fastapi>=0.100; extra == "dev"
Requires-Dist: httpx>=0.24; extra == "dev"
Requires-Dist: cryptography>=41; extra == "dev"
Dynamic: license-file

# softechlog

Server-side Python SDK for [Softechlog](https://softechlog.com) — drop-in user activity logging and customer-facing activity feeds for SaaS. Works with FastAPI, Django, Flask, or plain scripts. Fully typed.

## Install

```bash
pip install softechlog
```

## Quick start

```python
# app/softechlog.py — initialize once, import everywhere
import os
from softechlog import Softechlog

log = Softechlog(secret_key=os.environ["SOFTECHLOG_SECRET_KEY"])  # stl_sk_… — server-side only
```

## Track an event (sync — Django, Flask, scripts)

```python
from app.softechlog import log

log.track(
    actor={"id": str(user.id), "name": user.name, "email": user.email},
    action="member.invited",                 # resource.verb
    target_type="workspace",
    target_id=str(workspace.id),
    target_name=workspace.name,
    metadata={"role": "admin"},              # any JSON, ≤ 4 KB
    session_id=request.session.session_key, # optional: groups events into a session
)
```

`track()` returns immediately: events are queued and sent by a background thread, so a slow or unreachable Softechlog can never stall your request handler. Call `log.flush()` before a short-lived process (a serverless function, a cron job) exits — `log.close()` and interpreter exit flush automatically. Pass `background=False` to send inline and get the created event back.

## Track an event (async — FastAPI)

```python
from fastapi import BackgroundTasks

@router.delete("/files/{file_id}")
async def delete_file(file_id: str, background: BackgroundTasks, current_user: User = Depends(get_current_user)):
    file = await File.get(file_id)
    await file.delete()
    # Runs after the response is sent — the client never waits on Softechlog.
    background.add_task(
        log.atrack,
        actor={"id": str(current_user.id), "name": current_user.name},
        action="file.deleted",
        target_type="file",
        target_id=file_id,
        target_name=file.name,
    )
    return {"deleted": True}
```

`await log.atrack(...)` directly is fine too when you want the event id back; it is bounded by `timeout × (retries + 1)` plus backoff.

`track()` / `atrack()` **never raise** — failures return `None` and log a warning (unless `silent=True`). Requests are retried on network errors, timeouts, `429` and `5xx` with exponential backoff. Retries are safe: every event carries an `Idempotency-Key`, so an event the API stored before the response was lost is never recorded twice. Invalid actions and oversized metadata are rejected locally without a round-trip. Offset-less `datetime` timestamps are sent as UTC.

## FastAPI middleware — record the *end user's* IP and User-Agent

Without it, the API only sees your server's address.

```python
from softechlog.fastapi import SoftechlogMiddleware

app.add_middleware(SoftechlogMiddleware)   # honours X-Forwarded-For; pass trust_proxy_headers=False if not behind a proxy
```

Every `track()`/`atrack()` call made while handling a request now carries `context={"ip_address": …, "user_agent": …}` automatically. You can also pass `context=` explicitly anywhere.

## Show a feed to your users

The `<softechlog-feed>` web component needs a **feed token** — a short-lived credential scoped to one user. Mint it on your server:

```python
@router.get("/activity-token")
async def activity_token(current_user: User = Depends(get_current_user)):
    tok = await log.afeed_token(actor_id=str(current_user.id), ttl_seconds=3600)
    return {"token": tok.token, "expires_at": tok.expires_at}
```

```html
<script src="https://softechlog.com/feed.js"></script>
<softechlog-feed feed-token="stl_ft_…"></softechlog-feed>
```

**B2B? Show a customer's admin their whole account.** Track with `tenant_id=str(org.id)`, then mint `log.feed_token(tenant_id=str(org.id))` — the token reads every event in that account and nothing else. Pass `actor_id` too to show one member within it.

`feed_token()` / `afeed_token()` raise `SoftechlogError` on failure.

## Erase a user (GDPR right to erasure)

Stop sending events for the user first, then:

```python
erasure = log.erase("user_123", mode="delete", reference="DSR-2026-0142")
if erasure.status != "completed":           # 202 → finishing in the background
    erasure = log.wait_for_erasure(erasure.id)
```

- `mode`: `"delete"` (default) removes their events; `"redact"` keeps what happened and when but removes who, IP, device, metadata and targets.
- `target_types` (default `["user"]`): target types whose `target_id` is this user, so mentions in other people's events are scrubbed. `[]` leaves them alone.
- `reference`: your ticket id — **no personal data**; it is stored and appears in the evidence event.
- `get_erasure(id)`, `list_erasures(actor_id=, status=, limit=, cursor=)`, `wait_for_erasure(id, timeout=60, interval=1)` (raises on `failed` or timeout). Async: `aerase`, `aget_erasure`, `alist_erasures`, `await_for_erasure`.

The returned `Erasure` never contains the user id — store it with the ticket as your record. See [softechlog.com/docs/erasure](https://softechlog.com/docs/erasure).

## Tamper evidence

`track()` with `background=False` (and `atrack()`) return `leaf_hash` — the event's integrity leaf, sealed into a signed, hash-chained checkpoint within 5 minutes. Keep it for high-value events.

```python
# Daily: pin the latest checkpoint somewhere you control (e.g. S3 Object Lock)
cp = log.latest_checkpoint()                 # None before the first seal

report = log.verify_integrity(pins=[(cp.seq, cp.hash)], expected_retention_days=90)
if not report.ok:
    print(report.failures)
```

Also: `integrity_status()`, `list_checkpoints(after_seq=, limit=)`, `checkpoint_bundle(seq)`, `integrity_keys()`, each with an `a`-prefixed async twin.

### Verify offline — `softechlog.integrity`

```bash
pip install "softechlog[verify]"            # adds cryptography for Ed25519
```

```python
from softechlog.integrity import verify_bundle, verify_chain

keys = log.integrity_keys()
page = log.list_checkpoints(limit=500)
verify_chain(page.checkpoints, keys, pins=my_pins)   # VerifyResult(ok, failures, warnings, counts)
verify_bundle(log.checkpoint_bundle(42), keys)
```

Bundles contain your events' IPs and user agents — handle them like an export. Encoding and threat model: [softechlog.com/docs/integrity](https://softechlog.com/docs/integrity).

## Errors

`track()` / `atrack()` never raise. Everything else raises `SoftechlogError`, with `.status` (HTTP status, or `None` for network errors) and `.body`.

## Options

| Option        | Type  | Default                      | Description                                                              |
|---------------|-------|------------------------------|--------------------------------------------------------------------------|
| `secret_key`  | str   | required                     | Your `stl_sk_…` secret key                                               |
| `base_url`    | str   | `https://api.softechlog.com` | Override the API base URL                                                |
| `timeout`     | float | `5.0`                        | Request timeout in seconds                                               |
| `retries`     | int   | `2`                          | Retries on network errors, `429`, `5xx` (backoff, honours `Retry-After`; idempotent) |
| `silent`      | bool  | `False`                      | Suppress warnings on errors                                              |
| `background`  | bool  | `True`                       | Sync `track()` queues to a worker thread instead of sending inline       |
| `queue_size`  | int   | `1000`                       | Max queued events in background mode (oldest are kept, new ones dropped) |
| `transport`   | —     | —                            | Custom `httpx` transport (tests)                                         |

The client keeps persistent HTTP connections; call `log.close()` / `await log.aclose()` on shutdown (or use it as a context manager).

## Limits (enforced by the API)

- `action` ≤ 200 chars, `actor.id` ≤ 255, `metadata` ≤ 4 KB with finite numbers; the `softechlog.` action namespace is reserved
- 60 erasure requests / minute per project; 10 `verify_integrity()` / `checkpoint_bundle()` calls / minute per project
- 1,000 events / minute per secret key
- Free plan: 10,000 events / month (`402` when exceeded)

MIT © FutureGenSystems
