Metadata-Version: 2.5
Name: x3ui
Version: 2.0.0
Summary: Typed Python client for the 3x-ui panel API, generated from OpenAPI
Project-URL: Homepage, https://github.com/vahellame/x3ui
Project-URL: Repository, https://github.com/vahellame/x3ui
Project-URL: Issues, https://github.com/vahellame/x3ui/issues
Project-URL: Changelog, https://github.com/vahellame/x3ui/blob/main/CHANGELOG.md
Author-email: Vadim Reznichenko <vahellame@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: 3x-ui,api-client,openapi,vpn,xray,xui
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.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: attrs>=22.2.0
Requires-Dist: httpx<0.29.0,>=0.23.0
Requires-Dist: python-dateutil>=2.8.0
Requires-Dist: typing-extensions>=4.0.0
Description-Content-Type: text/markdown

# x3ui
[![PyPI](https://img.shields.io/pypi/v/x3ui)](https://pypi.org/project/x3ui/)
[![Python](https://img.shields.io/pypi/pyversions/x3ui)](https://pypi.org/project/x3ui/)
[![CI](https://github.com/vahellame/x3ui/actions/workflows/ci.yml/badge.svg)](https://github.com/vahellame/x3ui/actions/workflows/ci.yml)
[![License](https://img.shields.io/pypi/l/x3ui)](https://github.com/vahellame/x3ui/blob/main/LICENSE)

Typed Python client for the 3x-ui panel API, generated from OpenAPI.

Unofficial project. Not affiliated with the [3x-ui](https://github.com/MHSanaei/3x-ui) developers.

## Installation

```
pip install x3ui
```

Requires Python 3.10 or newer.

## Quick start

```python
from x3ui import Panel

with Panel("https://panel.example.com:2053") as panel:
    panel.login("admin", "admin")

    for inbound in panel.inbounds.list():
        print(inbound.id, inbound.remark, inbound.protocol, inbound.port)

    print(panel.clients.online())
```

Base URL must include the port and the panel's base path if one is configured, for example `https://panel.example.com:2053/mypath`.

## Two layers

`Panel` is the high-level interface. It authenticates, unwraps the panel's `{success, msg, obj}` envelope, and raises `X3uiError` when the panel reports a failure — so methods return the payload directly.

Everything the facade does not wrap is reachable through the generated layer, which covers all 186 operations. Pass `panel.raw` as the client:

```python
from x3ui._generated.api.hosts import get_panel_api_hosts_list

response = get_panel_api_hosts_list.sync(client=panel.raw)
print(response.success, response.obj)
```

The generated layer is regenerated from the specification, so its names track the panel's routes and can change between releases. The facade is hand-written and stable.

## Authentication

### Username and password

```python
panel = Panel("https://panel.example.com:2053")
panel.login("admin", "admin")
```

`login` fetches a CSRF token before sending the credentials. The panel rejects unauthenticated POST requests — including the login request itself — with an empty `403` when the `X-CSRF-Token` header is absent, so the order matters. The session cookie is kept afterwards; keep using the same `Panel` object.

With two-factor authentication enabled, pass the current OTP code. It rotates every 30 seconds, so generate it immediately before logging in:

```python
panel.login("admin", "admin", two_factor_code="123456")
```

### API token

```python
panel = Panel("https://panel.example.com:2053", token="YOUR_API_TOKEN")
```

Create a token in the panel under Settings → Security → API Token. Requests are sent with an `Authorization: Bearer <token>` header, and CSRF does not apply. The plaintext token is shown once at creation; the panel stores only a hash.

An API token is a full-admin credential — treat it like the panel password.

## Common operations

### Inbounds

```python
panel.inbounds.list()
panel.inbounds.get(1)
panel.inbounds.set_enable(1, False)
panel.inbounds.reset_traffic(1)
```

`list()` returns typed `Inbound` objects with `client_stats` attached.

### Clients

```python
panel.clients.list()
panel.clients.get("alice@example.com")
panel.clients.traffic("alice@example.com")
panel.clients.links("alice@example.com")
panel.clients.sub_links("abcd1234")
panel.clients.online()
panel.clients.ips("alice@example.com")
```

The email is the client identifier in 3x-ui, not necessarily a real address.

Creating a client and attaching it to inbounds in one call:

```python
panel.clients.add(
    "alice@example.com",
    inbound_ids=[3, 5],
    total_gb=53687091200,
    expiry_time=1735689600000,
    limit_ip=2,
)
```

Per-protocol secrets (UUID, password, keys) are generated by the panel when omitted. Byte counts are bytes; timestamps are Unix milliseconds, where `0` means unlimited.

Changing and removing:

```python
panel.clients.update("alice@example.com", total_gb=107374182400, enable=True)
panel.clients.reset_traffic("alice@example.com")
panel.clients.attach("alice@example.com", [7, 9])
panel.clients.detach("alice@example.com", [5])
panel.clients.delete("alice@example.com", keep_traffic=True)
```

`update` replaces the fields you pass; anything omitted is left as stored.

### Server

```python
panel.server.status()
panel.server.new_uuid()
panel.server.restart_xray()
```

## Error handling

```python
from x3ui import NotAuthenticated, X3uiError

try:
    panel.clients.get("nobody")
except NotAuthenticated:
    panel.login("admin", "admin")
except X3uiError as error:
    print(error.operation, error.message)
```

`X3uiError` is raised when the panel answers with `success: false`; `NotAuthenticated` is the subclass raised when the message points at an expired or missing session. Network timeouts surface as `httpx.TimeoutException`.

## Configuration

```python
panel = Panel(
    "https://panel.example.com:2053",
    token="YOUR_API_TOKEN",
    timeout=60.0,
    verify_ssl=False,
)
```

`verify_ssl=False` disables certificate verification — only for panels with self-signed certificates, never in production. Extra keyword arguments are passed through to the underlying `httpx` client.

For direct access to the HTTP session:

```python
panel.raw.get_httpx_client()
```

## Async

The facade is synchronous. Every generated operation also has `asyncio` and `asyncio_detailed` variants — import them under an alias so they do not shadow the standard library module:

```python
import asyncio as aio

from x3ui import Panel
from x3ui._generated.api.inbounds import get_panel_api_inbounds_list

async def main():
    panel = Panel("https://panel.example.com:2053", token="YOUR_API_TOKEN")
    result = await get_panel_api_inbounds_list.asyncio(client=panel.raw)
    print(result.obj)

aio.run(main())
```

An async facade is not implemented yet.

## Endpoint groups

Generated modules live under `x3ui._generated.api.<group>`, mirroring the tags in the specification: `authentication`, `clients`, `inbounds`, `server`, `settings`, `xray_settings`, `nodes`, `hosts`, `backup`, `api_tokens`, `subscription_server`, `subscription_balancers`, `web_socket`.

Module names follow `<method>_<path>`, so `POST /panel/api/clients/add` becomes `x3ui._generated.api.clients.post_panel_api_clients_add`.

## Typing

The panel returns every payload inside `obj`, and its specification leaves most of those undescribed. Before generation, `tools/infer_obj_schemas.py` reads the response and request examples in the document and writes back the schemas it can infer, typing 61 responses and 43 request bodies that would otherwise be `Any`.

Inferred object schemas are hoisted into `components/schemas` under names derived from the route, so the generated models read as `ServerStatus`, `ClientsListItem` and `ClientsAddRequest` rather than `GetPanelApiServerStatusResponse200Obj`:

```python
status = panel.server.status()
print(status.cpu, status.xray.state)
```

The remaining 101 responses carry no example, stay `Any`, and come back as plain dicts and lists.

## Regenerating

```
./regenerate.sh
```

Or pull a fresh document from a live panel first:

```
PANEL_URL=https://panel.example.com:2053/basepath PANEL_TOKEN=... ./regenerate.sh --fetch
```

The script resets the `servers` entry before writing the file. A specification fetched from a live panel contains that panel's base path, which is a security-relevant secret — never commit it.

Regeneration only replaces `x3ui/_generated`. The facade in `x3ui/__init__.py` and `x3ui/panel.py` is hand-written and survives.

Generated against 3x-ui version 3.x. Endpoints may differ on other panel versions.

## Contributing

The most valuable contribution is describing more of the specification: the panel documents `obj` for only a fraction of its endpoints, and every schema added there turns a dict into a typed model for everyone. Adding facade coverage for endpoints that currently need the generated layer is equally welcome.

## License

MIT
