Metadata-Version: 2.5
Name: norbix
Version: 3.13.0
Summary: Official Python SDK for Norbix — split API and Hub clients with flat module access.
Project-URL: Homepage, https://norbix.ai
Project-URL: Repository, https://github.com/norbix-code/sdk-python
Project-URL: Issues, https://github.com/norbix-code/sdk-python/issues
Author: UAB Isidos
License: MIT
License-File: LICENSE
Keywords: api,baas,hub,norbix,python,sdk
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Requires-Dist: pydantic>=2.0
Description-Content-Type: text/markdown

# norbix-python

[![CI](https://github.com/norbix-code/sdk-python/actions/workflows/ci.yml/badge.svg)](https://github.com/norbix-code/sdk-python/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/norbix.svg)](https://pypi.org/project/norbix/)
[![Python](https://img.shields.io/badge/python-%3E=3.10-blue)](https://python.org)
[![License](https://img.shields.io/pypi/l/norbix.svg)](./LICENSE)

Official Python SDK for [Norbix](https://norbix.ai).
Use split clients with flat module access:

- `NorbixApi` for API scope (`client.database`, `client.membership`, `client.public`, ...)
- `NorbixHub` for Hub scope (`client.database`, `client.account`, ...)

## Install

```bash
uv add norbix
```

Optional: load `.env` in apps with `python-dotenv` (`load_dotenv()` before constructing `Norbix()`).

## Quickstart

```python
from norbix_python import NorbixApi

# Service mode
norbix = NorbixApi(api_key="<api_key>", project_id="proj_123")

norbix.database.find("orders", take=20, skip=0, orderBy=[{"field": "createdAt", "direction": "desc"}])
```

```python
# User mode
from norbix_python import LoginCredentials, NorbixApi

norbix = NorbixApi(project_id="proj_123")
norbix.login(LoginCredentials(user_name="alice@team.io", password="secret"))
norbix.database.find("orders", take=10)
```

### Async client

```python
from norbix_python import AsyncNorbix

async def main() -> None:
    async with AsyncNorbix(api_key="...", project_id="proj_123") as client:
        await client.api.echo.echo()

# asyncio.run(main())
```

## Real-world examples

### 1) List recent orders (API scope)

```python
from norbix_python import DatabaseFindResult, NorbixApi, NorbixError

norbix = NorbixApi(api_key="sk_live_xxx", project_id="proj_123")

try:
    raw = norbix.database.find("orders", take=20, skip=0, orderBy=[{"field": "createdAt", "direction": "desc"}])
    typed = DatabaseFindResult.model_validate(raw) if isinstance(raw, dict) else DatabaseFindResult()
    items = typed.results
    print(f"Fetched {len(items)} orders")
except NorbixError as exc:
    print(exc.code, exc.status, exc.message)
```

### 2) Login as user and load profile

```python
from norbix_python import LoginCredentials, NorbixApi

norbix = NorbixApi(project_id="proj_123")

auth = norbix.login(LoginCredentials(user_name="alice@team.io", password="secret"))
print("Logged in, token prefix:", str(auth.get("bearerToken", ""))[:16])

users = norbix.membership.get_users()
print("Users response:", users)
```

### 3) Account Hub call (token only)

```python
from norbix_python import NorbixHub

norbix = NorbixHub(
    api_key="sk_live_xxx",
    project_id="proj_123",
)  # no account_id: the gateway takes the account from the signed-in session

account = norbix.account.get_account_profile()
print(account)
```

### 4) Preview a notification with its signed link (no sign-in)

A preview link carries a signed `hash`. The hash alone is the key: no API key
and no login are needed. If the client does have a key or a token, it is still
sent.

```python
from norbix_python import NorbixHub

norbix = NorbixHub(project_id="proj_123")  # no api_key, no bearer_token

preview = norbix.notifications.preview_push_notification(hash=signed_link)
```

`preview_email_notification` and `preview_sms_notification` work the same way.
A signed-in member can pass `projectId` + `notificationId` instead of `hash`.
The gateway answers `401` for a bad or expired link, `403` for a missing
permission and `400` for a malformed request.

## End-user AI chat and project AI settings

A signed-in project user (bearer token from login) talks to the project's AI
assistant through `norbix.api.ai`. `start_end_user_chat_turn` answers at once
with a `turnId`; the answer streams over the gateway's SSE endpoint on the
user's own channel `ai-chat:{projectId}:{authId}` (events `ai.chat.turn.*`,
`ai.chat.session.*`). A subscription to another user's channel is refused with
403 and `responseStatus.errorCode = "AiChatChannelRefused"` — do not retry it.

```python
avail = norbix.api.ai.get_end_user_chat_availability()
session = norbix.api.ai.create_end_user_chat_session(title="Help")
turn = norbix.api.ai.start_end_user_chat_turn(sessionId=session["id"], message="What can you do?")
entries = norbix.api.ai.get_end_user_chat_entries(session_id=session["id"])
```

Project owners configure the assistant on the Hub: `norbix.hub.account`
(`get_project_ai_settings`, `update_project_ai_settings`,
`create_project_ai_assistant`, `update_project_ai_assistant`,
`delete_project_ai_assistant`, `get_project_ai_usage`, `set_admin_portal_enabled`)
and `norbix.hub.ai` (`get_embedding_integrations`, `save_embedding_integration`,
`get_embedding_integration`, `delete_embedding_integration`,
`test_embedding_integration`, `set_llm_integration_as_default`). Full tables:
[docs/api/ai.md](./docs/api/ai.md), [docs/hub/ai.md](./docs/hub/ai.md),
[docs/hub/account.md](./docs/hub/account.md).

## Project settings, public pages, MCP and AI service users

Project owners manage the Admin Portal and legal pages on the Hub,
`norbix.hub.account`: `update_project_admin_url`, `update_project_legal_documents`,
`update_project_expose_legal`, `get_admin_portal_structure`,
`assign_admin_portal_service_user`. Two switches choose what the public Admin
Portal config shows: `update_project_expose_brand` (brand, on by default) and
`update_project_expose_auth` (sign-in methods and password policy, off by default).

Once the legal pages are exposed, anyone can read them — and the project's
public config — from the API host with no sign-in, `norbix.api.public`:

```python
norbix.hub.account.update_project_legal_documents("proj_123", termsMarkdown="# Terms", privacyMarkdown="# Privacy")
norbix.hub.account.update_project_expose_legal("proj_123", exposed=True)
norbix.api.public.get_public_project_legal("proj_123", "terms")  # {"kind", "title", "body", "available"}
norbix.api.public.get_public_project_config("proj_123")
```

AI agents reach Norbix through the developer MCP endpoint with an AI service
user key: `create_ai_service_user`, `list_ai_service_users`,
`rotate_ai_service_user_key`, `revoke_ai_service_user_key`,
`delete_ai_service_user`, then `mcp` / `mcp_stream` / `mcp_end_session`.
Full tables and an MCP example: [docs/hub/account.md](./docs/hub/account.md),
[docs/api/public.md](./docs/api/public.md).

## Campaigns and triggers: the provider integration id

The server now **requires the provider integration id** when you create an
Email, Push or SMS campaign, and on every trigger action. Pick it from
`get_email_integrations` / `get_push_integrations` / `get_sms_integrations`.
Email and Push put it inside `campaign`; SMS sends it at the top level, next to
(and different from) `databaseIntegrationId`:

```python
norbix.hub.notifications.create_email_campaign(
    campaign={"templateId": "tpl_123", "integrationId": "int_email", "source": "allUsers"}
)
norbix.hub.notifications.create_push_campaign(
    campaign={"source": "allUsers", "templateId": "tpl_456", "integrationId": "int_push"}
)
norbix.hub.notifications.create_sms_campaign(
    templateId="tpl_789",
    integrationId="int_sms",
    deliveryType="PhoneNumbers",
    phoneNumbers={"recipientsSourceType": "PhoneNumbers", "phoneNumbers": ["+37060000000"]},
)
```

A trigger action (`save_schema_trigger`, `save_files_trigger`,
`save_membership_trigger`, `save_payments_trigger`) takes the same required
`integrationId`, plus the optional `language` (template language) and
`initiatorId` (who the send is attributed to) on Email, Push and SMS actions.
Two related calls:

- `norbix.hub.triggers.get_triggers_needing_attention(triggerType="Schema")` —
  triggers that need a fix (for example, their provider is gone); `{"items": [...]}`.
- `norbix.hub.account.check_project_languages(project_id, languages=["en", "de"])` —
  a dry run before `update_project_languages`: which templates miss a language;
  `{"templates": [...]}`. Needs only a key or a token.

SMS campaigns can also go to the **account owner and team members**
(`deliveryType="AccountUsers"`, `accountUsers={"recipientsSourceType": "AccountUsers", "recipients": [...]}`);
each member gets it on the phone saved with
`norbix.hub.account.update_my_account_user_phone(phone="+370...")`
(`get_my_account_user_profile()` reads it back). `get_sms_campaigns(campaignId=...)`
returns one campaign. SMS templates have a body only (no `subject`).

Details: [docs/hub/triggers.md](./docs/hub/triggers.md),
[docs/hub/sms.md](./docs/hub/sms.md), [docs/hub/push.md](./docs/hub/push.md),
[docs/hub/account.md](./docs/hub/account.md).

## Errors

```python
from norbix_python import NorbixError

try:
    norbix.files.get_file_info(integration_id, path="a/b.txt")
except NorbixError as exc:
    # http_status / error_code are the names every Norbix SDK uses.
    # status / code are the same values, kept for older code.
    print(exc.http_status, exc.error_code, exc.message)
    for item in exc.errors:
        print(item.error_code, item.field_name, item.message)
    print(exc.body)  # the answer exactly as it arrived
```

The Database rules the gateway checks — `expand_references` for `{ id, display }`
references, nested documents and `array_filters`, the typed schema fields, files
by id, and the code of each refusal — are on
[docs/database-rules.md](./docs/database-rules.md).

`message` and `error_code` are the gateway's own. The gateway puts them inside
`responseStatus.errors[]`, so the SDK reads that list first, takes the first
entry for the message and the code, and keeps every entry in `errors`. Only
when the body has no `responseStatus` are the top-level `message` and
`errorCode` read. `Request failed (HTTP <status>)` with the code
`HTTP_<status>` is the last fallback, used when the body says nothing — a 500
page that is not JSON, say.

### Breaking change — a refused call now raises

The gateway answers a business refusal (an unknown id, a rule that says no)
with **HTTP 200** and `responseStatus.isSuccess = False`. The SDK used to hand
that answer back as a normal value, so code carried on as if the call had
worked. It now raises a `NorbixError` with `http_status` 200 and the gateway's
message and error code.

If your code checked `result["responseStatus"]["isSuccess"]` itself, move that
check into a `try / except`. Endpoints that answer with raw bytes rather than a
document (file download, the public file link) are not JSON and are unchanged.
Both the sync and the async client follow the same rule.

## Breaking changes (recent major-style refresh)

- Methods use **snake_case** (`find_one`, `get_database_schemas`) instead of camelCase.
- Path parameters are **positional or keyword** arguments (for example `find("orders", ...)`,
  `find_one("orders", id)`). Remaining query/body fields are passed as keyword args.
- Use **typed errors** where helpful: `AuthenticationError`, `NotFoundError`, `RateLimitError`,
  `ValidationError` (all subclass `NorbixError`).

## Authentication

- API key: set `api_key` or `NORBIX_API_KEY`
- JWT bearer: set `bearer_token`, `NORBIX_BEARER_TOKEN`, or call `norbix.login(...)`
- If both are configured, bearer token wins
- If neither is configured, SDK raises `NORBIX_NOT_AUTHENTICATED`

API keys and JWTs are sent as `Authorization: Bearer ...` (document your backend expectations).

## Configuration from environment

```bash
NORBIX_API_KEY=sk_live_...
NORBIX_PROJECT_ID=proj_123
NORBIX_ACCOUNT_ID=acc_456
NORBIX_API_URL=https://api.norbix.ai
NORBIX_HUB_URL=https://hub.norbix.ai
NORBIX_REGION=nb-eu-germany
```

```python
norbix = NorbixApi()  # reads from environment when values omitted
```

## Multi-region support

Norbix can run a project in one or more regions (region codes like
`nb-eu-germany`). The SDK has **no default region**: when no region is
configured, no region header is sent and the standard base URLs are used.

### Selecting a region

Resolution order: explicit `region=` on the client → `NORBIX_REGION`
environment variable → unset (no header).

```python
from norbix_python import Norbix

norbix = Norbix(api_key="sk_live_xxx", project_id="proj_123", region="nb-eu-germany")
```

`region=` is accepted by all clients: `Norbix`, `NorbixApi`, `NorbixHub`,
and `AsyncNorbix`. Every request then carries the `nb-region` header.

### Switching at runtime

```python
norbix.set_region("nb-eu-germany")   # subsequent requests target this region
norbix.get_region()                  # "nb-eu-germany"
norbix.set_region(None)              # clear — no nb-region header is sent
```

Available on the sync clients and `AsyncNorbix` alike.

### Per-call override (header only)

The `client.hub.regions` methods accept a per-call `region=` that overrides
the client default **for that request's `nb-region` header only** — the
request URL is never changed by a per-call region:

```python
norbix.hub.regions.list(region="nb-us-east")
```

### Regional base URLs

When a region is set and you are using the SDK-default base URLs
(`https://api.norbix.ai` / `https://hub.norbix.ai`), the SDK prefixes the
region as a subdomain:

```text
region="nb-eu-germany"  →  https://nb-eu-germany.api.norbix.ai
                           https://nb-eu-germany.hub.norbix.ai
```

Custom base URLs (`base_url_api=`, `base_url_hub=`, `NORBIX_API_URL`,
`NORBIX_HUB_URL`) are **never rewritten** — self-hosted and custom
deployments are unaffected; the `nb-region` header is still sent when a
region is configured.

### Managing regions (Hub)

`regions.list()` needs no token at all; `update_project_regions` needs a key
or a bearer token (see [Project vs account scope](#project-vs-account-scope)).

```python
from norbix_python import NorbixHub

norbix = NorbixHub(api_key="sk_live_xxx", project_id="proj_123")

# Regions available to the account.
# Response shape: {"items": [{"id": ..., "continent": ..., "name": ...}, ...]}
# where "id" is the region code (e.g. "nb-eu-germany").
regions = norbix.regions.list()

# Update the regions a project runs in (omitted fields are left unchanged)
norbix.regions.update_project_regions(
    "proj_123",
    primary_region="nb-eu-germany",
    additional_regions=["nb-us-east"],
)

# Pin a new project to regions at creation time
norbix.account.create_project(
    name="my-project",
    primary_region="nb-eu-germany",
    additional_regions=["nb-us-east"],
)
```

With the combined client the same modules live under `client.hub`
(`norbix.hub.regions.list()`, `norbix.hub.account.create_project(...)`).

### Async

```python
from norbix_python import AsyncNorbix

async def main() -> None:
    async with AsyncNorbix(
        api_key="sk_live_xxx",
        project_id="proj_123",
        region="nb-eu-germany",
    ) as client:
        regions = await client.hub.regions.list()
        await client.hub.regions.update_project_regions(
            "proj_123",
            primary_region="nb-eu-germany",
            region="nb-us-east",  # per-call header override
        )
        client.set_region(None)  # clear at runtime
```

## Project vs account scope

- `project_id` is required (set explicitly or via env).
- `account_id` is optional, and no Hub account method needs it: the gateway takes the account from the signed-in session or the project id in the path
- Account routes (profile, projects, project settings, team, licenses, …) need only a key or a bearer token
- Sign-up (`hub.account.create_account`), accepting an invitation (`hub.account.create_team_member_from_invitation`), the region list (`hub.account.get_account_regions`, `hub.regions.list`) and `hub.account.verify_account(accountId=..., token=...)` need no token at all — they are public on the gateway, and no `Authorization` header is sent
- Optional-scope methods (the signed notification preview links) send auth when the client has a token and never require one

## SDK maintenance

Regenerate API and Hub modules from DTO stubs:

```bash
uv run python scripts/generate_endpoints.py
```

This refreshes `src/norbix_python/api/`, `hub/`, matching tests under `tests/api` and `tests/hub`, and docs under `docs/`.

Some methods are **hand-written** and marked `HAND-WRITTEN` in the source
(`hub/triggers.py`, its registration in `hub/__init__.py`, and
`account.check_project_languages`). Keep them when you regenerate.

## Development

```bash
uv sync
uv run ruff check .
uv run mypy src
uv run pytest
```

## Versioning

The major version is frozen at **v3** until the public launch.

- A breaking change is released as a **minor** (for example v3.2.0 → v3.3.0), never as a new major.
- Write it as `feat(<scope>): <what>` and add a line `Breaking: <what changed and what callers must do>` in plain words, in the pull-request body and in the commit message.
- Never mark it the conventional-commits way: no `!` in the title (`feat!:`), no BREAKING CHANGE footer. The `PR title` check fails a pull request that does.
- As a safety net, the release config (`pyproject.toml` → `commit_parser`, see `scripts/release_parser.py`) maps breaking commits to a minor, so one that slips through still does not bump the major.

## Releases

Pushes to `main`, `next`, and `beta` run
[python-semantic-release](https://python-semantic-release.readthedocs.io/)
and publish to PyPI.

## License

MIT
