feat: Add models for subscriptions, users, and system statistics

- Implemented TemplateResponseDto and UpdateTemplateRequestDto for subscription templates.
- Created models for system statistics including CPU, memory, and bandwidth.
- Developed user models for user creation, updates, and responses.
- Added bulk actions for user updates and statistics tracking.
- Introduced tests for authentication, bandwidth statistics, hosts, inbounds, key generation, nodes, subscriptions, and user management.
- Enhanced utility functions for generating random strings, emails, and date ranges for testing.
This commit is contained in:
Artem 2025-04-21 20:44:27 +02:00
commit f47cda9378
No known key found for this signature in database
GPG key ID: 833485276B7902CE
80 changed files with 3994 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

175
.gitignore vendored Normal file
View file

@ -0,0 +1,175 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.py~
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 sm1ky
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

77
README.md Normal file
View file

@ -0,0 +1,77 @@
# Remnawave SDK
A Python SDK client for interacting with the [Remnawave API](https://remna.st).
This library simplifies working with the API by providing convenient controllers, Pydantic models for requests and responses, and fast serialization with `orjson`.
## ✨ Key Features
- **Controller-based design**: Split functionality into separate controllers for flexibility. Use only what you need!
- **Pydantic models**: Strongly-typed requests and responses for better reliability.
- **Fast serialization**: Powered by `orjson` for efficient JSON handling.
- **Modular usage**: Import individual controllers or the full SDK as needed.
## 📦 Installation
Currently, the SDK is available via Git. You can install it directly using `pip`:
```bash
pip install git+https://github.com/sm1ky/remnawave_api.git
```
---
### Dependencies
- `orjson` (>=3.10.15, <4.0.0)
- `rapid-api-client` (==0.6.0)
## 🚀 Usage
Heres a quick example to get you started:
```python
import os
import asyncio
from remnawave import RemnawaveSDK
from remnawave.models import UsersResponseDto, UserResponseDto
async def main():
# URL to your panel (ex. https://vpn.com or http://127.0.0.1:3000)
base_url: str = os.getenv("REMNAWAVE_BASE_URL")
# Bearer Token from panel (section: API Tokens)
token: str = os.getenv("REMNAWAVE_TOKEN")
# Initialize the SDK
remnawave = RemnawaveSDK(base_url=base_url, token=token)
# Fetch all users
response: UsersResponseDto = await remnawave.users.get_all_users_v2()
total_users: int = response.total
users: list[UserResponseDto] = response.users
print("Total users: ", total_users)
print("List of users: ", users)
# Disable a specific user
test_uuid: str = "e4d3f3d2-4f4f-4f4f-4f4f-4f4f4f4f4f4f"
disabled_user: UserResponseDto = await remnawave.users.disable_user(test_uuid)
print("Disabled user: ", disabled_user)
if __name__ == "__main__":
asyncio.run(main())
```
---
## 🧪 Running Tests
To run the test suite, use Poetry:
```bash
poetry run pytest
```
## ❤️ About
This SDK was originally developed by [@kesevone](https://github.com/kesevone) for integration with Remnawave's API.
Maintained and extended by [@sm1ky](https://github.com/sm1ky).

View file

@ -0,0 +1,54 @@
name: Publish Python Package
on:
push:
branches:
- production
permissions:
contents: write
id-token: write
packages: write
jobs:
publish:
runs-on: ubuntu-latest
needs: bump-version
steps:
- name: Checkout repository
if: ${{ !contains(github.event.head_commit.message, '[skip publish]') }}
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Pull latest changes
if: ${{ !contains(github.event.head_commit.message, '[skip publish]') }}
run: git pull origin production --tags --force
- name: Set up Python
if: ${{ !contains(github.event.head_commit.message, '[skip publish]') }}
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
if: ${{ !contains(github.event.head_commit.message, '[skip publish]') }}
run: |
python -m pip install --upgrade pip
pip install build wheel
- name: Build package
if: ${{ !contains(github.event.head_commit.message, '[skip publish]') }}
run: python -m build
- name: Inspect METADATA (for debug)
run: |
unzip dist/*.whl -d temp/
cat temp/*.dist-info/METADATA
- name: Publish package
if: ${{ !contains(github.event.head_commit.message, '[skip publish]') }}
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}

657
poetry.lock generated Normal file
View file

@ -0,0 +1,657 @@
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
[[package]]
name = "annotated-types"
version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "anyio"
version = "4.9.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.9"
groups = ["main", "test"]
files = [
{file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
{file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
]
[package.dependencies]
idna = ">=2.8"
sniffio = ">=1.1"
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras]
doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
trio = ["trio (>=0.26.1)"]
[[package]]
name = "black"
version = "24.10.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
{file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
{file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"},
{file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"},
{file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"},
{file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"},
{file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"},
{file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"},
{file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"},
{file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"},
{file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"},
{file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"},
{file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"},
{file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"},
{file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"},
{file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"},
{file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"},
{file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"},
{file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"},
{file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"},
{file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"},
{file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.10)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "certifi"
version = "2025.1.31"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
groups = ["main", "test"]
files = [
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
]
[[package]]
name = "click"
version = "8.1.8"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
groups = ["test"]
files = [
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["test"]
markers = "platform_system == \"Windows\" or sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
groups = ["main", "test"]
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
[[package]]
name = "httpcore"
version = "1.0.7"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main", "test"]
files = [
{file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"},
{file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"},
]
[package.dependencies]
certifi = "*"
h11 = ">=0.13,<0.15"
[package.extras]
asyncio = ["anyio (>=4.0,<5.0)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
trio = ["trio (>=0.22.0,<1.0)"]
[[package]]
name = "httpx"
version = "0.27.2"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
groups = ["main", "test"]
files = [
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
]
[package.dependencies]
anyio = "*"
certifi = "*"
httpcore = "==1.*"
idna = "*"
sniffio = "*"
[package.extras]
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "idna"
version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
groups = ["main", "test"]
files = [
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
]
[package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "iniconfig"
version = "2.1.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
groups = ["test"]
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "orjson"
version = "3.10.15"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "orjson-3.10.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:552c883d03ad185f720d0c09583ebde257e41b9521b74ff40e08b7dec4559c04"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e3e8d438d02e4854f70bfdc03a6bcdb697358dbaa6bcd19cbe24d24ece1f8"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c2c79fa308e6edb0ffab0a31fd75a7841bf2a79a20ef08a3c6e3b26814c8ca8"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cb85490aa6bf98abd20607ab5c8324c0acb48d6da7863a51be48505646c814"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763dadac05e4e9d2bc14938a45a2d0560549561287d41c465d3c58aec818b164"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a330b9b4734f09a623f74a7490db713695e13b67c959713b78369f26b3dee6bf"},
{file = "orjson-3.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a61a4622b7ff861f019974f73d8165be1bd9a0855e1cad18ee167acacabeb061"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd271247691574416b3228db667b84775c497b245fa275c6ab90dc1ffbbd2b3"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4759b109c37f635aa5c5cc93a1b26927bfde24b254bcc0e1149a9fada253d2d"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e992fd5cfb8b9f00bfad2fd7a05a4299db2bbe92e6440d9dd2fab27655b3182"},
{file = "orjson-3.10.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f95fb363d79366af56c3f26b71df40b9a583b07bbaaf5b317407c4d58497852e"},
{file = "orjson-3.10.15-cp310-cp310-win32.whl", hash = "sha256:f9875f5fea7492da8ec2444839dcc439b0ef298978f311103d0b7dfd775898ab"},
{file = "orjson-3.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:17085a6aa91e1cd70ca8533989a18b5433e15d29c574582f76f821737c8d5806"},
{file = "orjson-3.10.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c4cc83960ab79a4031f3119cc4b1a1c627a3dc09df125b27c4201dff2af7eaa6"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddbeef2481d895ab8be5185f2432c334d6dec1f5d1933a9c83014d188e102cef"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e590a0477b23ecd5b0ac865b1b907b01b3c5535f5e8a8f6ab0e503efb896334"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6be38bd103d2fd9bdfa31c2720b23b5d47c6796bcb1d1b598e3924441b4298d"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ff4f6edb1578960ed628a3b998fa54d78d9bb3e2eb2cfc5c2a09732431c678d0"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0482b21d0462eddd67e7fce10b89e0b6ac56570424662b685a0d6fccf581e13"},
{file = "orjson-3.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb5cc3527036ae3d98b65e37b7986a918955f85332c1ee07f9d3f82f3a6899b5"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d569c1c462912acdd119ccbf719cf7102ea2c67dd03b99edcb1a3048651ac96b"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1e6d33efab6b71d67f22bf2962895d3dc6f82a6273a965fab762e64fa90dc399"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c33be3795e299f565681d69852ac8c1bc5c84863c0b0030b2b3468843be90388"},
{file = "orjson-3.10.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eea80037b9fae5339b214f59308ef0589fc06dc870578b7cce6d71eb2096764c"},
{file = "orjson-3.10.15-cp311-cp311-win32.whl", hash = "sha256:d5ac11b659fd798228a7adba3e37c010e0152b78b1982897020a8e019a94882e"},
{file = "orjson-3.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:cf45e0214c593660339ef63e875f32ddd5aa3b4adc15e662cdb80dc49e194f8e"},
{file = "orjson-3.10.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9d11c0714fc85bfcf36ada1179400862da3288fc785c30e8297844c867d7505a"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dba5a1e85d554e3897fa9fe6fbcff2ed32d55008973ec9a2b992bd9a65d2352d"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7723ad949a0ea502df656948ddd8b392780a5beaa4c3b5f97e525191b102fff0"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6fd9bc64421e9fe9bd88039e7ce8e58d4fead67ca88e3a4014b143cec7684fd4"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dadba0e7b6594216c214ef7894c4bd5f08d7c0135f4dd0145600be4fbcc16767"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48f59114fe318f33bbaee8ebeda696d8ccc94c9e90bc27dbe72153094e26f41"},
{file = "orjson-3.10.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:035fb83585e0f15e076759b6fedaf0abb460d1765b6a36f48018a52858443514"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d13b7fe322d75bf84464b075eafd8e7dd9eae05649aa2a5354cfa32f43c59f17"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7066b74f9f259849629e0d04db6609db4cf5b973248f455ba5d3bd58a4daaa5b"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88dc3f65a026bd3175eb157fea994fca6ac7c4c8579fc5a86fc2114ad05705b7"},
{file = "orjson-3.10.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b342567e5465bd99faa559507fe45e33fc76b9fb868a63f1642c6bc0735ad02a"},
{file = "orjson-3.10.15-cp312-cp312-win32.whl", hash = "sha256:0a4f27ea5617828e6b58922fdbec67b0aa4bb844e2d363b9244c47fa2180e665"},
{file = "orjson-3.10.15-cp312-cp312-win_amd64.whl", hash = "sha256:ef5b87e7aa9545ddadd2309efe6824bd3dd64ac101c15dae0f2f597911d46eaa"},
{file = "orjson-3.10.15-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bae0e6ec2b7ba6895198cd981b7cca95d1487d0147c8ed751e5632ad16f031a6"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f93ce145b2db1252dd86af37d4165b6faa83072b46e3995ecc95d4b2301b725a"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c203f6f969210128af3acae0ef9ea6aab9782939f45f6fe02d05958fe761ef9"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8918719572d662e18b8af66aef699d8c21072e54b6c82a3f8f6404c1f5ccd5e0"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f71eae9651465dff70aa80db92586ad5b92df46a9373ee55252109bb6b703307"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e117eb299a35f2634e25ed120c37c641398826c2f5a3d3cc39f5993b96171b9e"},
{file = "orjson-3.10.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:13242f12d295e83c2955756a574ddd6741c81e5b99f2bef8ed8d53e47a01e4b7"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7946922ada8f3e0b7b958cc3eb22cfcf6c0df83d1fe5521b4a100103e3fa84c8"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b7155eb1623347f0f22c38c9abdd738b287e39b9982e1da227503387b81b34ca"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:208beedfa807c922da4e81061dafa9c8489c6328934ca2a562efa707e049e561"},
{file = "orjson-3.10.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eca81f83b1b8c07449e1d6ff7074e82e3fd6777e588f1a6632127f286a968825"},
{file = "orjson-3.10.15-cp313-cp313-win32.whl", hash = "sha256:c03cd6eea1bd3b949d0d007c8d57049aa2b39bd49f58b4b2af571a5d3833d890"},
{file = "orjson-3.10.15-cp313-cp313-win_amd64.whl", hash = "sha256:fd56a26a04f6ba5fb2045b0acc487a63162a958ed837648c5781e1fe3316cfbf"},
{file = "orjson-3.10.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e8afd6200e12771467a1a44e5ad780614b86abb4b11862ec54861a82d677746"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9a18c500f19273e9e104cca8c1f0b40a6470bcccfc33afcc088045d0bf5ea6"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb00b7bfbdf5d34a13180e4805d76b4567025da19a197645ca746fc2fb536586"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33aedc3d903378e257047fee506f11e0833146ca3e57a1a1fb0ddb789876c1e1"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd0099ae6aed5eb1fc84c9eb72b95505a3df4267e6962eb93cdd5af03be71c98"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c864a80a2d467d7786274fce0e4f93ef2a7ca4ff31f7fc5634225aaa4e9e98c"},
{file = "orjson-3.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c25774c9e88a3e0013d7d1a6c8056926b607a61edd423b50eb5c88fd7f2823ae"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e78c211d0074e783d824ce7bb85bf459f93a233eb67a5b5003498232ddfb0e8a"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:43e17289ffdbbac8f39243916c893d2ae41a2ea1a9cbb060a56a4d75286351ae"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:781d54657063f361e89714293c095f506c533582ee40a426cb6489c48a637b81"},
{file = "orjson-3.10.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6875210307d36c94873f553786a808af2788e362bd0cf4c8e66d976791e7b528"},
{file = "orjson-3.10.15-cp38-cp38-win32.whl", hash = "sha256:305b38b2b8f8083cc3d618927d7f424349afce5975b316d33075ef0f73576b60"},
{file = "orjson-3.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:5dd9ef1639878cc3efffed349543cbf9372bdbd79f478615a1c633fe4e4180d1"},
{file = "orjson-3.10.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ffe19f3e8d68111e8644d4f4e267a069ca427926855582ff01fc012496d19969"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d433bf32a363823863a96561a555227c18a522a8217a6f9400f00ddc70139ae2"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da03392674f59a95d03fa5fb9fe3a160b0511ad84b7a3914699ea5a1b3a38da2"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a63bb41559b05360ded9132032239e47983a39b151af1201f07ec9370715c82"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3766ac4702f8f795ff3fa067968e806b4344af257011858cc3d6d8721588b53f"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1c73dcc8fadbd7c55802d9aa093b36878d34a3b3222c41052ce6b0fc65f8e8"},
{file = "orjson-3.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b299383825eafe642cbab34be762ccff9fd3408d72726a6b2a4506d410a71ab3"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:abc7abecdbf67a173ef1316036ebbf54ce400ef2300b4e26a7b843bd446c2480"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:3614ea508d522a621384c1d6639016a5a2e4f027f3e4a1c93a51867615d28829"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:295c70f9dc154307777ba30fe29ff15c1bcc9dfc5c48632f37d20a607e9ba85a"},
{file = "orjson-3.10.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:63309e3ff924c62404923c80b9e2048c1f74ba4b615e7584584389ada50ed428"},
{file = "orjson-3.10.15-cp39-cp39-win32.whl", hash = "sha256:a2f708c62d026fb5340788ba94a55c23df4e1869fec74be455e0b2f5363b8507"},
{file = "orjson-3.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:efcf6c735c3d22ef60c4aa27a5238f1a477df85e9b15f2142f9d669beb2d13fd"},
{file = "orjson-3.10.15.tar.gz", hash = "sha256:05ca7fe452a2e9d8d9d706a2984c95b9c2ebc5db417ce0b7a49b91d50642a23e"},
]
[[package]]
name = "packaging"
version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "platformdirs"
version = "4.3.7"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"},
{file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"},
]
[package.extras]
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"]
type = ["mypy (>=1.14.1)"]
[[package]]
name = "pluggy"
version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "pydantic"
version = "2.10.6"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"},
{file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
pydantic-core = "2.27.2"
typing-extensions = ">=4.12.2"
[package.extras]
email = ["email-validator (>=2.0.0)"]
timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
[[package]]
name = "pydantic-core"
version = "2.27.2"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
{file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
{file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
{file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
{file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
{file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
{file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
{file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
{file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
{file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
{file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
]
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pytest"
version = "8.3.5"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.5,<2"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-asyncio"
version = "0.25.3"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"},
{file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"},
]
[package.dependencies]
pytest = ">=8.2,<9"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
name = "pytest-mock"
version = "3.14.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
{file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
]
[package.dependencies]
pytest = ">=6.2.5"
[package.extras]
dev = ["pre-commit", "pytest-asyncio", "tox"]
[[package]]
name = "python-dotenv"
version = "1.0.1"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"},
{file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "pytz"
version = "2025.1"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
groups = ["test"]
files = [
{file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"},
{file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"},
]
[[package]]
name = "rapid-api-client"
version = "0.6.0"
description = "Rapidly develop your API clients using decorators and annotations"
optional = false
python-versions = "<4.0,>=3.11"
groups = ["main"]
files = [
{file = "rapid_api_client-0.6.0-py3-none-any.whl", hash = "sha256:f8bbeb1432e48b6456a2e83802d31162b406bc63ef992d2cb9fc720b0eebc3ce"},
{file = "rapid_api_client-0.6.0.tar.gz", hash = "sha256:68d7bddb91306e33d0b9f12355f17245f6c820a9e7cc4934b8fafe68c20aebf0"},
]
[package.dependencies]
httpx = ">=0.27.2,<0.28.0"
pydantic = ">=2.9.2,<3.0.0"
[[package]]
name = "ruff"
version = "0.4.10"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["test"]
files = [
{file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"},
{file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"},
{file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"},
{file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"},
{file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"},
{file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"},
]
[[package]]
name = "sniffio"
version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
groups = ["main", "test"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
groups = ["main", "test"]
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
markers = {test = "python_version < \"3.13\""}
[metadata]
lock-version = "2.1"
python-versions = ">=3.11,<4.0"
content-hash = "bafddd23889fa67157f6041f778f5c6b8288af1150cfb280ebaa2dde6244fd00"

34
pyproject.toml Normal file
View file

@ -0,0 +1,34 @@
[project]
name = "remnawave_api"
version = "1.0.0"
description = "A Python SDK for interacting with the Remnawave API."
authors = [
{name = "sm1ky",email = "sm1ky@forestsnet.com"}
]
license = { text = "MIT" }
readme = "README.md"
requires-python = ">=3.11,<4.0"
dependencies = [
"rapid-api-client (==0.6.0)",
"orjson (>=3.10.15,<4.0.0)",
]
[tool.poetry.group.test.dependencies]
ruff = "^0.4.4"
black = "^24.4.2"
pytest = ">=8.0"
pytest-asyncio = "^0.25.3"
pytest-mock = ">=3.9"
httpx = ">=0.27.2,<0.28.0"
python-dotenv = "^1.0.1"
pytz = "^2025.1"
[tool.pytest.ini_options]
addopts = [ "-n", "logical", "-ra", "--strict-config", "--strict-markers" ]
testpaths = ["tests"]
log_cli_level = "INFO"
xfail_strict = true
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

2
pytest.ini Normal file
View file

@ -0,0 +1,2 @@
[pytest]
asyncio_mode = auto

BIN
remnawave_api/.DS_Store vendored Normal file

Binary file not shown.

90
remnawave_api/__init__.py Normal file
View file

@ -0,0 +1,90 @@
import logging
from typing import Optional
import httpx
from remnawave_api.controllers import (
APITokensManagementController,
AuthController,
BandWidthStatsController,
HostsBulkActionsController,
HostsController,
InboundsBulkActionsController,
InboundsController,
KeygenController,
NodesController,
SubscriptionController,
SubscriptionsSettingsController,
SubscriptionsTemplateController,
SystemController,
UsersBulkActionsController,
UsersController,
UsersStatsController,
XrayConfigController,
)
class RemnawaveSDK:
def __init__(
self,
client: Optional[httpx.AsyncClient] = None,
base_url: Optional[str] = None,
token: Optional[str] = None,
):
self._client = client
self._token = token
self.base_url = base_url
self._validate_params()
if self._client is None:
self._client = self._prepare_client()
self.api_tokens_management = APITokensManagementController(self._client)
self.auth = AuthController(self._client)
self.bandwidthstats = BandWidthStatsController(self._client)
self.hosts = HostsController(self._client)
self.hosts_bulk_actions = HostsBulkActionsController(self._client)
self.inbounds = InboundsController(self._client)
self.inbounds_bulk_actions = InboundsBulkActionsController(self._client)
self.keygen = KeygenController(self._client)
self.nodes = NodesController(self._client)
self.subscription = SubscriptionController(self._client)
self.subscriptions_settings = SubscriptionsSettingsController(self._client)
self.subscriptions_template = SubscriptionsTemplateController(self._client)
self.system = SystemController(self._client)
self.users = UsersController(self._client)
self.users_bulk_actions = UsersBulkActionsController(self._client)
self.users_stats = UsersStatsController(self._client)
self.xray_config = XrayConfigController(self._client)
def _validate_params(self) -> None:
if self._client is None:
if self.base_url is None or self._token is None:
raise ValueError(
"base_url and token must be provided if client is not provided"
)
else:
if self.base_url is not None or self._token is not None:
logging.warning(
"base_url and token will be ignored if client is provided"
)
def _prepare_client(self) -> httpx.AsyncClient:
return httpx.AsyncClient(
base_url=self._prepare_url(), headers=self._prepare_headers()
)
def _prepare_headers(self) -> dict:
if not self._token.startswith("Bearer "):
self._token = "Bearer " + self._token
return {"Authorization": self._token}
def _prepare_url(self) -> str:
if self.base_url.endswith("/"):
self.base_url[:-1]
if not self.base_url.endswith("/api"):
self.base_url += "/api"
return self.base_url

View file

@ -0,0 +1,37 @@
from .api_tokens_management import APITokensManagementController
from .auth import AuthController
from .bandwidthstats import BandWidthStatsController
from .hosts import HostsController
from .hosts_bulk_actions import HostsBulkActionsController
from .inbounds import InboundsController
from .inbounds_bulk_actions import InboundsBulkActionsController
from .keygen import KeygenController
from .nodes import NodesController
from .subscription import SubscriptionController
from .subscriptions_settings import SubscriptionsSettingsController
from .subscriptions_template import SubscriptionsTemplateController
from .system import SystemController
from .users import UsersController
from .users_bulk_actions import UsersBulkActionsController
from .users_stats import UsersStatsController
from .xray_config import XrayConfigController
__all__ = [
"APITokensManagementController",
"AuthController",
"BandWidthStatsController",
"HostsController",
"HostsBulkActionsController",
"InboundsController",
"InboundsBulkActionsController",
"KeygenController",
"NodesController",
"SubscriptionController",
"SubscriptionsSettingsController",
"SubscriptionsTemplateController",
"SystemController",
"UsersController",
"UsersBulkActionsController",
"UsersStatsController",
"XrayConfigController",
]

View file

@ -0,0 +1,33 @@
from typing import Annotated
from httpx import Response
from rapid_api_client import Path
from rapid_api_client.annotations import PydanticBody
from remnawave_api.models import CreateApiTokenRequestDto
from remnawave_api.rapid import BaseController, delete, get, post
class APITokensManagementController(BaseController):
@post("/tokens/create", response_class=Response)
async def create(
self,
body: Annotated[CreateApiTokenRequestDto, PydanticBody()],
) -> Response:
"""Create new API token"""
...
@delete("/tokens/delete/{uuid}", response_class=Response)
async def delete(
self,
uuid: Annotated[str, Path(description="UUID of the API token")],
) -> Response:
"""Delete API token"""
...
@get("/tokens", response_class=Response)
async def find_all(
self,
) -> Response:
"""Get all API tokens"""
...

View file

@ -0,0 +1,37 @@
from typing import Annotated
from rapid_api_client.annotations import PydanticBody
from remnawave_api.models import (
LoginRequestDto,
LoginResponseDto,
RegisterRequestDto,
RegisterResponseDto,
StatusResponseDto,
)
from remnawave_api.rapid import BaseController, get, post
class AuthController(BaseController):
@post("/auth/login", response_class=LoginResponseDto)
async def login(
self,
body: Annotated[LoginRequestDto, PydanticBody()],
) -> LoginResponseDto:
"""Login"""
...
@post("/auth/register", response_class=RegisterResponseDto)
async def register(
self,
body: Annotated[RegisterRequestDto, PydanticBody()],
) -> RegisterResponseDto:
"""Register"""
...
@get("/auth/status", response_class=StatusResponseDto)
async def get_status(
self,
) -> StatusResponseDto:
"""Get status"""
...

View file

@ -0,0 +1,17 @@
from typing import Annotated
from rapid_api_client import Query
from remnawave_api.models import NodesUsageResponseDto
from remnawave_api.rapid import BaseController, get
class BandWidthStatsController(BaseController):
@get("/nodes/usage/range", response_class=NodesUsageResponseDto)
async def get_nodes_usage_by_range(
self,
start: Annotated[str, Query(description="Start date in ISO format")],
end: Annotated[str, Query(description="End date in ISO format")],
) -> NodesUsageResponseDto:
"""Get Nodes Usage By Range"""
...

View file

@ -0,0 +1,64 @@
from typing import Annotated, List
from rapid_api_client import Path
from rapid_api_client.annotations import PydanticBody
from remnawave_api.models import (
CreateHostRequestDto,
DeleteHostResponseDto,
HostResponseDto,
HostsResponseDto,
ReorderHostRequestDto,
ReorderHostResponseDto,
UpdateHostRequestDto,
)
from remnawave_api.rapid import AttributeBody, BaseController, delete, get, post
class HostsController(BaseController):
@post("/hosts/create", response_class=HostResponseDto)
async def create_host(
self,
body: Annotated[CreateHostRequestDto, PydanticBody()],
) -> HostResponseDto:
"""Create Host"""
...
@post("/hosts/update", response_class=HostResponseDto)
async def update_host(
self,
body: Annotated[UpdateHostRequestDto, PydanticBody()],
) -> HostResponseDto:
"""Update Host"""
...
@get("/hosts/all", response_class=HostsResponseDto)
async def get_all_hosts(
self,
) -> HostsResponseDto:
"""Get All Hosts"""
...
@get("/hosts/get-one/{uuid}", response_class=HostResponseDto)
async def get_one_host(
self,
uuid: Annotated[str, Path(description="UUID of the host")],
) -> HostResponseDto:
"""Get One Host"""
...
@post("/hosts/reorder", response_class=ReorderHostResponseDto)
async def reorder_hosts(
self,
hosts: Annotated[List[ReorderHostRequestDto], AttributeBody()],
) -> ReorderHostResponseDto:
"""Reorder Hosts"""
...
@delete("/hosts/delete/{uuid}", response_class=DeleteHostResponseDto)
async def delete_host(
self,
uuid: Annotated[str, Path(description="UUID of the host")],
) -> DeleteHostResponseDto:
"""Delete Host"""
...

View file

@ -0,0 +1,60 @@
from typing import Annotated, List
from uuid import UUID
from rapid_api_client import PydanticBody
from remnawave_api.models import (
BulkDeleteHostsResponseDto,
BulkDisableHostsResponseDto,
BulkEnableHostsResponseDto,
SetInboundToManyHostsRequestDto,
SetInboundToManyHostsResponseDto,
SetPortToManyHostsResponseDto,
)
from remnawave_api.rapid import AttributeBody, BaseController, post
class HostsBulkActionsController(BaseController):
@post("/hosts/bulk/delete", response_class=BulkDeleteHostsResponseDto)
async def delete_hosts(
self,
uuids: Annotated[List[UUID], AttributeBody()],
) -> BulkDeleteHostsResponseDto:
"""Delete many hosts"""
...
@post("/hosts/bulk/disable", response_class=BulkDisableHostsResponseDto)
async def disable_hosts(
self,
uuids: Annotated[List[UUID], AttributeBody()],
) -> BulkDisableHostsResponseDto:
"""Disable many hosts"""
...
@post("/hosts/bulk/enable", response_class=BulkEnableHostsResponseDto)
async def enable_hosts(
self,
uuids: Annotated[List[UUID], AttributeBody()],
) -> BulkEnableHostsResponseDto:
"""Enable many hosts"""
...
@post(
"/hosts/bulk/set-inbound",
response_class=SetInboundToManyHostsResponseDto,
)
async def set_inbound_to_hosts(
self,
body: Annotated[SetInboundToManyHostsRequestDto, PydanticBody()],
) -> SetInboundToManyHostsResponseDto:
"""Set inbound to many hosts"""
...
@post("/hosts/bulk/set-port", response_class=SetPortToManyHostsResponseDto)
async def set_port_to_hosts(
self,
uuids: Annotated[List[UUID], AttributeBody()],
port: Annotated[float, AttributeBody()],
) -> SetPortToManyHostsResponseDto:
"""Set port to many hosts"""
...

View file

@ -0,0 +1,18 @@
from remnawave_api.models import FullInboundsResponseDto, InboundsResponseDto
from remnawave_api.rapid import BaseController, get
class InboundsController(BaseController):
@get("/inbounds", response_class=InboundsResponseDto)
async def get_inbounds(
self,
) -> InboundsResponseDto:
"""Get Inbounds"""
...
@get("/inbounds/full", response_class=FullInboundsResponseDto)
async def get_full_inbounds(
self,
) -> FullInboundsResponseDto:
"""Get Full Inbounds"""
...

View file

@ -0,0 +1,50 @@
from typing import Annotated
from uuid import UUID
from remnawave_api.models import (
AddInboundToNodesResponseDto,
AddInboundToUsersResponseDto,
RemoveInboundFromNodesResponseDto,
RemoveInboundFromUsersResponseDto,
)
from remnawave_api.rapid import AttributeBody, BaseController, post
class InboundsBulkActionsController(BaseController):
@post("/inbounds/bulk/add-to-users", response_class=AddInboundToUsersResponseDto)
async def add_inbound_to_users(
self,
inbound_uuid: Annotated[UUID, AttributeBody()],
) -> AddInboundToUsersResponseDto:
"""Add Inbound To Users"""
...
@post(
"/inbounds/bulk/remove-from-users",
response_class=RemoveInboundFromUsersResponseDto,
)
async def remove_inbound_from_users(
self,
inbound_uuid: Annotated[UUID, AttributeBody()],
) -> RemoveInboundFromUsersResponseDto:
"""Remove Inbound From Users"""
...
@post("/inbounds/bulk/add-to-nodes", response_class=AddInboundToNodesResponseDto)
async def add_inbound_to_nodes(
self,
inbound_uuid: Annotated[UUID, AttributeBody()],
) -> AddInboundToNodesResponseDto:
"""Add Inbound To All Nodes"""
...
@post(
"/inbounds/bulk/remove-from-nodes",
response_class=RemoveInboundFromNodesResponseDto,
)
async def remove_inbound_from_nodes(
self,
inbound_uuid: Annotated[UUID, AttributeBody()],
) -> RemoveInboundFromNodesResponseDto:
"""Remove Inbound From All Nodes"""
...

View file

@ -0,0 +1,11 @@
from remnawave_api.models import PubKeyResponseDto
from remnawave_api.rapid import BaseController, get
class KeygenController(BaseController):
@get("/keygen/get", response_class=PubKeyResponseDto)
async def generate_key(
self,
) -> PubKeyResponseDto:
"""Get Public Key"""
...

View file

@ -0,0 +1,95 @@
from typing import Annotated, List
from rapid_api_client import Path
from rapid_api_client.annotations import PydanticBody
from remnawave_api.models import (
CreateNodeRequestDto,
DeleteNodeResponseDto,
NodeResponseDto,
NodesResponseDto,
ReorderNodeRequestDto,
RestartNodeResponseDto,
UpdateNodeRequestDto,
)
from remnawave_api.rapid import AttributeBody, BaseController, delete, get, patch, post
class NodesController(BaseController):
@post("/nodes/create", response_class=NodeResponseDto)
async def create_node(
self,
body: Annotated[CreateNodeRequestDto, PydanticBody()],
) -> NodeResponseDto:
"""Create Node"""
...
@get("/nodes/get-all", response_class=NodesResponseDto)
async def get_all_nodes(
self,
) -> NodesResponseDto:
"""Get All Nodes"""
...
@get("/nodes/get-one/{uuid}", response_class=NodeResponseDto)
async def get_one_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
) -> NodeResponseDto:
"""Get One Node"""
...
@patch("/nodes/enable/{uuid}", response_class=NodeResponseDto)
async def enable_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
) -> NodeResponseDto:
"""Enable Node"""
...
@patch("/nodes/disable/{uuid}", response_class=NodeResponseDto)
async def disable_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
) -> NodeResponseDto:
"""Disable Node"""
...
@delete("/nodes/delete/{uuid}", response_class=DeleteNodeResponseDto)
async def delete_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
) -> DeleteNodeResponseDto:
"""Delete Node"""
...
@post("/nodes/update", response_class=NodeResponseDto)
async def update_node(
self,
body: Annotated[UpdateNodeRequestDto, PydanticBody()],
) -> NodeResponseDto:
"""Update Node"""
...
@get("/nodes/restart/{uuid}", response_class=RestartNodeResponseDto)
async def restart_node(
self,
uuid: Annotated[str, Path(description="Node UUID")],
) -> RestartNodeResponseDto:
"""Restart Node"""
...
@patch("/nodes/restart-all", response_class=RestartNodeResponseDto)
async def restart_all_nodes(
self,
) -> RestartNodeResponseDto:
"""Restart All Nodes"""
...
@post("/nodes/reorder", response_class=NodesResponseDto)
async def reorder_nodes(
self,
nodes: Annotated[List[ReorderNodeRequestDto], AttributeBody()],
) -> NodesResponseDto:
"""Reorder Nodes"""
...

View file

@ -0,0 +1,54 @@
from typing import Annotated
from rapid_api_client import Path
from remnawave_api.enums import ClientType
from remnawave_api.models import SubscriptionInfoResponseDto
from remnawave_api.rapid import BaseController, get
class SubscriptionController(BaseController):
@get("/sub/{short_uuid}/info", response_class=SubscriptionInfoResponseDto)
async def get_subscription_info_by_short_uuid(
self,
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
) -> SubscriptionInfoResponseDto:
"""None"""
...
@get("/sub/{short_uuid}", response_class=str)
async def get_subscription(
self,
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
) -> str:
"""None"""
...
@get("/sub/{short_uuid}/{client_type}", response_class=str)
async def get_subscription_by_client_type(
self,
client_type: Annotated[ClientType, Path(description="Client type")],
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
) -> str:
"""None"""
...
@get("/sub/outline/{short_uuid}/{type}/{encoded_tag}", response_class=str)
async def get_subscription_with_type(
self,
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
type: Annotated[
str,
Path(
description="Subscription type (required if encodedTag is provided). Only SS is supported for now."
),
] = "ss",
encoded_tag: Annotated[
str,
Path(
description="Base64 encoded tag for Outline config. This paramter is optional. It is required only when type=ss."
),
] = "VGVzdGVy",
) -> str:
"""None"""
...

View file

@ -0,0 +1,29 @@
from typing import Annotated
from rapid_api_client.annotations import PydanticBody
from remnawave_api.models import (
SubscriptionSettingsResponseDto,
UpdateSubscriptionSettingsRequestDto,
)
from remnawave_api.rapid import BaseController, get, post
class SubscriptionsSettingsController(BaseController):
@get("/subscription-settings/get", response_class=SubscriptionSettingsResponseDto)
async def get_settings(
self,
) -> SubscriptionSettingsResponseDto:
"""Get Subscription Settings"""
...
@post(
"/subscription-settings/update",
response_class=SubscriptionSettingsResponseDto,
)
async def update_settings(
self,
body: Annotated[UpdateSubscriptionSettingsRequestDto, PydanticBody()],
) -> SubscriptionSettingsResponseDto:
"""Update Subscription Settings"""
...

View file

@ -0,0 +1,31 @@
from typing import Annotated
from rapid_api_client.annotations import Path, PydanticBody
from remnawave_api.enums import TemplateType
from remnawave_api.models import TemplateResponseDto, UpdateTemplateRequestDto
from remnawave_api.rapid import BaseController, get, post
class SubscriptionsTemplateController(BaseController):
@get(
"/subscription-templates/get-template/{template_type}",
response_class=TemplateResponseDto,
)
async def get_template(
self,
template_type: Annotated[TemplateType, Path(description="Template type")],
) -> TemplateResponseDto:
"""Get Template"""
...
@post(
"/subscription-templates/update-template",
response_class=TemplateResponseDto,
)
async def update_template(
self,
body: Annotated[UpdateTemplateRequestDto, PydanticBody()],
) -> TemplateResponseDto:
"""Update Template"""
...

View file

@ -0,0 +1,29 @@
from remnawave_api.models import (
BandwidthStatisticResponseDto,
NodesStatisticResponseDto,
StatisticResponseDto,
)
from remnawave_api.rapid import BaseController, get
class SystemController(BaseController):
@get("/system/stats", response_class=StatisticResponseDto)
async def get_stats(
self,
) -> StatisticResponseDto:
"""Get System Stats"""
...
@get("/system/bandwidth", response_class=BandwidthStatisticResponseDto)
async def get_bandwidth_stats(
self,
) -> BandwidthStatisticResponseDto:
"""Get System Bandwidth Statistics"""
...
@get("/system/statistics/nodes", response_class=NodesStatisticResponseDto)
async def get_nodes_statistics(
self,
) -> NodesStatisticResponseDto:
"""Get Nodes Statistics"""
...

View file

@ -0,0 +1,131 @@
from typing import Annotated
from rapid_api_client import Path
from rapid_api_client.annotations import PydanticBody
from remnawave_api.models import (
CreateUserRequestDto,
DeleteUserResponseDto,
EmailUserResponseDto,
TelegramUserResponseDto,
UpdateUserRequestDto,
UserResponseDto,
UsersResponseDto,
)
from remnawave_api.rapid import BaseController, delete, get, patch, post
class UsersController(BaseController):
@post("/users", response_class=UserResponseDto)
async def create_user(
self,
body: Annotated[CreateUserRequestDto, PydanticBody()],
) -> UserResponseDto:
"""Create User"""
...
@post("/users/update", response_class=UserResponseDto)
async def update_user(
self,
body: Annotated[UpdateUserRequestDto, PydanticBody()],
) -> UserResponseDto:
"""Update User"""
...
@get("/users/v2", response_class=UsersResponseDto)
async def get_all_users_v2(
self,
) -> UsersResponseDto:
"""Get All Users"""
...
@patch("/users/revoke/{uuid}", response_class=UserResponseDto)
async def revoke_user_subscription(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Revoke User Subscription"""
...
@patch("/users/disable/{uuid}", response_class=UserResponseDto)
async def disable_user(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Disable User"""
...
@delete("/users/delete/{uuid}", response_class=DeleteUserResponseDto)
async def delete_user(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> DeleteUserResponseDto:
"""Delete User"""
...
@patch("/users/enable/{uuid}", response_class=UserResponseDto)
async def enable_user(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Enable User"""
...
@patch("/users/reset-traffic/{uuid}", response_class=UserResponseDto)
async def reset_user_traffic(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Reset User Traffic"""
...
@get("/users/short-uuid/{short_uuid}", response_class=UserResponseDto)
async def get_user_by_short_uuid(
self,
short_uuid: Annotated[str, Path(description="Short UUID of the user")],
) -> UserResponseDto:
"""Get User By Short UUID"""
...
@get("/users/sub-uuid/{subscription_uuid}", response_class=UserResponseDto)
async def get_user_by_subscription_uuid(
self,
subscription_uuid: Annotated[str, Path(description="UUID of the subscription")],
) -> UserResponseDto:
"""Get User By Subscription UUID"""
...
@get("/users/uuid/{uuid}", response_class=UserResponseDto)
async def get_user_by_uuid(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
) -> UserResponseDto:
"""Get User By UUID"""
...
@get("/users/username/{username}", response_class=UserResponseDto)
async def get_user_by_username(
self,
username: Annotated[str, Path(description="Username of the user")],
) -> UserResponseDto:
"""Get User By Username"""
...
@get(
"/users/tg/{telegram_id}",
response_class=TelegramUserResponseDto,
)
async def get_users_by_telegram_id(
self,
telegram_id: Annotated[str, Path(description="Telegram ID of the user")],
) -> TelegramUserResponseDto:
"""Get Users By Telegram ID"""
...
@get("/users/email/{email}", response_class=EmailUserResponseDto)
async def get_users_by_email(
self,
email: Annotated[str, Path(description="Email of the user")],
) -> EmailUserResponseDto:
"""Get Users By Email"""
...

View file

@ -0,0 +1,89 @@
from typing import Annotated, List
from uuid import UUID
from rapid_api_client.annotations import PydanticBody
from remnawave_api.enums import UserStatus
from remnawave_api.models import (
BulkAllResetTrafficUsersResponseDto,
BulkAllUpdateUsersRequestDto,
BulkAllUpdateUsersResponseDto,
BulkResponseDto,
BulkUpdateUsersInboundsRequestDto,
UpdateUserFields,
)
from remnawave_api.rapid import AttributeBody, BaseController, patch, post
class UsersBulkActionsController(BaseController):
@post(
"/users/bulk/delete-by-status",
response_class=BulkResponseDto,
)
async def bulk_delete_users_by_status(
self, status: Annotated[UserStatus, AttributeBody()]
) -> BulkResponseDto:
"""Bulk Delete Users By Status"""
...
@post("/users/bulk/delete", response_class=BulkResponseDto)
async def bulk_delete_users(
self,
uuids: Annotated[List[UUID], AttributeBody()],
) -> BulkResponseDto:
"""Bulk Delete Users By UUIDs"""
...
@post(
"/users/bulk/revoke-subscription",
response_class=BulkResponseDto,
)
async def bulk_revoke_users_subscription(
self,
uuids: Annotated[List[UUID], AttributeBody()],
) -> BulkResponseDto:
"""Bulk Revoke Users Subscription"""
...
@post("/users/bulk/reset-traffic", response_class=BulkResponseDto)
async def bulk_reset_user_traffic(
self,
uuids: Annotated[List[UUID], AttributeBody()],
) -> BulkResponseDto:
"""Bulk Reset User Traffic"""
...
@post("/users/bulk/update", response_class=BulkResponseDto)
async def bulk_update_users(
self,
uuids: Annotated[List[UUID], AttributeBody()],
fields: Annotated[UpdateUserFields, AttributeBody()],
) -> BulkResponseDto:
"""Bulk Update Users"""
...
@post("/users/bulk/update-inbounds", response_class=BulkResponseDto)
async def bulk_update_users_inbounds(
self,
body: Annotated[BulkUpdateUsersInboundsRequestDto, PydanticBody()],
) -> BulkResponseDto:
"""Bulk Update Users Inbounds"""
...
@post("/users/bulk/all/update", response_class=BulkAllUpdateUsersResponseDto)
async def bulk_update_all_users(
self,
body: Annotated[BulkAllUpdateUsersRequestDto, PydanticBody()],
) -> BulkAllUpdateUsersResponseDto:
"""Bulk Update All Users"""
...
@patch(
"/users/bulk/all/reset-traffic",
response_class=BulkAllResetTrafficUsersResponseDto,
)
async def bulk_all_reset_user_traffic(
self,
) -> BulkAllResetTrafficUsersResponseDto:
"""Bulk Reset All Users Traffic"""
...

View file

@ -0,0 +1,21 @@
from typing import Annotated
from rapid_api_client import Path, Query
from remnawave_api.models import UserUsageByRangeResponseDto
from remnawave_api.rapid import BaseController, get
class UsersStatsController(BaseController):
@get(
"/users/stats/usage/range/{uuid}",
response_class=UserUsageByRangeResponseDto,
)
async def get_user_usage_by_range(
self,
uuid: Annotated[str, Path(description="UUID of the user")],
start: Annotated[str, Query(description="Start date in ISO format")],
end: Annotated[str, Query(description="End date in ISO format")],
) -> UserUsageByRangeResponseDto:
"""Get User Usage By Range"""
...

View file

@ -0,0 +1,23 @@
from typing import Annotated
from rapid_api_client.annotations import JsonBody
from remnawave_api.models import ConfigResponseDto
from remnawave_api.rapid import BaseController, get, post
class XrayConfigController(BaseController):
@get("/xray/get-config", response_class=ConfigResponseDto)
async def get_config(
self,
) -> ConfigResponseDto:
"""Get Xray Config"""
...
@post("/xray/update-config", response_class=ConfigResponseDto)
async def update_config(
self,
body: Annotated[dict, JsonBody()],
) -> ConfigResponseDto:
"""Update Xray Config"""
...

View file

@ -0,0 +1,18 @@
from .alpn import ALPN
from .client_type import ClientType
from .error_code import ErrorCode
from .fingerprint import Fingerprint
from .security_layer import SecurityLayer
from .template_type import TemplateType
from .users import TrafficLimitStrategy, UserStatus
__all__ = [
"TrafficLimitStrategy",
"UserStatus",
"ErrorCode",
"ClientType",
"ALPN",
"Fingerprint",
"SecurityLayer",
"TemplateType",
]

View file

@ -0,0 +1,10 @@
from enum import StrEnum
class ALPN(StrEnum):
H3 = "h3"
H2 = "h2"
HTTP_1_1 = "http/1.1"
H_COMBINED = "h2,http/1.1"
H3_H2_H1_COMBINED = "h3,h2,http/1.1"
H3_H2_COMBINED = "h3,h2"

View file

@ -0,0 +1,10 @@
from enum import StrEnum
class ClientType(StrEnum):
STASH = "stash"
SINGBOX = "singbox"
SINGBOX_LEGACY = "singbox_legacy"
MIHOMO = "mihomo"
JSON = "json"
CLASH = "clash"

View file

@ -0,0 +1,80 @@
from enum import StrEnum
class ErrorCode(StrEnum):
INTERNAL_SERVER_ERROR = "A001"
LOGIN_ERROR = "A002"
UNAUTHORIZED = "A003"
FORBIDDEN_ROLE_ERROR = "A004"
CREATE_API_TOKEN_ERROR = "A005"
DELETE_API_TOKEN_ERROR = "A006"
REQUESTED_TOKEN_NOT_FOUND = "A007"
FIND_ALL_API_TOKENS_ERROR = "A008"
GET_PUBLIC_KEY_ERROR = "A009"
ENABLE_NODE_ERROR = "A010"
NODE_NOT_FOUND = "A011"
CONFIG_NOT_FOUND = "A012"
UPDATE_CONFIG_ERROR = "A013"
GET_CONFIG_ERROR = "A014"
DELETE_MANY_INBOUNDS_ERROR = "A015"
CREATE_MANY_INBOUNDS_ERROR = "A016"
FIND_ALL_INBOUNDS_ERROR = "A017"
CREATE_USER_ERROR = "A018"
USER_USERNAME_ALREADY_EXISTS = "A019"
USER_SHORT_UUID_ALREADY_EXISTS = "A020"
USER_SUBSCRIPTION_UUID_ALREADY_EXISTS = "A021"
CREATE_USER_WITH_INBOUNDS_ERROR = "A022"
CANT_GET_CREATED_USER_WITH_INBOUNDS = "A023"
GET_ALL_USERS_ERROR = "A024"
USER_NOT_FOUND = "A025"
GET_USER_BY_ERROR = "A026"
REVOKE_USER_SUBSCRIPTION_ERROR = "A027"
DISABLE_USER_ERROR = "A028"
USER_ALREADY_DISABLED = "A029"
USER_ALREADY_ENABLED = "A030"
ENABLE_USER_ERROR = "A031"
CREATE_NODE_ERROR = "A032"
NODE_NAME_ALREADY_EXISTS = "A033"
NODE_ADDRESS_ALREADY_EXISTS = "A034"
NODE_ERROR_WITH_MSG = "N001"
NODE_ERROR_500_WITH_MSG = "N002"
RESTART_NODE_ERROR = "A035"
GET_CONFIG_WITH_USERS_ERROR = "A036"
DELETE_USER_ERROR = "A037"
UPDATE_NODE_ERROR = "A038"
UPDATE_USER_ERROR = "A039"
INCREMENT_USED_TRAFFIC_ERROR = "A040"
GET_ALL_NODES_ERROR = "A041"
GET_ONE_NODE_ERROR = "A042"
DELETE_NODE_ERROR = "A043"
CREATE_HOST_ERROR = "A044"
HOST_REMARK_ALREADY_EXISTS = "A045"
HOST_NOT_FOUND = "A046"
DELETE_HOST_ERROR = "A047"
GET_USER_STATS_ERROR = "A048"
UPDATE_USER_WITH_INBOUNDS_ERROR = "A049"
GET_ALL_HOSTS_ERROR = "A050"
REORDER_HOSTS_ERROR = "A051"
UPDATE_HOST_ERROR = "A052"
CREATE_CONFIG_ERROR = "A053"
ENABLED_NODES_NOT_FOUND = "A054"
GET_NODES_USAGE_BY_RANGE_ERROR = "A055"
RESET_USER_TRAFFIC_ERROR = "A056"
REORDER_NODES_ERROR = "A057"
GET_ALL_INBOUNDS_ERROR = "A058"
BULK_DELETE_USERS_BY_STATUS_ERROR = "A059"
UPDATE_INBOUND_ERROR = "A060"
CONFIG_VALIDATION_ERROR = "A061"
USERS_NOT_FOUND = "A062"
GET_USER_BY_UNIQUE_FIELDS_NOT_FOUND = "A063"
UPDATE_EXCEEDED_TRAFFIC_USERS_ERROR = "A064"
ADMIN_NOT_FOUND = "A065"
CREATE_ADMIN_ERROR = "A066"
GET_AUTH_STATUS_ERROR = "A067"
FORBIDDEN_ONE = "A068"
FORBIDDEN_TWO = "E000"
DISABLE_NODE_ERROR = "A069"
GET_ONE_HOST_ERROR = "A070"
SUBSCRIPTION_SETTINGS_NOT_FOUND = "A071"
GET_SUBSCRIPTION_SETTINGS_ERROR = "A072"
UPDATE_SUBSCRIPTION_SETTINGS_ERROR = "A073"

View file

@ -0,0 +1,13 @@
from enum import StrEnum
class Fingerprint(StrEnum):
CHROME = "chrome"
FIREFOX = "firefox"
SAFARI = "safari"
IOS = "ios"
ANDROID = "android"
EDGE = "edge"
QQ = "qq"
RANDOM = "random"
RANDOMIZED = "randomized"

View file

@ -0,0 +1,7 @@
from enum import StrEnum
class SecurityLayer(StrEnum):
DEFAULT = "DEFAULT"
TLS = "TLS"
NONE = "NONE"

View file

@ -0,0 +1,10 @@
from enum import StrEnum
class TemplateType(StrEnum):
STASH = "STASH"
SINGBOX = "SINGBOX"
SINGBOX_LEGACY = "SINGBOX_LEGACY"
MIHOMO = "MIHOMO"
XRAY_JSON = "XRAY_JSON"
CLASH = "CLASH"

View file

@ -0,0 +1,15 @@
from enum import StrEnum
class UserStatus(StrEnum):
ACTIVE = "ACTIVE"
DISABLED = "DISABLED"
LIMITED = "LIMITED"
EXPIRED = "EXPIRED"
class TrafficLimitStrategy(StrEnum):
NO_RESET = "NO_RESET"
DAY = "DAY"
WEEK = "WEEK"
MONTH = "MONTH"

View file

@ -0,0 +1,23 @@
from .handler import handle_api_error
from .general import (
ConflictError,
ApiErrorResponse,
BadRequestError,
NotFoundError,
ForbiddenError,
UnauthorizedError,
ServerError,
ApiError,
)
__all__ = [
"handle_api_error",
"ApiError",
"ApiErrorResponse",
"NotFoundError",
"BadRequestError",
"ForbiddenError",
"UnauthorizedError",
"ConflictError",
"ServerError",
]

View file

@ -0,0 +1,61 @@
from datetime import datetime
from pydantic import AliasChoices, BaseModel, Field
from remnawave_api.enums import ErrorCode
class ApiErrorResponse(BaseModel):
timestamp: datetime = Field(..., description="Время возникновения ошибки")
path: str = Field(..., description="Путь запроса")
message: str = Field(..., description="Сообщение об ошибке")
code: ErrorCode | str = Field(
...,
validation_alias=AliasChoices("errorCode", "code", "error_code"),
description="Код ошибки",
)
class ApiError(Exception):
def __init__(self, status_code: int, error: ApiErrorResponse):
self.status_code = status_code
self.error = error
super().__init__(
f"API Error {error.code}: {error.message} (HTTP {status_code})"
)
class BadRequestError(ApiError):
"""Ошибки клиента (400)"""
pass
class UnauthorizedError(ApiError):
"""Ошибка авторизации (401)"""
pass
class ForbiddenError(ApiError):
"""Доступ запрещен (403)"""
pass
class NotFoundError(ApiError):
"""Ресурс не найден (404)"""
pass
class ConflictError(ApiError):
"""Конфликт (409)"""
pass
class ServerError(ApiError):
"""Серверная ошибка (500)"""
pass

View file

@ -0,0 +1,130 @@
from datetime import datetime
import httpx
from remnawave_api.enums import ErrorCode
from .general import (
ApiError,
ApiErrorResponse,
BadRequestError,
ConflictError,
ForbiddenError,
NotFoundError,
ServerError,
UnauthorizedError,
)
ERRORS: dict[str, dict] = {
ErrorCode.INTERNAL_SERVER_ERROR: ServerError,
ErrorCode.LOGIN_ERROR: ServerError,
ErrorCode.UNAUTHORIZED: UnauthorizedError,
ErrorCode.FORBIDDEN_ROLE_ERROR: ForbiddenError,
ErrorCode.CREATE_API_TOKEN_ERROR: ServerError,
ErrorCode.DELETE_API_TOKEN_ERROR: ServerError,
ErrorCode.REQUESTED_TOKEN_NOT_FOUND: NotFoundError,
ErrorCode.FIND_ALL_API_TOKENS_ERROR: ServerError,
ErrorCode.GET_PUBLIC_KEY_ERROR: ServerError,
ErrorCode.ENABLE_NODE_ERROR: ServerError,
ErrorCode.NODE_NOT_FOUND: NotFoundError,
ErrorCode.CONFIG_NOT_FOUND: NotFoundError,
ErrorCode.UPDATE_CONFIG_ERROR: ServerError,
ErrorCode.GET_CONFIG_ERROR: ServerError,
ErrorCode.DELETE_MANY_INBOUNDS_ERROR: ServerError,
ErrorCode.CREATE_MANY_INBOUNDS_ERROR: ServerError,
ErrorCode.FIND_ALL_INBOUNDS_ERROR: ServerError,
ErrorCode.CREATE_USER_ERROR: ServerError,
ErrorCode.USER_USERNAME_ALREADY_EXISTS: BadRequestError,
ErrorCode.USER_SHORT_UUID_ALREADY_EXISTS: BadRequestError,
ErrorCode.USER_SUBSCRIPTION_UUID_ALREADY_EXISTS: BadRequestError,
ErrorCode.CREATE_USER_WITH_INBOUNDS_ERROR: ServerError,
ErrorCode.CANT_GET_CREATED_USER_WITH_INBOUNDS: ServerError,
ErrorCode.GET_ALL_USERS_ERROR: ServerError,
ErrorCode.USER_NOT_FOUND: NotFoundError,
ErrorCode.GET_USER_BY_ERROR: ServerError,
ErrorCode.REVOKE_USER_SUBSCRIPTION_ERROR: ServerError,
ErrorCode.DISABLE_USER_ERROR: ServerError,
ErrorCode.USER_ALREADY_DISABLED: BadRequestError,
ErrorCode.USER_ALREADY_ENABLED: BadRequestError,
ErrorCode.ENABLE_USER_ERROR: ServerError,
ErrorCode.CREATE_NODE_ERROR: ServerError,
ErrorCode.NODE_NAME_ALREADY_EXISTS: BadRequestError,
ErrorCode.NODE_ADDRESS_ALREADY_EXISTS: BadRequestError,
ErrorCode.NODE_ERROR_WITH_MSG: ServerError,
ErrorCode.NODE_ERROR_500_WITH_MSG: ServerError,
ErrorCode.RESTART_NODE_ERROR: ServerError,
ErrorCode.GET_CONFIG_WITH_USERS_ERROR: ServerError,
ErrorCode.DELETE_USER_ERROR: ServerError,
ErrorCode.UPDATE_NODE_ERROR: ServerError,
ErrorCode.UPDATE_USER_ERROR: ServerError,
ErrorCode.INCREMENT_USED_TRAFFIC_ERROR: ServerError,
ErrorCode.GET_ALL_NODES_ERROR: ServerError,
ErrorCode.GET_ONE_NODE_ERROR: ServerError,
ErrorCode.DELETE_NODE_ERROR: ServerError,
ErrorCode.CREATE_HOST_ERROR: ServerError,
ErrorCode.HOST_REMARK_ALREADY_EXISTS: BadRequestError,
ErrorCode.HOST_NOT_FOUND: NotFoundError,
ErrorCode.DELETE_HOST_ERROR: ServerError,
ErrorCode.GET_USER_STATS_ERROR: ServerError,
ErrorCode.UPDATE_USER_WITH_INBOUNDS_ERROR: ServerError,
ErrorCode.GET_ALL_HOSTS_ERROR: ServerError,
ErrorCode.REORDER_HOSTS_ERROR: ServerError,
ErrorCode.UPDATE_HOST_ERROR: ServerError,
ErrorCode.CREATE_CONFIG_ERROR: ServerError,
ErrorCode.ENABLED_NODES_NOT_FOUND: ConflictError,
ErrorCode.GET_NODES_USAGE_BY_RANGE_ERROR: ServerError,
ErrorCode.RESET_USER_TRAFFIC_ERROR: ServerError,
ErrorCode.REORDER_NODES_ERROR: ServerError,
ErrorCode.GET_ALL_INBOUNDS_ERROR: ServerError,
ErrorCode.BULK_DELETE_USERS_BY_STATUS_ERROR: ServerError,
ErrorCode.UPDATE_INBOUND_ERROR: ServerError,
ErrorCode.CONFIG_VALIDATION_ERROR: ServerError,
ErrorCode.USERS_NOT_FOUND: NotFoundError,
ErrorCode.GET_USER_BY_UNIQUE_FIELDS_NOT_FOUND: NotFoundError,
ErrorCode.UPDATE_EXCEEDED_TRAFFIC_USERS_ERROR: ServerError,
ErrorCode.ADMIN_NOT_FOUND: NotFoundError,
ErrorCode.CREATE_ADMIN_ERROR: ServerError,
ErrorCode.GET_AUTH_STATUS_ERROR: ServerError,
ErrorCode.FORBIDDEN_ONE: ForbiddenError,
ErrorCode.DISABLE_NODE_ERROR: ServerError,
ErrorCode.GET_ONE_HOST_ERROR: ServerError,
ErrorCode.SUBSCRIPTION_SETTINGS_NOT_FOUND: NotFoundError,
ErrorCode.GET_SUBSCRIPTION_SETTINGS_ERROR: ServerError,
ErrorCode.UPDATE_SUBSCRIPTION_SETTINGS_ERROR: ServerError,
}
def handle_api_error(response: httpx.Response) -> None:
if response.status_code >= 400:
try:
error_data = response.json()
error_response = ApiErrorResponse(**error_data)
if error_response.code in ERRORS:
exception_class = ERRORS[error_response.code]
else:
if response.status_code == 400:
exception_class = BadRequestError
elif response.status_code == 401:
exception_class = UnauthorizedError
elif response.status_code == 403:
exception_class = ForbiddenError
elif response.status_code == 404:
exception_class = NotFoundError
elif response.status_code == 409:
exception_class = ConflictError
elif response.status_code >= 500:
exception_class = ServerError
else:
exception_class = ApiError
raise exception_class(response.status_code, error_response)
except ValueError:
raise ApiError(
response.status_code,
ApiErrorResponse(
timestamp=datetime.now(),
path=response.request.url.path,
message="Unknown error " + response.text,
code="UNKNOWN",
),
)

View file

@ -0,0 +1,167 @@
from .api_tokens_management import CreateApiTokenRequestDto
from .auth import (
LoginRequestDto,
LoginResponseDto,
RegisterRequestDto,
RegisterResponseDto,
StatusResponseDto,
)
from .bandwidthstats import NodesUsageResponseDto, NodeUsageResponseDto
from .hosts import (
CreateHostRequestDto,
DeleteHostResponseDto,
HostResponseDto,
HostsResponseDto,
ReorderHostRequestDto,
ReorderHostResponseDto,
UpdateHostRequestDto,
)
from .hosts_bulk_actions import (
BulkDeleteHostsResponseDto,
BulkDisableHostsResponseDto,
BulkEnableHostsResponseDto,
SetInboundToManyHostsRequestDto,
SetInboundToManyHostsResponseDto,
SetPortToManyHostsResponseDto,
)
from .inbounds import (
FullInboundResponseDto,
FullInboundsResponseDto,
InboundResponseDto,
InboundsResponseDto,
FullInboundStatistic
)
from .inbounds_bulk_actions import (
AddInboundToNodesResponseDto,
AddInboundToUsersResponseDto,
RemoveInboundFromNodesResponseDto,
RemoveInboundFromUsersResponseDto,
)
from .keygen import PubKeyResponseDto
from .nodes import (
CreateNodeRequestDto,
DeleteNodeResponseDto,
NodeResponseDto,
NodesResponseDto,
ReorderNodeRequestDto,
RestartNodeResponseDto,
UpdateNodeRequestDto,
ExcludedInbounds
)
from .subscription import SubscriptionInfoResponseDto, UserSubscription
from .subscriptions_settings import (
SubscriptionSettingsResponseDto,
UpdateSubscriptionSettingsRequestDto,
)
from .subscriptions_template import TemplateResponseDto, UpdateTemplateRequestDto
from .system import (
BandwidthStatistic,
BandwidthStatisticResponseDto,
NodesStatisticResponseDto,
StatisticResponseDto,
NodeStatistic,
CPUStatistic,
MemoryStatistic,
StatusCounts,
UsersStatistic,
OnlineStatistic
)
from .users import (
CreateUserRequestDto,
DeleteUserResponseDto,
EmailUserResponseDto,
TelegramUserResponseDto,
UpdateUserRequestDto,
UserActiveInboundsDto,
UserLastConnectedNodeDto,
UserResponseDto,
UsersResponseDto,
)
from .users_bulk_actions import (
BulkAllResetTrafficUsersResponseDto,
BulkAllUpdateUsersRequestDto,
BulkAllUpdateUsersResponseDto,
BulkResponseDto,
BulkUpdateUsersInboundsRequestDto,
UpdateUserFields,
)
from .users_stats import UserUsageByRange, UserUsageByRangeResponseDto
from .xray_config import ConfigResponseDto
__all__ = [
"CPUStatistic",
"MemoryStatistic",
"StatusCounts",
"UsersStatistic",
"OnlineStatistic",
"NodeStatistic",
"ExcludedInbounds",
"FullInboundStatistic",
"ConfigResponseDto",
"AddInboundToNodesResponseDto",
"AddInboundToUsersResponseDto",
"RemoveInboundFromNodesResponseDto",
"RemoveInboundFromUsersResponseDto",
"BulkDeleteHostsResponseDto",
"BulkEnableHostsResponseDto",
"BulkDisableHostsResponseDto",
"SetPortToManyHostsResponseDto",
"SetInboundToManyHostsRequestDto",
"SetInboundToManyHostsResponseDto",
"TemplateResponseDto",
"UpdateTemplateRequestDto",
"SubscriptionSettingsResponseDto",
"UpdateSubscriptionSettingsRequestDto",
"PubKeyResponseDto",
"UserUsageByRange",
"UserUsageByRangeResponseDto",
"BandwidthStatistic",
"BandwidthStatisticResponseDto",
"NodesStatisticResponseDto",
"StatisticResponseDto",
"CreateApiTokenRequestDto",
"UserActiveInboundsDto",
"EmailUserResponseDto",
"CreateUserRequestDto",
"UserResponseDto",
"DeleteUserResponseDto",
"UsersResponseDto",
"TelegramUserResponseDto",
"UpdateUserRequestDto",
"UserLastConnectedNodeDto",
"StatusResponseDto",
"LoginRequestDto",
"LoginResponseDto",
"RegisterRequestDto",
"RegisterResponseDto",
"NodesUsageResponseDto",
"NodeUsageResponseDto",
"HostResponseDto",
"DeleteHostResponseDto",
"CreateHostRequestDto",
"HostsResponseDto",
"ReorderHostResponseDto",
"ReorderHostRequestDto",
"UpdateHostRequestDto",
"FullInboundResponseDto",
"FullInboundsResponseDto",
"InboundResponseDto",
"InboundsResponseDto",
"DeleteNodeResponseDto",
"NodeResponseDto",
"NodesResponseDto",
"CreateNodeRequestDto",
"ReorderNodeRequestDto",
"RestartNodeResponseDto",
"UpdateNodeRequestDto",
"SubscriptionInfoResponseDto",
"UserSubscription",
"BulkAllResetTrafficUsersResponseDto",
"BulkAllUpdateUsersRequestDto",
"BulkAllUpdateUsersResponseDto",
"UpdateUserFields",
"BulkDeleteUsersByStatusRequestDto",
"BulkDeleteUsersRequestDto",
"BulkResponseDto",
"BulkUpdateUsersInboundsRequestDto",
]

View file

@ -0,0 +1,10 @@
from typing import Optional
from pydantic import BaseModel, Field
class CreateApiTokenRequestDto(BaseModel):
token_name: str = Field(serialization_alias="tokenName")
token_description: Optional[str] = Field(
None, serialization_alias="tokenDescription"
)

View file

@ -0,0 +1,26 @@
from typing import Annotated
from pydantic import BaseModel, Field, StringConstraints
class RegisterResponseDto(BaseModel):
access_token: str = Field(alias="accessToken")
class LoginResponseDto(BaseModel):
access_token: str = Field(alias="accessToken")
class LoginRequestDto(BaseModel):
username: str
password: str
class RegisterRequestDto(BaseModel):
username: str
password: Annotated[str, StringConstraints(min_length=24)]
class StatusResponseDto(BaseModel):
is_login_allowed: bool = Field(alias="isLoginAllowed")
is_register_allowed: bool = Field(alias="isRegisterAllowed")

View file

@ -0,0 +1,21 @@
import datetime
from typing import List
from uuid import UUID
from pydantic import BaseModel, Field
class NodeUsageResponseDto(BaseModel):
node_uuid: UUID = Field(alias="nodeUuid")
node_name: str = Field(alias="nodeName")
total: float
total_download: float = Field(alias="totalDownload")
total_upload: float = Field(alias="totalUpload")
human_readable_total: str = Field(alias="humanReadableTotal")
human_readable_total_download: str = Field(alias="humanReadableTotalDownload")
human_readable_total_upload: str = Field(alias="humanReadableTotalUpload")
date: datetime.date
class NodesUsageResponseDto(BaseModel):
response: List[NodeUsageResponseDto]

View file

@ -0,0 +1,91 @@
from typing import Annotated, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field, StringConstraints
from remnawave_api.enums import ALPN, Fingerprint, SecurityLayer
class ReorderHostRequestDto(BaseModel):
view_position: int = Field(serialization_alias="viewPosition")
uuid: UUID
class UpdateHostRequestDto(BaseModel):
uuid: UUID
inbound_uuid: Optional[UUID] = Field(None, serialization_alias="inboundUuid")
remark: Annotated[Optional[str], StringConstraints(max_length=40)] = None
address: Optional[str] = None
port: Optional[int] = None
path: Optional[str] = None
sni: Optional[str] = None
host: Optional[str] = None
alpn: Optional[ALPN] = None
fingerprint: Optional[Fingerprint] = None
allow_insecure: Optional[bool] = Field(None, serialization_alias="allowInsecure")
is_disabled: Optional[bool] = Field(None, serialization_alias="isDisabled")
security_layer: Optional[SecurityLayer] = Field(
None, serialization_alias="securityLayer"
)
class HostResponseDto(BaseModel):
uuid: UUID
inbound_uuid: UUID = Field(alias="inboundUuid")
view_position: int = Field(alias="viewPosition")
remark: str
address: str
port: int
path: Optional[str] = None
sni: Optional[str] = None
host: Optional[str] = None
alpn: Optional[ALPN] = None
fingerprint: Optional[Fingerprint] = None
allow_insecure: Optional[bool] = Field(
None,
alias="allowInsecure",
)
is_disabled: Optional[bool] = Field(
None,
alias="isDisabled",
)
security_layer: Optional[SecurityLayer] = Field(
None,
alias="securityLayer",
)
class HostsResponseDto(BaseModel):
response: List[HostResponseDto]
class ReorderHostResponseDto(BaseModel):
is_updated: bool = Field(alias="isUpdated")
class CreateHostRequestDto(BaseModel):
inbound_uuid: UUID = Field(serialization_alias="inboundUuid")
remark: Annotated[str, StringConstraints(max_length=40)]
address: str
port: int
path: Optional[str] = None
sni: Optional[str] = None
host: Optional[str] = None
alpn: Optional[ALPN] = None
fingerprint: Optional[Fingerprint] = None
allow_insecure: Optional[bool] = Field(
None,
serialization_alias="allowInsecure",
)
is_disabled: Optional[bool] = Field(
None,
serialization_alias="isDisabled",
)
security_layer: Optional[SecurityLayer] = Field(
None,
serialization_alias="securityLayer",
)
class DeleteHostResponseDto(BaseModel):
is_deleted: bool = Field(alias="isDeleted")

View file

@ -0,0 +1,31 @@
from typing import List
from uuid import UUID
from pydantic import BaseModel, Field
from remnawave_api.models import HostResponseDto
class SetInboundToManyHostsRequestDto(BaseModel):
uuids: List[UUID]
inbound_uuid: UUID = Field(serialization_alias="inboundUuid")
class BulkDeleteHostsResponseDto(BaseModel):
response: List[HostResponseDto]
class BulkDisableHostsResponseDto(BaseModel):
response: List[HostResponseDto]
class BulkEnableHostsResponseDto(BaseModel):
response: List[HostResponseDto]
class SetInboundToManyHostsResponseDto(BaseModel):
response: List[HostResponseDto]
class SetPortToManyHostsResponseDto(BaseModel):
response: List[HostResponseDto]

View file

@ -0,0 +1,38 @@
from typing import Any, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field
class InboundResponseDto(BaseModel):
uuid: UUID
tag: str
type: str
port: float
network: Optional[str] = None
security: Optional[str] = None
class InboundsResponseDto(BaseModel):
response: List[InboundResponseDto]
class FullInboundStatistic(BaseModel):
enabled: float
disabled: float
class FullInboundResponseDto(BaseModel):
uuid: UUID
tag: str
type: str
port: float
network: Optional[str] = None
security: Optional[str] = None
raw_from_config: Any = Field(alias="rawFromConfig")
users: FullInboundStatistic
nodes: FullInboundStatistic
class FullInboundsResponseDto(BaseModel):
response: List[FullInboundResponseDto]

View file

@ -0,0 +1,17 @@
from pydantic import BaseModel, Field
class AddInboundToUsersResponseDto(BaseModel):
is_success: bool = Field(alias="isSuccess")
class RemoveInboundFromNodesResponseDto(BaseModel):
is_success: bool = Field(alias="isSuccess")
class RemoveInboundFromUsersResponseDto(BaseModel):
is_success: bool = Field(alias="isSuccess")
class AddInboundToNodesResponseDto(BaseModel):
is_success: bool = Field(alias="isSuccess")

View file

@ -0,0 +1,5 @@
from pydantic import BaseModel, Field
class PubKeyResponseDto(BaseModel):
pub_key: str = Field(alias="pubKey")

View file

@ -0,0 +1,113 @@
from datetime import datetime
from typing import Annotated, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field, StringConstraints
class ExcludedInbounds(BaseModel):
uuid: UUID
tag: str
type: str
class RestartNodeResponseDto(BaseModel):
event_sent: bool = Field(alias="eventSent")
class CreateNodeRequestDto(BaseModel):
name: Annotated[str, StringConstraints(min_length=5)]
address: Annotated[str, StringConstraints(min_length=2)]
port: int = Field(strict=True, ge=1)
is_traffic_tracking_active: Optional[bool] = Field(
None,
serialization_alias="isTrafficTrackingActive",
)
traffic_limit_bytes: Optional[int] = Field(
None, serialization_alias="trafficLimitBytes", strict=True, ge=0
)
notify_percent: Optional[int] = Field(
None, serialization_alias="notifyPercent", strict=True, ge=0
)
traffic_reset_day: Optional[int] = Field(
None, serialization_alias="trafficResetDay", strict=True, ge=1
)
excluded_inbounds: Optional[List[UUID]] = Field(
None, serialization_alias="excludedInbounds"
)
country_code: Annotated[Optional[str], StringConstraints(max_length=2)] = Field(
None, serialization_alias="countryCode"
)
consumption_multiplier: Optional[float] = Field(
None, serialization_alias="consumptionMultiplier"
)
class UpdateNodeRequestDto(BaseModel):
uuid: UUID
name: Annotated[Optional[str], StringConstraints(min_length=5)] = None
address: Annotated[Optional[str], StringConstraints(min_length=2)] = None
port: Optional[int] = None
is_traffic_tracking_active: Optional[bool] = Field(
None, serialization_alias="isTrafficTrackingActive"
)
traffic_limit_bytes: Optional[float] = Field(
None, serialization_alias="trafficLimitBytes"
)
notify_percent: Optional[float] = Field(None, serialization_alias="notifyPercent")
traffic_reset_day: Optional[float] = Field(
None, serialization_alias="trafficResetDay"
)
excluded_inbounds: Optional[List[UUID]] = Field(
None, serialization_alias="excludedInbounds"
)
country_code: Annotated[Optional[str], StringConstraints(max_length=2)] = Field(
None, serialization_alias="countryCode"
)
consumption_multiplier: Optional[float] = Field(
None, serialization_alias="consumptionMultiplier"
)
class ReorderNodeRequestDto(BaseModel):
view_position: int = Field(serialization_alias="viewPosition")
uuid: UUID
class NodeResponseDto(BaseModel):
uuid: UUID
name: str
address: str
port: Optional[int] = None
is_connected: bool = Field(alias="isConnected")
is_disabled: bool = Field(alias="isDisabled")
is_connecting: bool = Field(alias="isConnecting")
is_node_online: bool = Field(alias="isNodeOnline")
is_xray_running: bool = Field(alias="isXrayRunning")
last_status_change: Optional[datetime] = Field(None, alias="lastStatusChange")
last_status_message: Optional[str] = Field(None, alias="lastStatusMessage")
xray_version: Optional[str] = Field(None, alias="xrayVersion")
xray_uptime: str = Field(alias="xrayUptime")
is_traffic_tracking_active: bool = Field(alias="isTrafficTrackingActive")
traffic_reset_day: Optional[int] = Field(None, alias="trafficResetDay")
traffic_limit_bytes: Optional[float] = Field(None, alias="trafficLimitBytes")
traffic_used_bytes: Optional[float] = Field(None, alias="trafficUsedBytes")
notify_percent: Optional[int] = Field(None, alias="notifyPercent")
users_online: Optional[int] = Field(None, alias="usersOnline")
view_position: int = Field(alias="viewPosition")
country_code: str = Field(alias="countryCode")
consumption_multiplier: float = Field(alias="consumptionMultiplier")
cpu_count: Optional[int] = Field(None, alias="cpuCount")
cpu_model: Optional[str] = Field(None, alias="cpuModel")
total_ram: Optional[str] = Field(None, alias="totalRam")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
excluded_inbounds: List[ExcludedInbounds] = Field(alias="excludedInbounds")
class NodesResponseDto(BaseModel):
response: List[NodeResponseDto]
class DeleteNodeResponseDto(BaseModel):
is_deleted: bool = Field(alias="isDeleted")

View file

@ -0,0 +1,26 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
from remnawave_api.enums import TrafficLimitStrategy, UserStatus
class UserSubscription(BaseModel):
short_uuid: str = Field(alias="shortUuid")
username: str
days_left: int = Field(alias="daysLeft")
traffic_used: str = Field(alias="trafficUsed")
traffic_limit: str = Field(alias="trafficLimit")
traffic_limit_strategy: TrafficLimitStrategy = Field(alias="trafficLimitStrategy")
expires_at: datetime = Field(alias="expiresAt")
user_status: UserStatus = Field(alias="userStatus")
is_active: bool = Field(alias="isActive")
class SubscriptionInfoResponseDto(BaseModel):
is_found: bool = Field(alias="isFound")
user: Optional[UserSubscription] = None
links: list[str]
ss_conf_links: dict = Field(alias="ssConfLinks")
subscription_url: str = Field(alias="subscriptionUrl")

View file

@ -0,0 +1,57 @@
from datetime import datetime
from typing import Annotated, List, Optional
from uuid import UUID
from pydantic import BaseModel, Field, StringConstraints
class SubscriptionSettingsResponseDto(BaseModel):
uuid: UUID
profile_title: str = Field(alias="profileTitle")
support_link: str = Field(alias="supportLink")
profile_update_interval: int = Field(
alias="profileUpdateInterval", strict=True, ge=1
)
is_profile_webpage_url_enabled: bool = Field(alias="isProfileWebpageUrlEnabled")
serve_json_at_base_subscription: bool = Field(alias="serveJsonAtBaseSubscription")
add_username_to_base_subscription: bool = Field(
alias="addUsernameToBaseSubscription"
)
happ_announce: Optional[str] = Field(None, alias="happAnnounce")
happ_routing: Optional[str] = Field(None, alias="happRouting")
expired_users_remarks: List[str] = Field(alias="expiredUsersRemarks")
limited_users_remarks: List[str] = Field(alias="limitedUsersRemarks")
disabled_users_remarks: List[str] = Field(alias="disabledUsersRemarks")
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
class UpdateSubscriptionSettingsRequestDto(BaseModel):
uuid: UUID
profile_title: Optional[str] = Field(None, serialization_alias="profileTitle")
support_link: Optional[str] = Field(None, serialization_alias="supportLink")
profile_update_interval: Optional[int] = Field(
None, serialization_alias="profileUpdateInterval"
)
is_profile_webpage_url_enabled: Optional[bool] = Field(
None, serialization_alias="isProfileWebpageUrlEnabled"
)
serve_json_at_base_subscription: Optional[bool] = Field(
None, serialization_alias="serveJsonAtBaseSubscription"
)
add_username_to_base_subscription: Optional[bool] = Field(
None, serialization_alias="addUsernameToBaseSubscription"
)
happ_announce: Annotated[Optional[str], StringConstraints(max_length=200)] = Field(
None, serialization_alias="happAnnounce"
)
happ_routing: Optional[str] = Field(None, serialization_alias="happRouting")
expired_users_remarks: Optional[List[str]] = Field(
None, serialization_alias="expiredUsersRemarks"
)
limited_users_remarks: Optional[List[str]] = Field(
None, serialization_alias="limitedUsersRemarks"
)
disabled_users_remarks: Optional[List[str]] = Field(
None, serialization_alias="disabledUsersRemarks"
)

View file

@ -0,0 +1,21 @@
from typing import Optional
from uuid import UUID
from pydantic import BaseModel, Field
from remnawave_api.enums import TemplateType
class TemplateResponseDto(BaseModel):
uuid: UUID
template_type: TemplateType = Field(alias="templateType")
template_json: Optional[dict] = Field(None, alias="templateJson")
encoded_template_yaml: Optional[str] = Field(None, alias="encodedTemplateYaml")
class UpdateTemplateRequestDto(BaseModel):
template_type: TemplateType = Field(serialization_alias="templateType")
template_json: Optional[dict] = Field(None, serialization_alias="templateJson")
encoded_template_yaml: Optional[str] = Field(
None, serialization_alias="encodedTemplateYaml"
)

View file

@ -0,0 +1,70 @@
import datetime
from typing import List
from pydantic import BaseModel, Field
class NodeStatistic(BaseModel):
node_name: str = Field(alias="nodeName")
date: datetime.date
total_bytes: int = Field(alias="totalBytes")
class NodesStatisticResponseDto(BaseModel):
last_seven_days: List[NodeStatistic] = Field(alias="lastSevenDays")
class BandwidthStatistic(BaseModel):
current: str
previous: str
difference: str
class BandwidthStatisticResponseDto(BaseModel):
last_two_days: BandwidthStatistic = Field(alias="bandwidthLastTwoDays")
last_seven_days: BandwidthStatistic = Field(alias="bandwidthLastSevenDays")
last_30_days: BandwidthStatistic = Field(alias="bandwidthLast30Days")
calendar_month: BandwidthStatistic = Field(alias="bandwidthCalendarMonth")
current_year: BandwidthStatistic = Field(alias="bandwidthCurrentYear")
class CPUStatistic(BaseModel):
cores: int
physical_cores: int = Field(alias="physicalCores")
class MemoryStatistic(BaseModel):
total: int
free: int
used: int
active: int
available: int
class StatusCounts(BaseModel):
active: int = Field(alias="ACTIVE")
disabled: int = Field(alias="DISABLED")
limited: int = Field(alias="LIMITED")
expired: int = Field(alias="EXPIRED")
class UsersStatistic(BaseModel):
status_counts: StatusCounts = Field(alias="statusCounts")
total_users: int = Field(alias="totalUsers")
total_traffic_bytes: int = Field(alias="totalTrafficBytes")
class OnlineStatistic(BaseModel):
last_day: int = Field(alias="lastDay")
last_week: int = Field(alias="lastWeek")
never_online: int = Field(alias="neverOnline")
online_now: int = Field(alias="onlineNow")
class StatisticResponseDto(BaseModel):
cpu: CPUStatistic
memory: MemoryStatistic
uptime: float
timestamp: int
users: UsersStatistic
online_stats: OnlineStatistic = Field(alias="onlineStats")

View file

@ -0,0 +1,131 @@
from datetime import datetime
from typing import Annotated, List, Optional
from uuid import UUID
from pydantic import (
BaseModel,
Field,
StringConstraints,
)
from remnawave_api.enums import TrafficLimitStrategy, UserStatus
class UserActiveInboundsDto(BaseModel):
uuid: UUID
tag: str
type: str
class UserLastConnectedNodeDto(BaseModel):
connected_at: datetime = Field(alias="connectedAt")
node_name: str = Field(alias="nodeName")
class CreateUserRequestDto(BaseModel):
username: Annotated[
str, StringConstraints(pattern=r"^[a-zA-Z0-9_-]+$", min_length=6, max_length=34)
]
status: Optional[UserStatus] = None
subscription_uuid: Optional[str] = Field(
None, serialization_alias="subscriptionUuid"
)
short_uuid: Optional[str] = Field(None, serialization_alias="shortUuid")
trojan_password: Annotated[
Optional[str], StringConstraints(min_length=8, max_length=32)
] = Field(None, serialization_alias="trojanPassword")
vless_uuid: Optional[str] = Field(None, serialization_alias="vlessUuid")
ss_password: Annotated[
Optional[str], StringConstraints(min_length=8, max_length=32)
] = Field(None, serialization_alias="ssPassword")
traffic_limit_bytes: Optional[int] = Field(
None, serialization_alias="trafficLimitBytes", strict=True, ge=0
)
traffic_limit_strategy: Optional[TrafficLimitStrategy] = Field(
None, serialization_alias="trafficLimitStrategy"
)
active_user_inbounds: Optional[List[str]] = Field(
None, serialization_alias="activeUserInbounds"
)
expire_at: datetime = Field(..., serialization_alias="expireAt")
created_at: Optional[datetime] = Field(None, serialization_alias="createdAt")
last_traffic_reset_at: Optional[datetime] = Field(
None, serialization_alias="lastTrafficResetAt"
)
description: Optional[str] = None
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
email: Optional[str] = None
activate_all_inbounds: Optional[bool] = Field(
None, serialization_alias="activateAllInbounds"
)
class UpdateUserRequestDto(BaseModel):
uuid: UUID
status: Optional[UserStatus] = None
traffic_limit_bytes: Optional[int] = Field(
None, serialization_alias="trafficLimitBytes", strict=True, ge=0
)
traffic_limit_strategy: Optional[TrafficLimitStrategy] = Field(
None, serialization_alias="trafficLimitStrategy"
)
active_user_inbounds: Optional[List[str]] = Field(
None, serialization_alias="activeUserInbounds"
)
expire_at: Optional[datetime] = Field(None, serialization_alias="expireAt")
last_traffic_reset_at: Optional[datetime] = Field(
None, serialization_alias="lastTrafficResetAt"
)
description: Optional[str] = None
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
email: Optional[str] = None
class UserResponseDto(BaseModel):
uuid: UUID
subscription_uuid: UUID = Field(alias="subscriptionUuid")
short_uuid: str = Field(alias="shortUuid")
username: str
status: Optional[UserStatus] = None
used_traffic_bytes: float = Field(alias="usedTrafficBytes")
lifetime_used_traffic_bytes: float = Field(alias="lifetimeUsedTrafficBytes")
traffic_limit_bytes: Optional[int] = Field(None, alias="trafficLimitBytes")
traffic_limit_strategy: Optional[str] = Field(None, alias="trafficLimitStrategy")
sub_last_user_agent: Optional[str] = Field(None, alias="subLastUserAgent")
sub_last_opened_at: Optional[datetime] = Field(None, alias="subLastOpenedAt")
expire_at: Optional[datetime] = Field(None, alias="expireAt")
online_at: Optional[datetime] = Field(None, alias="onlineAt")
sub_revoked_at: Optional[datetime] = Field(None, alias="subRevokedAt")
last_traffic_reset_at: Optional[datetime] = Field(None, alias="lastTrafficResetAt")
trojan_password: str = Field(alias="trojanPassword")
vless_uuid: UUID = Field(alias="vlessUuid")
ss_password: str = Field(alias="ssPassword")
description: Optional[str] = None
telegram_id: Optional[int] = Field(None, alias="telegramId")
email: Optional[str] = None
created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt")
active_user_inbounds: List[UserActiveInboundsDto] = Field(
alias="activeUserInbounds"
)
subscription_url: str = Field(alias="subscriptionUrl")
last_connected_node: Optional[UserLastConnectedNodeDto] = Field(
None, alias="lastConnectedNode"
)
class EmailUserResponseDto(BaseModel):
response: List[UserResponseDto]
class TelegramUserResponseDto(BaseModel):
response: List[UserResponseDto]
class UsersResponseDto(BaseModel):
users: List[UserResponseDto]
total: float
class DeleteUserResponseDto(BaseModel):
is_deleted: bool = Field(alias="isDeleted")

View file

@ -0,0 +1,52 @@
from datetime import datetime
from typing import List, Optional
from uuid import UUID
from pydantic import BaseModel, Field
from remnawave_api.enums import UserStatus
class BulkUpdateUsersInboundsRequestDto(BaseModel):
uuids: List[UUID]
active_user_inbounds: List[UUID] = Field(serialization_alias="activeUserInbounds")
class UpdateUserFields(BaseModel):
status: Optional[UserStatus] = None
traffic_limit_bytes: Optional[int] = Field(
None, serialization_alias="trafficLimitBytes", strict=True, ge=0
)
traffic_limit_strategy: Optional[str] = Field(
None, serialization_alias="trafficLimitStrategy"
)
expire_at: Optional[datetime] = Field(None, serialization_alias="expireAt")
description: Optional[str] = None
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
email: Optional[str] = None
class BulkAllUpdateUsersRequestDto(BaseModel):
status: Optional[str] = None
traffic_limit_bytes: Optional[int] = Field(
None, serialization_alias="trafficLimitBytes", strict=True, ge=0
)
traffic_limit_strategy: Optional[str] = Field(
None, serialization_alias="trafficLimitStrategy"
)
expire_at: Optional[datetime] = Field(None, serialization_alias="expireAt")
description: Optional[str] = None
telegram_id: Optional[int] = Field(None, serialization_alias="telegramId")
email: Optional[str] = None
class BulkResponseDto(BaseModel):
affected_rows: int = Field(alias="affectedRows")
class BulkAllResetTrafficUsersResponseDto(BaseModel):
event_sent: bool = Field(alias="eventSent")
class BulkAllUpdateUsersResponseDto(BaseModel):
event_sent: bool = Field(alias="eventSent")

View file

@ -0,0 +1,17 @@
import datetime
from typing import List
from uuid import UUID
from pydantic import BaseModel, Field
class UserUsageByRange(BaseModel):
user_uuid: UUID = Field(alias="userUuid")
node_uuid: UUID = Field(alias="nodeUuid")
node_name: str = Field(alias="nodeName")
total: int
date: datetime.date
class UserUsageByRangeResponseDto(BaseModel):
response: List[UserUsageByRange]

View file

@ -0,0 +1,5 @@
from pydantic import BaseModel
class ConfigResponseDto(BaseModel):
config: dict

View file

@ -0,0 +1,5 @@
from .annotations import AttributeBody
from .client import BaseController
from .decorators import delete, get, patch, post, put
__all__ = ["BaseController", "post", "get", "patch", "put", "delete", "AttributeBody"]

View file

@ -0,0 +1,5 @@
from rapid_api_client import Body
class AttributeBody(Body):
pass

View file

@ -0,0 +1,210 @@
from __future__ import annotations
from datetime import datetime
from inspect import BoundArguments, Signature
from typing import Any, Dict, Mapping, Self, Tuple, Type
import httpx
import orjson
from httpx import Request, Response
from pydantic import BaseModel, TypeAdapter
from rapid_api_client import (
Body,
FileBody,
FormBody,
PydanticBody,
PydanticXmlBody,
RapidApi,
)
from rapid_api_client.annotations import Header, JsonBody, Path, Query
from rapid_api_client.client import pydantic_xml, RapidParameter, RapidParameters
from rapid_api_client.typing import BM, T
from rapid_api_client.utils import filter_none_values, find_annotation
from remnawave_api.exceptions import ApiError, ApiErrorResponse, handle_api_error
from remnawave_api.rapid import AttributeBody
from remnawave_api.utils.serializer import orjson_default
class BaseController(RapidApi):
def _build_request(
self,
sig: Signature,
rapid_parameters: CustomRapidParameters,
method: str,
path: str,
args: Tuple[Any],
kwargs: Mapping[str, Any],
timeout: float | None,
) -> Request:
ba = sig.bind_partial(*args, **kwargs)
ba.apply_defaults()
path = rapid_parameters.get_resolved_path(path, ba)
build_kwargs: Dict[str, Any] = {
"headers": rapid_parameters.get_headers(ba),
"params": rapid_parameters.get_query(ba),
}
post_kw, post_data = rapid_parameters.get_body(ba)
if post_kw is not None:
build_kwargs[post_kw] = post_data
if timeout is not None:
build_kwargs["timeout"] = timeout
return self.client.build_request(method, path, **build_kwargs)
def _handle_response(
self,
response: Response,
response_class: Type[Response | str | bytes | BM] | TypeAdapter[T] = Response,
) -> Response | str | bytes | BM | T:
if response_class is Response:
return response
try:
response.raise_for_status()
except httpx.HTTPStatusError as e:
handle_api_error(e.response)
except httpx.RequestError as e:
now_time = datetime.now()
raise ApiError(
0,
ApiErrorResponse(
timestamp=now_time,
path="/api/users",
message=f"Request error: {str(e)}",
code="NETWORK_ERROR",
),
)
if response_class is str:
return response.text
if response_class is bytes:
return response.content
if isinstance(response_class, TypeAdapter):
return response_class.validate_json(response.content)
if pydantic_xml is not None and issubclass(
response_class, pydantic_xml.BaseXmlModel
):
return response_class.from_xml(response.content)
if issubclass(response_class, BaseModel):
data: dict = response.json()
api_response: dict | list = data.get("response")
if isinstance(api_response, list):
return response_class.model_validate(data)
return response_class.model_validate(api_response)
raise ValueError(f"Response class not supported: {response_class}")
class CustomRapidParameters(RapidParameters):
@classmethod
def from_sig(cls, sig: Signature) -> Self:
out = cls()
for parameter in sig.parameters.values():
if (annot := find_annotation(parameter, Path)) is not None:
out.path_parameters.append(RapidParameter(parameter, annot))
if (annot := find_annotation(parameter, Query)) is not None:
out.query_parameters.append(RapidParameter(parameter, annot))
if (annot := find_annotation(parameter, Header)) is not None:
out.header_parameters.append(RapidParameter(parameter, annot))
if (annot := find_annotation(parameter, Body)) is not None:
out.body_parameters.append(RapidParameter(parameter, annot))
if len(out.body_parameters) > 0:
first_body_param = out.body_parameters[0]
if isinstance(first_body_param.annot, FileBody):
assert all(
map(lambda p: isinstance(p.annot, FileBody), out.body_parameters)
), "All body parameters must be of type FileBody"
elif isinstance(first_body_param.annot, FormBody):
assert all(
map(lambda p: isinstance(p.annot, FormBody), out.body_parameters)
), "All body parameters must be of type FormBody"
elif isinstance(first_body_param.annot, JsonBody):
assert len(out.body_parameters) == 1, "Only one JsonBody allowed"
elif isinstance(first_body_param.annot, Body) and not isinstance(
first_body_param.annot,
AttributeBody, # don't check the AttributeBody because there can be more than one
):
assert (
len(out.body_parameters) == 1
), "Only one Body (JsonBody, FormBody, PydanticBody, FileBody, PydanticXmlBody) allowed"
return out
def get_body(self, ba: BoundArguments) -> Tuple[str | None, Any]:
"""
Prepares the body of an HTTP request based on annotated parameters.
For parameters annotated with `AttributeBody`, collects them into a dictionary,
serializes them into JSON with custom type processing via `orjson`, and returns
the result as `("json", dict)`. Supports other body types like `FileBody`,
`FormBody`, `PydanticBody`, etc., with appropriate serialization.
Args:
ba (BoundArguments): Bound arguments of the function containing parameter values.
Returns:
Tuple[str | None, Any]: A tuple of (body_type, body_data), where body_type
indicates the content type (e.g., "json", "files") and body_data is the
serialized content, or (None, None) if no body is constructed.
"""
if len(self.body_parameters) > 0:
first_body_param = self.body_parameters[0]
if isinstance(first_body_param.annot, FileBody):
values = filter_none_values(
{p.get_name(): p.get_value(ba) for p in self.body_parameters}
)
if len(values) > 0:
return "files", values
elif isinstance(first_body_param.annot, FormBody):
values = {}
def update_values(p: RapidParameter[Body]) -> None:
if (value := p.get_value(ba)) is not None:
if isinstance(value, dict):
values.update(value)
else:
values[p.get_name()] = value
for param in self.body_parameters:
update_values(param)
if len(values) > 0:
return "data", values
elif isinstance(first_body_param.annot, PydanticXmlBody):
assert (
pydantic_xml is not None
), "pydantic-xml must be installed to use PydanticXmlBody"
if (value := first_body_param.get_value(ba)) is not None:
assert isinstance(value, pydantic_xml.BaseXmlModel)
return "content", value.to_xml()
elif isinstance(first_body_param.annot, PydanticBody):
if (value := first_body_param.get_value(ba)) is not None:
assert isinstance(value, BaseModel)
return "json", value.model_dump(
exclude_none=True, by_alias=True, mode="json"
)
elif isinstance(first_body_param.annot, JsonBody):
if (value := first_body_param.get_value(ba)) is not None:
assert isinstance(value, dict)
return "json", value
elif isinstance(first_body_param.annot, AttributeBody):
body: dict[str, Any] = {}
for param in self.body_parameters:
if (value := param.get_value(ba)) is not None:
param_name: str = param.get_name()
body[param_name] = value
if body:
return "json", orjson.loads(
orjson.dumps(body, default=orjson_default)
)
else:
if (value := first_body_param.get_value(ba)) is not None:
return "content", value
return None, None

View file

@ -0,0 +1,54 @@
from functools import partial, wraps
from inspect import signature
from typing import Any, Callable, Coroutine, Type
from httpx import AsyncClient, Response
from pydantic import TypeAdapter
from rapid_api_client.typing import BM, T
from .client import BaseController, CustomRapidParameters
def http(
method: str,
path: str,
response_class: Type[BM | str | bytes | Response] | TypeAdapter[T] = Response,
timeout: float | None = None,
) -> Callable[
[Callable], Callable[..., Coroutine[Any, Any, BM | str | bytes | Response | T]]
]:
def decorator(
func: Callable,
) -> Callable[..., Coroutine[Any, Any, BM | str | bytes | Response | T]]:
sig = signature(func)
rapid_parameters = CustomRapidParameters.from_sig(sig)
@wraps(func)
async def wrapper(
api: BaseController, *args, **kwargs
) -> BM | str | bytes | Response | T:
assert isinstance(
api, BaseController
), f"{api} should be an instance of BaseController"
assert isinstance(
api.client, AsyncClient
), f"{api.client} should be an instance of httpx.AsyncClient"
# noinspection PyProtectedMember
request = api._build_request(
sig, rapid_parameters, method, path, (api,) + args, kwargs, timeout
)
response = await api.client.send(request)
# noinspection PyProtectedMember
return api._handle_response(response, response_class=response_class)
return wrapper
return decorator
get = partial(http, "GET")
post = partial(http, "POST")
delete = partial(http, "DELETE")
put = partial(http, "PUT")
patch = partial(http, "PATCH")

View file

View file

@ -0,0 +1,7 @@
from pydantic import BaseModel
def orjson_default(obj):
if isinstance(obj, BaseModel):
return obj.model_dump(mode="json", exclude_none=True, by_alias=True)
return obj

7
tests/.env.test Normal file
View file

@ -0,0 +1,7 @@
REMNAWAVE_BASE_URL=
REMNAWAVE_TOKEN=
REMNAWAVE_ADMIN_USERNAME=
REMNAWAVE_ADMIN_PASSWORD=
REMNAWAVE_INBOUND_UUID=
REMNAWAVE_USER_UUID=
REMNAWAVE_SHORT_UUID=

0
tests/__init__.py Normal file
View file

45
tests/conftest.py Normal file
View file

@ -0,0 +1,45 @@
import os
import pytest
from dotenv import load_dotenv
from remnawave_api import RemnawaveSDK
load_dotenv()
REMNAWAVE_BASE_URL = os.getenv("REMNAWAVE_BASE_URL")
REMNAWAVE_TOKEN = os.getenv("REMNAWAVE_TOKEN")
REMNAWAVE_ADMIN_USERNAME = os.getenv("REMNAWAVE_ADMIN_USERNAME")
REMNAWAVE_ADMIN_PASSWORD = os.getenv("REMNAWAVE_ADMIN_PASSWORD")
REMNAWAVE_INBOUND_UUID = os.getenv("REMNAWAVE_INBOUND_UUID")
REMNAWAVE_USER_UUID = os.getenv("REMNAWAVE_USER_UUID")
REMNAWAVE_SHORT_UUID = os.getenv("REMNAWAVE_SHORT_UUID")
@pytest.fixture
async def remnawave() -> RemnawaveSDK:
assert REMNAWAVE_TOKEN
assert REMNAWAVE_BASE_URL
sdk = RemnawaveSDK(
base_url=REMNAWAVE_BASE_URL,
token=REMNAWAVE_TOKEN,
)
assert sdk.api_tokens_management is not None
assert sdk.auth
assert sdk.bandwidthstats is not None
assert sdk.hosts is not None
assert sdk.hosts_bulk_actions is not None
assert sdk.inbounds is not None
assert sdk.inbounds_bulk_actions is not None
assert sdk.keygen is not None
assert sdk.nodes is not None
assert sdk.subscription is not None
assert sdk.subscriptions_settings is not None
assert sdk.subscriptions_template is not None
assert sdk.system is not None
assert sdk.users is not None
assert sdk.users_bulk_actions is not None
assert sdk.users_stats is not None
assert sdk.xray_config is not None
return sdk

15
tests/test_auth.py Normal file
View file

@ -0,0 +1,15 @@
import pytest
from remnawave_api.models import LoginRequestDto, LoginResponseDto
from tests.conftest import REMNAWAVE_ADMIN_PASSWORD, REMNAWAVE_ADMIN_USERNAME
@pytest.mark.asyncio
async def test_auth(remnawave):
login = await remnawave.auth.login(
LoginRequestDto(
username=REMNAWAVE_ADMIN_USERNAME,
password=REMNAWAVE_ADMIN_PASSWORD,
)
)
assert isinstance(login, LoginResponseDto)

View file

@ -0,0 +1,10 @@
from remnawave_api.models import NodesUsageResponseDto
from tests.utils import generate_isoformat_range
async def test_bandwidthstats(remnawave):
start, end = generate_isoformat_range()
nodes_usage_by_range = await remnawave.bandwidthstats.get_nodes_usage_by_range(
start=start, end=end
)
assert isinstance(nodes_usage_by_range, NodesUsageResponseDto)

71
tests/test_hosts.py Normal file
View file

@ -0,0 +1,71 @@
import random
import pytest
from remnawave_api.enums import ALPN, Fingerprint
from remnawave_api.models import (
CreateHostRequestDto,
DeleteHostResponseDto,
HostResponseDto,
HostsResponseDto,
ReorderHostRequestDto,
ReorderHostResponseDto,
UpdateHostRequestDto,
)
from tests.conftest import REMNAWAVE_INBOUND_UUID
from tests.utils import generate_random_string
@pytest.mark.asyncio
async def test_hosts(remnawave):
all_hosts = await remnawave.hosts.get_all_hosts()
assert isinstance(all_hosts, HostsResponseDto)
random_ip: str = f"{random.randint(500, 800)}" + ".0.0.1"
random_port: int = random.randint(5000, 8000)
random_remark: str = generate_random_string()
create_host = await remnawave.hosts.create_host(
CreateHostRequestDto(
inbound_uuid=REMNAWAVE_INBOUND_UUID,
remark=random_remark,
address=random_ip,
port=random_port,
)
)
assert isinstance(create_host, HostResponseDto)
assert str(create_host.inbound_uuid) == REMNAWAVE_INBOUND_UUID
assert create_host.address == random_ip
assert create_host.port == random_port
assert create_host.remark == random_remark
string_uuid = str(create_host.uuid)
host = await remnawave.hosts.get_one_host(uuid=string_uuid)
assert isinstance(host, HostResponseDto)
assert host.uuid == create_host.uuid
reorder_host = await remnawave.hosts.reorder_hosts(
hosts=[ReorderHostRequestDto(view_position=1, uuid=string_uuid)]
)
assert isinstance(reorder_host, ReorderHostResponseDto)
assert reorder_host.is_updated is True
update_remark: str = "TEST_REMARK"
update_fingerprint: Fingerprint = Fingerprint.ANDROID
update_alpn: ALPN = ALPN.H3_H2_COMBINED
update_host = await remnawave.hosts.update_host(
UpdateHostRequestDto(
uuid=string_uuid,
remark=update_remark,
alpn=update_alpn,
fingerprint=update_fingerprint,
)
)
assert isinstance(update_host, HostResponseDto)
assert update_host.remark == update_remark
assert update_host.alpn == update_alpn
assert update_host.fingerprint == update_fingerprint
delete_host = await remnawave.hosts.delete_host(uuid=string_uuid)
assert isinstance(delete_host, DeleteHostResponseDto)
assert delete_host.is_deleted is True

12
tests/test_inbounds.py Normal file
View file

@ -0,0 +1,12 @@
import pytest
from remnawave_api.models import FullInboundsResponseDto, InboundsResponseDto
@pytest.mark.asyncio
async def test_inbounds(remnawave):
full_inbounds = await remnawave.inbounds.get_full_inbounds()
assert isinstance(full_inbounds, FullInboundsResponseDto)
inbounds = await remnawave.inbounds.get_inbounds()
assert isinstance(inbounds, InboundsResponseDto)

9
tests/test_keygen.py Normal file
View file

@ -0,0 +1,9 @@
import pytest
from remnawave_api.models import PubKeyResponseDto
@pytest.mark.asyncio
async def test_keygen(remnawave):
key = await remnawave.keygen.generate_key()
assert isinstance(key, PubKeyResponseDto)

50
tests/test_nodes.py Normal file
View file

@ -0,0 +1,50 @@
import random
import pytest
from remnawave_api.models import (
CreateNodeRequestDto,
DeleteNodeResponseDto,
NodeResponseDto,
NodesResponseDto,
ReorderNodeRequestDto,
UpdateNodeRequestDto,
)
from tests.utils import generate_random_string
@pytest.mark.asyncio
async def test_nodes(remnawave):
all_nodes = await remnawave.nodes.get_all_nodes()
assert isinstance(all_nodes, NodesResponseDto)
random_ip: str = f"{random.randint(500, 800)}" + ".0.0.1"
random_port: int = random.randint(5000, 8000)
random_name: str = generate_random_string()
create_node = await remnawave.nodes.create_node(
CreateNodeRequestDto(name=random_name, address=random_ip, port=random_port)
)
assert isinstance(create_node, NodeResponseDto)
string_uuid = str(create_node.uuid)
node = await remnawave.nodes.get_one_node(uuid=string_uuid)
assert isinstance(node, NodeResponseDto)
reorder_node = await remnawave.nodes.reorder_nodes(
nodes=[ReorderNodeRequestDto(view_position=1, uuid=string_uuid)]
)
assert isinstance(reorder_node, NodesResponseDto)
assert any(node.uuid == create_node.uuid for node in reorder_node.response)
update_name: str = "TEST_NAME"
update_node = await remnawave.nodes.update_node(
UpdateNodeRequestDto(uuid=string_uuid, name=update_name)
)
assert isinstance(update_node, NodeResponseDto)
assert update_node.uuid == create_node.uuid
assert update_node.name == update_name
delete_node = await remnawave.nodes.delete_node(uuid=string_uuid)
assert isinstance(delete_node, DeleteNodeResponseDto)
assert delete_node.is_deleted is True

View file

@ -0,0 +1,33 @@
import pytest
from remnawave_api.enums import ClientType
from remnawave_api.models import SubscriptionInfoResponseDto
from tests.conftest import REMNAWAVE_SHORT_UUID
@pytest.mark.asyncio
async def test_subscriptions(remnawave):
subscription_info = (
await remnawave.subscription.get_subscription_info_by_short_uuid(
short_uuid=REMNAWAVE_SHORT_UUID
)
)
assert isinstance(subscription_info, SubscriptionInfoResponseDto)
assert subscription_info.is_found is True
subscription = await remnawave.subscription.get_subscription(
short_uuid=REMNAWAVE_SHORT_UUID
)
assert isinstance(subscription, str)
subscription_by_client_type = (
await remnawave.subscription.get_subscription_by_client_type(
short_uuid=REMNAWAVE_SHORT_UUID, client_type=ClientType.SINGBOX
)
)
assert isinstance(subscription_by_client_type, str)
subscription_with_type = await remnawave.subscription.get_subscription_with_type(
short_uuid=REMNAWAVE_SHORT_UUID
)
assert isinstance(subscription_with_type, str)

View file

@ -0,0 +1,17 @@
import pytest
from remnawave_api.models import (
SubscriptionSettingsResponseDto,
UpdateSubscriptionSettingsRequestDto,
)
@pytest.mark.asyncio
async def test_subscriptions_settings(remnawave):
settings = await remnawave.subscriptions_settings.get_settings()
assert isinstance(settings, SubscriptionSettingsResponseDto)
update_settings = await remnawave.subscriptions_settings.update_settings(
UpdateSubscriptionSettingsRequestDto(uuid=settings.uuid)
)
assert isinstance(update_settings, SubscriptionSettingsResponseDto)

View file

@ -0,0 +1,19 @@
import pytest
from remnawave_api.enums import TemplateType
from remnawave_api.models import TemplateResponseDto, UpdateTemplateRequestDto
@pytest.mark.asyncio
async def test_subscriptions_template(remnawave):
template_type: TemplateType = TemplateType.SINGBOX
template = await remnawave.subscriptions_template.get_template(
template_type=template_type
)
assert isinstance(template, TemplateResponseDto)
update_template = await remnawave.subscriptions_template.update_template(
UpdateTemplateRequestDto(template_type=template_type)
)
assert isinstance(update_template, TemplateResponseDto)
assert update_template.template_type == template_type

19
tests/test_system.py Normal file
View file

@ -0,0 +1,19 @@
import pytest
from remnawave_api.models import (
BandwidthStatisticResponseDto,
NodesStatisticResponseDto,
StatisticResponseDto,
)
@pytest.mark.asyncio
async def test_system(remnawave):
stats = await remnawave.system.get_stats()
assert isinstance(stats, StatisticResponseDto)
bandwidth_stats = await remnawave.system.get_bandwidth_stats()
assert isinstance(bandwidth_stats, BandwidthStatisticResponseDto)
nodes_statistics = await remnawave.system.get_nodes_statistics()
assert isinstance(nodes_statistics, NodesStatisticResponseDto)

129
tests/test_users.py Normal file
View file

@ -0,0 +1,129 @@
import random
from datetime import datetime, timedelta
import pytest
import pytz
from remnawave_api.enums import ErrorCode, UserStatus
from remnawave_api.exceptions import ApiError
from remnawave_api.models import (
CreateUserRequestDto,
DeleteUserResponseDto,
EmailUserResponseDto,
TelegramUserResponseDto,
UpdateUserRequestDto,
UserResponseDto,
UsersResponseDto,
)
from tests.utils import generate_email, generate_random_string
@pytest.mark.asyncio
async def test_users(remnawave) -> None:
email: str = generate_email(length=8)
username: str = generate_random_string(length=8)
telegram_id: int = random.randint(100000000, 999999999)
expire_at: datetime = datetime.now(tz=pytz.UTC) + timedelta(days=7)
create_user = await remnawave.users.create_user(
CreateUserRequestDto(
username=username,
email=email,
telegram_id=telegram_id,
expire_at=expire_at,
activate_all_inbounds=True,
)
)
assert isinstance(create_user, UserResponseDto)
assert create_user.username == username
assert create_user.email == email
assert create_user.telegram_id == telegram_id
assert create_user.expire_at.isoformat(timespec="seconds") == expire_at.isoformat(
timespec="seconds"
)
string_uuid = str(create_user.uuid)
string_subscription_uuid = str(create_user.subscription_uuid)
string_telegram_id = str(create_user.telegram_id)
all_users = await remnawave.users.get_all_users_v2()
assert isinstance(all_users, UsersResponseDto)
user_uuid = await remnawave.users.get_user_by_uuid(uuid=string_uuid)
assert isinstance(user_uuid, UserResponseDto)
assert user_uuid.uuid == create_user.uuid
user_short_uuid = await remnawave.users.get_user_by_short_uuid(
short_uuid=user_uuid.short_uuid
)
assert isinstance(user_short_uuid, UserResponseDto)
assert user_short_uuid.uuid == create_user.uuid
user_subscription_uuid = await remnawave.users.get_user_by_subscription_uuid(
subscription_uuid=string_subscription_uuid
)
assert isinstance(user_subscription_uuid, UserResponseDto)
assert user_subscription_uuid.uuid == create_user.uuid
user_username = await remnawave.users.get_user_by_username(
username=user_uuid.username
)
assert isinstance(user_username, UserResponseDto)
assert user_username.uuid == create_user.uuid
user_telegram_id = await remnawave.users.get_users_by_telegram_id(
telegram_id=string_telegram_id
)
assert isinstance(user_telegram_id, TelegramUserResponseDto)
assert len(user_telegram_id.response) > 0
assert any(user.uuid == create_user.uuid for user in user_telegram_id.response)
user_email = await remnawave.users.get_users_by_email(email=user_uuid.email)
assert isinstance(user_email, EmailUserResponseDto)
assert len(user_email.response) > 0
assert any(user.uuid == create_user.uuid for user in user_email.response)
user_reset_traffic = await remnawave.users.reset_user_traffic(uuid=string_uuid)
assert isinstance(user_reset_traffic, UserResponseDto)
assert user_reset_traffic.uuid == create_user.uuid
assert user_reset_traffic.used_traffic_bytes == 0
try:
disable_user = await remnawave.users.disable_user(uuid=string_uuid)
assert isinstance(disable_user, UserResponseDto)
assert disable_user.uuid == create_user.uuid
assert disable_user.status == UserStatus.DISABLED
except ApiError as e:
assert e.error.code == ErrorCode.USER_ALREADY_DISABLED
try:
enable_user = await remnawave.users.enable_user(uuid=string_uuid)
assert isinstance(enable_user, UserResponseDto)
assert enable_user.uuid == create_user.uuid
assert enable_user.status == UserStatus.ACTIVE
except ApiError as e:
assert e.error.code == ErrorCode.USER_ALREADY_ENABLED
update_description: str = "TEST"
update_status: UserStatus = UserStatus.DISABLED
update_user = await remnawave.users.update_user(
UpdateUserRequestDto(
uuid=string_uuid, status=update_status, description=update_description
)
)
assert isinstance(update_user, UserResponseDto)
assert update_user.uuid == create_user.uuid
assert update_user.status == update_status
assert update_user.description == update_description
revoke_user_subscription = await remnawave.users.revoke_user_subscription(
uuid=string_uuid
)
assert isinstance(revoke_user_subscription, UserResponseDto)
assert revoke_user_subscription.uuid == create_user.uuid
assert revoke_user_subscription.short_uuid != create_user.short_uuid
delete_user = await remnawave.users.delete_user(uuid=string_uuid)
assert isinstance(delete_user, DeleteUserResponseDto)
assert delete_user.is_deleted is True

View file

@ -0,0 +1,25 @@
from datetime import datetime, timedelta
from typing import List
import pytest
import pytz
from remnawave_api.models import BulkResponseDto, UpdateUserFields
from tests.conftest import REMNAWAVE_USER_UUID
@pytest.mark.asyncio
async def test_users_bulk_actions(remnawave):
expire_at = datetime.now(tz=pytz.utc) + timedelta(days=14)
description = "TEST_DESCRIPTION"
uuids: List[str] = [REMNAWAVE_USER_UUID]
bulk_update_users = await remnawave.users_bulk_actions.bulk_update_users(
uuids=uuids,
fields=UpdateUserFields(
description=description,
expire_at=expire_at,
),
)
assert isinstance(bulk_update_users, BulkResponseDto)
assert bulk_update_users.affected_rows == len(uuids)

14
tests/test_users_stats.py Normal file
View file

@ -0,0 +1,14 @@
import pytest
from remnawave_api.models import UserUsageByRangeResponseDto
from tests.conftest import REMNAWAVE_USER_UUID
from tests.utils import generate_isoformat_range
@pytest.mark.asyncio
async def test_users_stats(remnawave):
start, end = generate_isoformat_range()
user_usage_by_range = await remnawave.users_stats.get_user_usage_by_range(
uuid=REMNAWAVE_USER_UUID, start=start, end=end
)
assert isinstance(user_usage_by_range, UserUsageByRangeResponseDto)

24
tests/utils.py Normal file
View file

@ -0,0 +1,24 @@
import random
import string
from datetime import datetime, timedelta
from typing import Tuple
def generate_random_string(length: int = 8, chars: str = string.ascii_letters) -> str:
return "".join(random.choices(chars, k=length))
def generate_password(length: int) -> str:
return generate_random_string(
length=length, chars=string.ascii_letters + string.digits
)
def generate_email(length: int, chars: str = string.ascii_letters) -> str:
return generate_random_string(length=length, chars=chars) + "@mail.com"
def generate_isoformat_range() -> Tuple[str, str]:
start = (datetime.now() - timedelta(days=7)).isoformat(timespec="seconds")
end = datetime.now().isoformat(timespec="seconds")
return start, end