Metadata-Version: 2.5
Name: 3cx-xapi-python-sdk
Version: 3.0.0
Summary: Python SDK for the 3CX XAPI (OData v4)
Author: 78wesley
License: MIT
Keywords: 3cx,pbx,sdk,voip,xapi
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: pyyaml>=6; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Requires-Dist: types-pyyaml; extra == 'dev'
Description-Content-Type: text/markdown

# 3CX Python SDK

A typed Python SDK for the **3CX XAPI** (`/xapi/v1`), built on [httpx](https://www.python-httpx.org/) and [Pydantic v2](https://docs.pydantic.dev/latest/).

## Features

- **OAuth2 client-credentials** authentication with automatic token refresh
- **OData v4** query builder (`$filter`, `$select`, `$expand`, `$orderby`, `$top`, `$skip`, `$count`)
- Automatic **pagination** — iterate over all pages with a single generator call
- **Full coverage** — 37 typed services covering every resource group in the 3CX XAPI swagger spec
- **Auto-generated models** — every Pydantic model is generated from `swagger.yaml`, giving you full IDE autocomplete on every field of every entity (User has 60+ typed fields, Queue has 50+, etc.)
- Reporting helpers for call log, queue performance, extension statistics, activity log, and more
- **Context-manager** support for clean resource management
- Escape hatches (`client.get()` / `client.post()`) for raw access to any endpoint

## Installation

```bash
pip install 3cx-xapi-python-sdk
```

## Quick start

```python
from threecx import ThreeCXClient, ODataQuery

client = ThreeCXClient(
    base_url="https://pbx.example.com",
    client_id="<service-principal-client-id>",
    client_secret="<service-principal-secret>",
)

# ── Active calls ─────────────────────────────────────────────────────────────
for call in client.active_calls.list():
    print(f"{call.caller} → {call.callee}  ({call.status} since {call.established_at})")

# Drop a specific call
client.active_calls.drop(call_id=42)

# ── Users ─────────────────────────────────────────────────────────────────────
# List first 50 enabled users, sorted by extension number
q = ODataQuery().filter("Enabled eq true").order_by("Number").top(50)
for user in client.users.list(q):
    print(user.number, user.full_name, user.email_address)

# Get a single user and update their e-mail
user = client.users.get(101)
client.users.update(101, {"EmailAddress": "new@example.com"})

# Create a user
from threecx.models import User
new_user = client.users.create(User(
    Number="200",
    FirstName="Alice",
    LastName="Smith",
    EmailAddress="alice@example.com",
))

# ── Queues ────────────────────────────────────────────────────────────────────
for queue in client.queues.list():
    agents = client.queues.get_agents(queue.id)
    print(f"Queue {queue.name}: {len(agents)} agents")

# ── Call history ──────────────────────────────────────────────────────────────
q = ODataQuery().filter("SrcExternal eq true").order_by("SegmentStartTime desc").top(100)
for entry in client.call_history.iterate(q):
    print(entry.segment_start_time, entry.src_caller_number, "→", entry.dst_caller_number, entry.call_answered)

# ── Reports ───────────────────────────────────────────────────────────────────
from datetime import datetime, timedelta, timezone

end = datetime.now(timezone.utc)
start = end - timedelta(days=7)
rows = client.reports.get_call_log(start, end)
print(f"{len(rows)} call-log entries for the last 7 days")

# ── System status ─────────────────────────────────────────────────────────────
status = client.system.get_status()
print(f"PBX {status.version}  calls: {status.calls_active}  disk: {status.disk_usage}%")

# ── Context manager ───────────────────────────────────────────────────────────
with ThreeCXClient(base_url="https://pbx.example.com",
                   client_id="...", client_secret="...") as c:
    print(c.system.get_license())
```

## OData query builder

```python
from threecx import ODataQuery

q = (
    ODataQuery()
    .filter("LastName eq 'Smith'")
    .select("Id", "Number", "FirstName", "LastName", "Email")
    .expand("Groups")
    .order_by("Number")
    .top(25)
    .skip(0)
    .count()
)
```

All query parameters map directly to OData v4 syntax. Refer to the [3CX XAPI swagger](swagger.yaml) for supported fields per endpoint.

Every method whose endpoint documents query options takes `query=`, including single-entity and settings getters
(`$select`/`$expand`), e.g. `client.settings.get_mail_settings(query=ODataQuery().select("MailServer"))`. `list()` methods
return the items of one page; read `$count` with `client.call_history.count(q)`, or through the escape hatch:
`client.get("/Users", params=ODataQuery().count().top(0).to_params())["@odata.count"]`.

## Calling actions

3CX actions (`POST …/Pbx.Name`) take named parameters, and the SDK methods take the same parameters as arguments. Pass
the object itself; the method posts it under the parameter name 3CX expects (`InstallUpdates` wants
`{"install": {...}}`, `SetUpdateSettings` wants `{"settings": {...}}`, and so on).

```python
from threecx.models import InstallUpdates, XServiceManageOptions

# Install every available update
available = client.updates.get_updates()
client.updates.install_updates(InstallUpdates(
    key=available["Key"],
    entries=[item["Guid"] for item in available["Entries"]],
))

# Restart a PBX service
client.pbx_services.restart(XServiceManageOptions(service_names=["3CXQueueManager"]))

# A dict with the API's PascalCase keys works wherever a model does
client.pbx_services.stop({"ServiceNames": ["3CXQueueManager"]})
```

Object arguments accept the model from `threecx.models` or a plain dict; enum arguments accept the enum or its string
value. Actions without parameters take no body argument: `client.recordings.archive()`.

## Creating a service principal

1. Log in to 3CX as admin.
2. Navigate to **Admin → Integrations → API**.
3. Create a new service principal and copy the **Client ID** and **Client Secret**.

## Services reference

The SDK exposes **37 services** as attributes of `ThreeCXClient`, covering every resource group in `swagger.yaml`.

### Calls & telephony
| `client.<service>` | Resource |
|---|---|
| `active_calls` | `/ActiveCalls` — list & drop live calls |
| `call_history` | `/CallHistoryView` — paginated call log |
| `recordings` | `/Recordings`, `/RemoteArchivingSettings` |
| `voicemail` | `/VoicemailSettings`, `/MusicOnHoldSettings` |
| `fax` | `/Fax`, `/FaxServerSettings` |

### Users, groups & directories
| `client.<service>` | Resource |
|---|---|
| `users` | `/Users` — CRUD extensions/users + greetings, phones, provisioning |
| `my_user` | `/MyUser` — current authenticated user |
| `groups` | `/Groups` — full admin groups (members, rights, restrictions) |
| `my_group` | `/MyGroup` — current user's primary group |
| `contacts` | `/Contacts` — company phone book |
| `parameters` | `/TenantProperties`, `/DNProperties`, directory info |

### Call routing & flow
| `client.<service>` | Resource |
|---|---|
| `queues` | `/Queues` — CRUD call queues & agents |
| `ring_groups` | `/RingGroups` — CRUD ring groups |
| `inbound_rules` | `/InboundRules` — DID/inbound rules |
| `outbound_rules` | `/OutboundRules` — outbound dial rules |
| `receptionists` | `/Receptionists` — digital receptionists |
| `holidays` | `/Holidays`, `/OfficeHours` |
| `parkings` | `/Parkings`, `/CallParkingSettings` |
| `call_flow` | `/CallFlowApps`, `/CallFlowScripts` |

### Trunks & PBX hardware
| `client.<service>` | Resource |
|---|---|
| `trunks` | `/Trunks`, `/TrunkTemplates`, `/Peers`, `/Sbcs` |
| `phones` | `/PhoneTemplates`, `/SipDevices`, `/Fxs`, `/FxsTemplates`, `/DeviceInfos`, `/Firmwares` |

### Communication
| `client.<service>` | Resource |
|---|---|
| `chat` | `/ChatHistoryView`, `/ChatMessagesHistoryView` |
| `prompts` | `/PromptSets`, `/CustomPrompts`, `/Playlists` |
| `email` | `/EmailTemplate` |

### System administration
| `client.<service>` | Resource |
|---|---|
| `system` | `/SystemStatus`, `/LicenseStatus`, `/Parameters` |
| `settings` | `/GeneralSettingsForPbx`, `/MailSettings`, `/CDRSettings`, ~20 settings domains |
| `pbx_services` | `/Services` — start/stop/enable/disable PBX services |
| `backups` | `/Backups`, failover & restore settings |
| `updates` | `/GetUpdates()`, `/InstallUpdates`, update settings, prompt set / CRM updates, purge |
| `event_logs` | `/EventLogs` |
| `security` | `/SecurityTokens`, `/ServicePrincipals`, `/BlackListNumbers`, `/Blocklist`, `/AntiHackingSettings`, `/Firewall` |
| `emergency` | `/EmergencyGeoLocations`, `/EmergencyNotificationsSettings` |
| `defs` | `/Defs` — codecs, timezones, gateway parameters, countries, DID numbers |

### Integrations
| `client.<service>` | Resource |
|---|---|
| `integrations` | `/Microsoft365Integration`, `/Microsoft365TeamsIntegration`, `/GoogleSettings`, `/AmazonIntegrationSettings`, `/DataConnectorSettings`, `/AISettings` |
| `crm` | `/CrmIntegration`, `/CrmTemplates` |
| `website_links` | `/WebsiteLinks` |

### Reporting
| `client.<service>` | Resource |
|---|---|
| `reports` | 30+ report endpoints: call log, queue & extension statistics, agent login, activity log, SLA breaches, audit, scheduled reports |

## Escape hatch

Any endpoint not covered by a typed service can be called directly:

```python
data = client.get("/InboundRules", params={"$top": "10"})
client.post("/Backups/Pbx.Backup", json={"description": {"Name": "before-upgrade"}})
```

`client.post()` sends `json` exactly as given, so key an action's body by its parameter names yourself (look them up
in `swagger.yaml` under the action's `…RequestBody` schema).
