mirror of
https://github.com/remnawave/python-sdk.git
synced 2026-08-04 06:35:09 +00:00
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:
commit
f47cda9378
80 changed files with 3994 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
175
.gitignore
vendored
Normal file
175
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
77
README.md
Normal 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
|
||||
|
||||
Here’s 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).
|
||||
54
github/workflows/upload.yml
Normal file
54
github/workflows/upload.yml
Normal 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
657
poetry.lock
generated
Normal 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
34
pyproject.toml
Normal 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
2
pytest.ini
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
[pytest]
|
||||
asyncio_mode = auto
|
||||
BIN
remnawave_api/.DS_Store
vendored
Normal file
BIN
remnawave_api/.DS_Store
vendored
Normal file
Binary file not shown.
90
remnawave_api/__init__.py
Normal file
90
remnawave_api/__init__.py
Normal 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
|
||||
37
remnawave_api/controllers/__init__.py
Normal file
37
remnawave_api/controllers/__init__.py
Normal 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",
|
||||
]
|
||||
33
remnawave_api/controllers/api_tokens_management.py
Normal file
33
remnawave_api/controllers/api_tokens_management.py
Normal 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"""
|
||||
...
|
||||
37
remnawave_api/controllers/auth.py
Normal file
37
remnawave_api/controllers/auth.py
Normal 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"""
|
||||
...
|
||||
17
remnawave_api/controllers/bandwidthstats.py
Normal file
17
remnawave_api/controllers/bandwidthstats.py
Normal 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"""
|
||||
...
|
||||
64
remnawave_api/controllers/hosts.py
Normal file
64
remnawave_api/controllers/hosts.py
Normal 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"""
|
||||
...
|
||||
60
remnawave_api/controllers/hosts_bulk_actions.py
Normal file
60
remnawave_api/controllers/hosts_bulk_actions.py
Normal 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"""
|
||||
...
|
||||
18
remnawave_api/controllers/inbounds.py
Normal file
18
remnawave_api/controllers/inbounds.py
Normal 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"""
|
||||
...
|
||||
50
remnawave_api/controllers/inbounds_bulk_actions.py
Normal file
50
remnawave_api/controllers/inbounds_bulk_actions.py
Normal 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"""
|
||||
...
|
||||
11
remnawave_api/controllers/keygen.py
Normal file
11
remnawave_api/controllers/keygen.py
Normal 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"""
|
||||
...
|
||||
95
remnawave_api/controllers/nodes.py
Normal file
95
remnawave_api/controllers/nodes.py
Normal 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"""
|
||||
...
|
||||
54
remnawave_api/controllers/subscription.py
Normal file
54
remnawave_api/controllers/subscription.py
Normal 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"""
|
||||
...
|
||||
29
remnawave_api/controllers/subscriptions_settings.py
Normal file
29
remnawave_api/controllers/subscriptions_settings.py
Normal 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"""
|
||||
...
|
||||
31
remnawave_api/controllers/subscriptions_template.py
Normal file
31
remnawave_api/controllers/subscriptions_template.py
Normal 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"""
|
||||
...
|
||||
29
remnawave_api/controllers/system.py
Normal file
29
remnawave_api/controllers/system.py
Normal 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"""
|
||||
...
|
||||
131
remnawave_api/controllers/users.py
Normal file
131
remnawave_api/controllers/users.py
Normal 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"""
|
||||
...
|
||||
89
remnawave_api/controllers/users_bulk_actions.py
Normal file
89
remnawave_api/controllers/users_bulk_actions.py
Normal 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"""
|
||||
...
|
||||
21
remnawave_api/controllers/users_stats.py
Normal file
21
remnawave_api/controllers/users_stats.py
Normal 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"""
|
||||
...
|
||||
23
remnawave_api/controllers/xray_config.py
Normal file
23
remnawave_api/controllers/xray_config.py
Normal 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"""
|
||||
...
|
||||
18
remnawave_api/enums/__init__.py
Normal file
18
remnawave_api/enums/__init__.py
Normal 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",
|
||||
]
|
||||
10
remnawave_api/enums/alpn.py
Normal file
10
remnawave_api/enums/alpn.py
Normal 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"
|
||||
10
remnawave_api/enums/client_type.py
Normal file
10
remnawave_api/enums/client_type.py
Normal 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"
|
||||
80
remnawave_api/enums/error_code.py
Normal file
80
remnawave_api/enums/error_code.py
Normal 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"
|
||||
13
remnawave_api/enums/fingerprint.py
Normal file
13
remnawave_api/enums/fingerprint.py
Normal 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"
|
||||
7
remnawave_api/enums/security_layer.py
Normal file
7
remnawave_api/enums/security_layer.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from enum import StrEnum
|
||||
|
||||
|
||||
class SecurityLayer(StrEnum):
|
||||
DEFAULT = "DEFAULT"
|
||||
TLS = "TLS"
|
||||
NONE = "NONE"
|
||||
10
remnawave_api/enums/template_type.py
Normal file
10
remnawave_api/enums/template_type.py
Normal 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"
|
||||
15
remnawave_api/enums/users.py
Normal file
15
remnawave_api/enums/users.py
Normal 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"
|
||||
23
remnawave_api/exceptions/__init__.py
Normal file
23
remnawave_api/exceptions/__init__.py
Normal 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",
|
||||
]
|
||||
61
remnawave_api/exceptions/general.py
Normal file
61
remnawave_api/exceptions/general.py
Normal 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
|
||||
130
remnawave_api/exceptions/handler.py
Normal file
130
remnawave_api/exceptions/handler.py
Normal 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",
|
||||
),
|
||||
)
|
||||
167
remnawave_api/models/__init__.py
Normal file
167
remnawave_api/models/__init__.py
Normal 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",
|
||||
]
|
||||
10
remnawave_api/models/api_tokens_management.py
Normal file
10
remnawave_api/models/api_tokens_management.py
Normal 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"
|
||||
)
|
||||
26
remnawave_api/models/auth.py
Normal file
26
remnawave_api/models/auth.py
Normal 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")
|
||||
21
remnawave_api/models/bandwidthstats.py
Normal file
21
remnawave_api/models/bandwidthstats.py
Normal 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]
|
||||
91
remnawave_api/models/hosts.py
Normal file
91
remnawave_api/models/hosts.py
Normal 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")
|
||||
31
remnawave_api/models/hosts_bulk_actions.py
Normal file
31
remnawave_api/models/hosts_bulk_actions.py
Normal 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]
|
||||
38
remnawave_api/models/inbounds.py
Normal file
38
remnawave_api/models/inbounds.py
Normal 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]
|
||||
17
remnawave_api/models/inbounds_bulk_actions.py
Normal file
17
remnawave_api/models/inbounds_bulk_actions.py
Normal 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")
|
||||
5
remnawave_api/models/keygen.py
Normal file
5
remnawave_api/models/keygen.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PubKeyResponseDto(BaseModel):
|
||||
pub_key: str = Field(alias="pubKey")
|
||||
113
remnawave_api/models/nodes.py
Normal file
113
remnawave_api/models/nodes.py
Normal 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")
|
||||
26
remnawave_api/models/subscription.py
Normal file
26
remnawave_api/models/subscription.py
Normal 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")
|
||||
57
remnawave_api/models/subscriptions_settings.py
Normal file
57
remnawave_api/models/subscriptions_settings.py
Normal 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"
|
||||
)
|
||||
21
remnawave_api/models/subscriptions_template.py
Normal file
21
remnawave_api/models/subscriptions_template.py
Normal 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"
|
||||
)
|
||||
70
remnawave_api/models/system.py
Normal file
70
remnawave_api/models/system.py
Normal 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")
|
||||
131
remnawave_api/models/users.py
Normal file
131
remnawave_api/models/users.py
Normal 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")
|
||||
52
remnawave_api/models/users_bulk_actions.py
Normal file
52
remnawave_api/models/users_bulk_actions.py
Normal 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")
|
||||
17
remnawave_api/models/users_stats.py
Normal file
17
remnawave_api/models/users_stats.py
Normal 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]
|
||||
5
remnawave_api/models/xray_config.py
Normal file
5
remnawave_api/models/xray_config.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ConfigResponseDto(BaseModel):
|
||||
config: dict
|
||||
5
remnawave_api/rapid/__init__.py
Normal file
5
remnawave_api/rapid/__init__.py
Normal 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"]
|
||||
5
remnawave_api/rapid/annotations.py
Normal file
5
remnawave_api/rapid/annotations.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from rapid_api_client import Body
|
||||
|
||||
|
||||
class AttributeBody(Body):
|
||||
pass
|
||||
210
remnawave_api/rapid/client.py
Normal file
210
remnawave_api/rapid/client.py
Normal 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
|
||||
54
remnawave_api/rapid/decorators.py
Normal file
54
remnawave_api/rapid/decorators.py
Normal 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")
|
||||
0
remnawave_api/utils/__init__.py
Normal file
0
remnawave_api/utils/__init__.py
Normal file
7
remnawave_api/utils/serializer.py
Normal file
7
remnawave_api/utils/serializer.py
Normal 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
7
tests/.env.test
Normal 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
0
tests/__init__.py
Normal file
45
tests/conftest.py
Normal file
45
tests/conftest.py
Normal 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
15
tests/test_auth.py
Normal 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)
|
||||
10
tests/test_bandwidthstats.py
Normal file
10
tests/test_bandwidthstats.py
Normal 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
71
tests/test_hosts.py
Normal 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
12
tests/test_inbounds.py
Normal 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
9
tests/test_keygen.py
Normal 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
50
tests/test_nodes.py
Normal 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
|
||||
33
tests/test_subscription.py
Normal file
33
tests/test_subscription.py
Normal 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)
|
||||
17
tests/test_subscriptions_settings.py
Normal file
17
tests/test_subscriptions_settings.py
Normal 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)
|
||||
19
tests/test_subscriptions_template.py
Normal file
19
tests/test_subscriptions_template.py
Normal 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
19
tests/test_system.py
Normal 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
129
tests/test_users.py
Normal 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
|
||||
25
tests/test_users_bulk_actions.py
Normal file
25
tests/test_users_bulk_actions.py
Normal 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
14
tests/test_users_stats.py
Normal 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
24
tests/utils.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue