From f47cda937851b9f3b1d75ad712a221bc4ba24979 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 21 Apr 2025 20:44:27 +0200 Subject: [PATCH] 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. --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 175 +++++ LICENSE | 21 + README.md | 77 ++ github/workflows/upload.yml | 54 ++ poetry.lock | 657 ++++++++++++++++++ pyproject.toml | 34 + pytest.ini | 2 + remnawave_api/.DS_Store | Bin 0 -> 6148 bytes remnawave_api/__init__.py | 90 +++ remnawave_api/controllers/__init__.py | 37 + .../controllers/api_tokens_management.py | 33 + remnawave_api/controllers/auth.py | 37 + remnawave_api/controllers/bandwidthstats.py | 17 + remnawave_api/controllers/hosts.py | 64 ++ .../controllers/hosts_bulk_actions.py | 60 ++ remnawave_api/controllers/inbounds.py | 18 + .../controllers/inbounds_bulk_actions.py | 50 ++ remnawave_api/controllers/keygen.py | 11 + remnawave_api/controllers/nodes.py | 95 +++ remnawave_api/controllers/subscription.py | 54 ++ .../controllers/subscriptions_settings.py | 29 + .../controllers/subscriptions_template.py | 31 + remnawave_api/controllers/system.py | 29 + remnawave_api/controllers/users.py | 131 ++++ .../controllers/users_bulk_actions.py | 89 +++ remnawave_api/controllers/users_stats.py | 21 + remnawave_api/controllers/xray_config.py | 23 + remnawave_api/enums/__init__.py | 18 + remnawave_api/enums/alpn.py | 10 + remnawave_api/enums/client_type.py | 10 + remnawave_api/enums/error_code.py | 80 +++ remnawave_api/enums/fingerprint.py | 13 + remnawave_api/enums/security_layer.py | 7 + remnawave_api/enums/template_type.py | 10 + remnawave_api/enums/users.py | 15 + remnawave_api/exceptions/__init__.py | 23 + remnawave_api/exceptions/general.py | 61 ++ remnawave_api/exceptions/handler.py | 130 ++++ remnawave_api/models/__init__.py | 167 +++++ remnawave_api/models/api_tokens_management.py | 10 + remnawave_api/models/auth.py | 26 + remnawave_api/models/bandwidthstats.py | 21 + remnawave_api/models/hosts.py | 91 +++ remnawave_api/models/hosts_bulk_actions.py | 31 + remnawave_api/models/inbounds.py | 38 + remnawave_api/models/inbounds_bulk_actions.py | 17 + remnawave_api/models/keygen.py | 5 + remnawave_api/models/nodes.py | 113 +++ remnawave_api/models/subscription.py | 26 + .../models/subscriptions_settings.py | 57 ++ .../models/subscriptions_template.py | 21 + remnawave_api/models/system.py | 70 ++ remnawave_api/models/users.py | 131 ++++ remnawave_api/models/users_bulk_actions.py | 52 ++ remnawave_api/models/users_stats.py | 17 + remnawave_api/models/xray_config.py | 5 + remnawave_api/rapid/__init__.py | 5 + remnawave_api/rapid/annotations.py | 5 + remnawave_api/rapid/client.py | 210 ++++++ remnawave_api/rapid/decorators.py | 54 ++ remnawave_api/utils/__init__.py | 0 remnawave_api/utils/serializer.py | 7 + tests/.env.test | 7 + tests/__init__.py | 0 tests/conftest.py | 45 ++ tests/test_auth.py | 15 + tests/test_bandwidthstats.py | 10 + tests/test_hosts.py | 71 ++ tests/test_inbounds.py | 12 + tests/test_keygen.py | 9 + tests/test_nodes.py | 50 ++ tests/test_subscription.py | 33 + tests/test_subscriptions_settings.py | 17 + tests/test_subscriptions_template.py | 19 + tests/test_system.py | 19 + tests/test_users.py | 129 ++++ tests/test_users_bulk_actions.py | 25 + tests/test_users_stats.py | 14 + tests/utils.py | 24 + 80 files changed, 3994 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 github/workflows/upload.yml create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 remnawave_api/.DS_Store create mode 100644 remnawave_api/__init__.py create mode 100644 remnawave_api/controllers/__init__.py create mode 100644 remnawave_api/controllers/api_tokens_management.py create mode 100644 remnawave_api/controllers/auth.py create mode 100644 remnawave_api/controllers/bandwidthstats.py create mode 100644 remnawave_api/controllers/hosts.py create mode 100644 remnawave_api/controllers/hosts_bulk_actions.py create mode 100644 remnawave_api/controllers/inbounds.py create mode 100644 remnawave_api/controllers/inbounds_bulk_actions.py create mode 100644 remnawave_api/controllers/keygen.py create mode 100644 remnawave_api/controllers/nodes.py create mode 100644 remnawave_api/controllers/subscription.py create mode 100644 remnawave_api/controllers/subscriptions_settings.py create mode 100644 remnawave_api/controllers/subscriptions_template.py create mode 100644 remnawave_api/controllers/system.py create mode 100644 remnawave_api/controllers/users.py create mode 100644 remnawave_api/controllers/users_bulk_actions.py create mode 100644 remnawave_api/controllers/users_stats.py create mode 100644 remnawave_api/controllers/xray_config.py create mode 100644 remnawave_api/enums/__init__.py create mode 100644 remnawave_api/enums/alpn.py create mode 100644 remnawave_api/enums/client_type.py create mode 100644 remnawave_api/enums/error_code.py create mode 100644 remnawave_api/enums/fingerprint.py create mode 100644 remnawave_api/enums/security_layer.py create mode 100644 remnawave_api/enums/template_type.py create mode 100644 remnawave_api/enums/users.py create mode 100644 remnawave_api/exceptions/__init__.py create mode 100644 remnawave_api/exceptions/general.py create mode 100644 remnawave_api/exceptions/handler.py create mode 100644 remnawave_api/models/__init__.py create mode 100644 remnawave_api/models/api_tokens_management.py create mode 100644 remnawave_api/models/auth.py create mode 100644 remnawave_api/models/bandwidthstats.py create mode 100644 remnawave_api/models/hosts.py create mode 100644 remnawave_api/models/hosts_bulk_actions.py create mode 100644 remnawave_api/models/inbounds.py create mode 100644 remnawave_api/models/inbounds_bulk_actions.py create mode 100644 remnawave_api/models/keygen.py create mode 100644 remnawave_api/models/nodes.py create mode 100644 remnawave_api/models/subscription.py create mode 100644 remnawave_api/models/subscriptions_settings.py create mode 100644 remnawave_api/models/subscriptions_template.py create mode 100644 remnawave_api/models/system.py create mode 100644 remnawave_api/models/users.py create mode 100644 remnawave_api/models/users_bulk_actions.py create mode 100644 remnawave_api/models/users_stats.py create mode 100644 remnawave_api/models/xray_config.py create mode 100644 remnawave_api/rapid/__init__.py create mode 100644 remnawave_api/rapid/annotations.py create mode 100644 remnawave_api/rapid/client.py create mode 100644 remnawave_api/rapid/decorators.py create mode 100644 remnawave_api/utils/__init__.py create mode 100644 remnawave_api/utils/serializer.py create mode 100644 tests/.env.test create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_auth.py create mode 100644 tests/test_bandwidthstats.py create mode 100644 tests/test_hosts.py create mode 100644 tests/test_inbounds.py create mode 100644 tests/test_keygen.py create mode 100644 tests/test_nodes.py create mode 100644 tests/test_subscription.py create mode 100644 tests/test_subscriptions_settings.py create mode 100644 tests/test_subscriptions_template.py create mode 100644 tests/test_system.py create mode 100644 tests/test_users.py create mode 100644 tests/test_users_bulk_actions.py create mode 100644 tests/test_users_stats.py create mode 100644 tests/utils.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0877001e06cca71da50559e938183e70f4f6e032 GIT binary patch literal 6148 zcmeHK!AiqG5S?wSO(;SS3VK`cTCk~D1TUf1A26Z^m736?!8BW%v^kVQ?)pRih~MMP z?zUK4uOjUZ%)Hs%nS^;8b~6CLI^(_rPzL}Dm9S)^StGPgx*{b#Q$b|%89wCT!vw-0 zUx_vo|B(UOI~zt2!T=K7yT$&c!ypy`u6_i=I7+i-^OKdzm7U$HRkdo?qxT|mFY~i; z+VMyCG`bNY4(7fe+(u#EtM6ZmB=e&r9La>JA7aSEU6k}i-Vx)ZpUPZMEm*c?_v%N} z>FH^^!JX#Wtih*FtL-%Sxzn1>Z0q3o3riOr;{KRHj=DrqXeq>o`|qp-`m*)6ECdZ)Unf zVe;+xd@jR*xeARn3>XHM87Qk^o$mju-~0dNBr`G$7zQ?q0aodH-42$d@79&#=&t3c sPpBjmS14Shpd+thjHRpi5LF7!b8-+}jfFy-K`}o9k_KZ81AofE7j>A5$^ZZW literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..229b697 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a2f2204 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..676ce48 --- /dev/null +++ b/README.md @@ -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). \ No newline at end of file diff --git a/github/workflows/upload.yml b/github/workflows/upload.yml new file mode 100644 index 0000000..b89f373 --- /dev/null +++ b/github/workflows/upload.yml @@ -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 }} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..ef67cc1 --- /dev/null +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f613f7d --- /dev/null +++ b/pyproject.toml @@ -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" \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..d280de0 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +asyncio_mode = auto \ No newline at end of file diff --git a/remnawave_api/.DS_Store b/remnawave_api/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..12347bb4a6e14f914e54e6eccd5edebf7529da63 GIT binary patch literal 6148 zcmeHK%}T>S5T3PvsYU2PL2nCQ3pOnl!Aq$11&ruHr6#6mFwK@EwTDv3U0=u-@p+v6 zDfY)+MaoRr{bpxp66Q;GHv<5yI}U7sIsjPMBo=qE`$gouY-_S(EM-Kd?va3i2(JMo zYu;oa28e2 zid}_Bkc7vbUt=X(<9UPyWU-e(3VJzM?pA5GjC3^4lV5lAV7KgbNIqMPt<# zY@`wc!~iiM8Ibz}H|QA5HL9%xJ5>T87SXH(by-WuSZ>fUm}|rt6sAKFbtp3>2Gika z=f=-5m}}JGz)bPM%#)d^P?&l;-k&RXV2(!Wi2-6@nSp{H*5&zsxw-yd4x$k;Kn!dZ z1FYC{dtFS)oULog$+MPYd&DM1`sEr`2zKT;R*F1|53wo1IW84M$6&4zEhzLOplG0u J82D2Lz5&QQ$>RV3 literal 0 HcmV?d00001 diff --git a/remnawave_api/__init__.py b/remnawave_api/__init__.py new file mode 100644 index 0000000..483e15c --- /dev/null +++ b/remnawave_api/__init__.py @@ -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 diff --git a/remnawave_api/controllers/__init__.py b/remnawave_api/controllers/__init__.py new file mode 100644 index 0000000..2c2d41b --- /dev/null +++ b/remnawave_api/controllers/__init__.py @@ -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", +] diff --git a/remnawave_api/controllers/api_tokens_management.py b/remnawave_api/controllers/api_tokens_management.py new file mode 100644 index 0000000..b219937 --- /dev/null +++ b/remnawave_api/controllers/api_tokens_management.py @@ -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""" + ... diff --git a/remnawave_api/controllers/auth.py b/remnawave_api/controllers/auth.py new file mode 100644 index 0000000..cb9a9a7 --- /dev/null +++ b/remnawave_api/controllers/auth.py @@ -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""" + ... diff --git a/remnawave_api/controllers/bandwidthstats.py b/remnawave_api/controllers/bandwidthstats.py new file mode 100644 index 0000000..e18ea2b --- /dev/null +++ b/remnawave_api/controllers/bandwidthstats.py @@ -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""" + ... diff --git a/remnawave_api/controllers/hosts.py b/remnawave_api/controllers/hosts.py new file mode 100644 index 0000000..b1d60bf --- /dev/null +++ b/remnawave_api/controllers/hosts.py @@ -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""" + ... diff --git a/remnawave_api/controllers/hosts_bulk_actions.py b/remnawave_api/controllers/hosts_bulk_actions.py new file mode 100644 index 0000000..64ad179 --- /dev/null +++ b/remnawave_api/controllers/hosts_bulk_actions.py @@ -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""" + ... diff --git a/remnawave_api/controllers/inbounds.py b/remnawave_api/controllers/inbounds.py new file mode 100644 index 0000000..a489e67 --- /dev/null +++ b/remnawave_api/controllers/inbounds.py @@ -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""" + ... diff --git a/remnawave_api/controllers/inbounds_bulk_actions.py b/remnawave_api/controllers/inbounds_bulk_actions.py new file mode 100644 index 0000000..3d3e316 --- /dev/null +++ b/remnawave_api/controllers/inbounds_bulk_actions.py @@ -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""" + ... diff --git a/remnawave_api/controllers/keygen.py b/remnawave_api/controllers/keygen.py new file mode 100644 index 0000000..2f65345 --- /dev/null +++ b/remnawave_api/controllers/keygen.py @@ -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""" + ... diff --git a/remnawave_api/controllers/nodes.py b/remnawave_api/controllers/nodes.py new file mode 100644 index 0000000..c2afbaf --- /dev/null +++ b/remnawave_api/controllers/nodes.py @@ -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""" + ... diff --git a/remnawave_api/controllers/subscription.py b/remnawave_api/controllers/subscription.py new file mode 100644 index 0000000..6b74eb8 --- /dev/null +++ b/remnawave_api/controllers/subscription.py @@ -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""" + ... diff --git a/remnawave_api/controllers/subscriptions_settings.py b/remnawave_api/controllers/subscriptions_settings.py new file mode 100644 index 0000000..986790c --- /dev/null +++ b/remnawave_api/controllers/subscriptions_settings.py @@ -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""" + ... diff --git a/remnawave_api/controllers/subscriptions_template.py b/remnawave_api/controllers/subscriptions_template.py new file mode 100644 index 0000000..2de1afa --- /dev/null +++ b/remnawave_api/controllers/subscriptions_template.py @@ -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""" + ... diff --git a/remnawave_api/controllers/system.py b/remnawave_api/controllers/system.py new file mode 100644 index 0000000..f41fc26 --- /dev/null +++ b/remnawave_api/controllers/system.py @@ -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""" + ... diff --git a/remnawave_api/controllers/users.py b/remnawave_api/controllers/users.py new file mode 100644 index 0000000..2868ab2 --- /dev/null +++ b/remnawave_api/controllers/users.py @@ -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""" + ... diff --git a/remnawave_api/controllers/users_bulk_actions.py b/remnawave_api/controllers/users_bulk_actions.py new file mode 100644 index 0000000..801ce4f --- /dev/null +++ b/remnawave_api/controllers/users_bulk_actions.py @@ -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""" + ... diff --git a/remnawave_api/controllers/users_stats.py b/remnawave_api/controllers/users_stats.py new file mode 100644 index 0000000..1ee17e3 --- /dev/null +++ b/remnawave_api/controllers/users_stats.py @@ -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""" + ... diff --git a/remnawave_api/controllers/xray_config.py b/remnawave_api/controllers/xray_config.py new file mode 100644 index 0000000..93309a4 --- /dev/null +++ b/remnawave_api/controllers/xray_config.py @@ -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""" + ... diff --git a/remnawave_api/enums/__init__.py b/remnawave_api/enums/__init__.py new file mode 100644 index 0000000..075eb42 --- /dev/null +++ b/remnawave_api/enums/__init__.py @@ -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", +] diff --git a/remnawave_api/enums/alpn.py b/remnawave_api/enums/alpn.py new file mode 100644 index 0000000..8f6c656 --- /dev/null +++ b/remnawave_api/enums/alpn.py @@ -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" diff --git a/remnawave_api/enums/client_type.py b/remnawave_api/enums/client_type.py new file mode 100644 index 0000000..1a0c16e --- /dev/null +++ b/remnawave_api/enums/client_type.py @@ -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" diff --git a/remnawave_api/enums/error_code.py b/remnawave_api/enums/error_code.py new file mode 100644 index 0000000..e532d21 --- /dev/null +++ b/remnawave_api/enums/error_code.py @@ -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" diff --git a/remnawave_api/enums/fingerprint.py b/remnawave_api/enums/fingerprint.py new file mode 100644 index 0000000..1ac4180 --- /dev/null +++ b/remnawave_api/enums/fingerprint.py @@ -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" diff --git a/remnawave_api/enums/security_layer.py b/remnawave_api/enums/security_layer.py new file mode 100644 index 0000000..21ba959 --- /dev/null +++ b/remnawave_api/enums/security_layer.py @@ -0,0 +1,7 @@ +from enum import StrEnum + + +class SecurityLayer(StrEnum): + DEFAULT = "DEFAULT" + TLS = "TLS" + NONE = "NONE" diff --git a/remnawave_api/enums/template_type.py b/remnawave_api/enums/template_type.py new file mode 100644 index 0000000..74222ba --- /dev/null +++ b/remnawave_api/enums/template_type.py @@ -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" diff --git a/remnawave_api/enums/users.py b/remnawave_api/enums/users.py new file mode 100644 index 0000000..a34ad56 --- /dev/null +++ b/remnawave_api/enums/users.py @@ -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" diff --git a/remnawave_api/exceptions/__init__.py b/remnawave_api/exceptions/__init__.py new file mode 100644 index 0000000..da87a70 --- /dev/null +++ b/remnawave_api/exceptions/__init__.py @@ -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", +] diff --git a/remnawave_api/exceptions/general.py b/remnawave_api/exceptions/general.py new file mode 100644 index 0000000..805d226 --- /dev/null +++ b/remnawave_api/exceptions/general.py @@ -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 diff --git a/remnawave_api/exceptions/handler.py b/remnawave_api/exceptions/handler.py new file mode 100644 index 0000000..65c9e78 --- /dev/null +++ b/remnawave_api/exceptions/handler.py @@ -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", + ), + ) diff --git a/remnawave_api/models/__init__.py b/remnawave_api/models/__init__.py new file mode 100644 index 0000000..0e91ad1 --- /dev/null +++ b/remnawave_api/models/__init__.py @@ -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", +] diff --git a/remnawave_api/models/api_tokens_management.py b/remnawave_api/models/api_tokens_management.py new file mode 100644 index 0000000..e1fc8c7 --- /dev/null +++ b/remnawave_api/models/api_tokens_management.py @@ -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" + ) diff --git a/remnawave_api/models/auth.py b/remnawave_api/models/auth.py new file mode 100644 index 0000000..84d5e62 --- /dev/null +++ b/remnawave_api/models/auth.py @@ -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") diff --git a/remnawave_api/models/bandwidthstats.py b/remnawave_api/models/bandwidthstats.py new file mode 100644 index 0000000..a2aaad8 --- /dev/null +++ b/remnawave_api/models/bandwidthstats.py @@ -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] diff --git a/remnawave_api/models/hosts.py b/remnawave_api/models/hosts.py new file mode 100644 index 0000000..d1844ad --- /dev/null +++ b/remnawave_api/models/hosts.py @@ -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") diff --git a/remnawave_api/models/hosts_bulk_actions.py b/remnawave_api/models/hosts_bulk_actions.py new file mode 100644 index 0000000..d71450f --- /dev/null +++ b/remnawave_api/models/hosts_bulk_actions.py @@ -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] diff --git a/remnawave_api/models/inbounds.py b/remnawave_api/models/inbounds.py new file mode 100644 index 0000000..fd79025 --- /dev/null +++ b/remnawave_api/models/inbounds.py @@ -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] diff --git a/remnawave_api/models/inbounds_bulk_actions.py b/remnawave_api/models/inbounds_bulk_actions.py new file mode 100644 index 0000000..f2c8dba --- /dev/null +++ b/remnawave_api/models/inbounds_bulk_actions.py @@ -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") diff --git a/remnawave_api/models/keygen.py b/remnawave_api/models/keygen.py new file mode 100644 index 0000000..798503a --- /dev/null +++ b/remnawave_api/models/keygen.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel, Field + + +class PubKeyResponseDto(BaseModel): + pub_key: str = Field(alias="pubKey") diff --git a/remnawave_api/models/nodes.py b/remnawave_api/models/nodes.py new file mode 100644 index 0000000..8dd937d --- /dev/null +++ b/remnawave_api/models/nodes.py @@ -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") diff --git a/remnawave_api/models/subscription.py b/remnawave_api/models/subscription.py new file mode 100644 index 0000000..52a600b --- /dev/null +++ b/remnawave_api/models/subscription.py @@ -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") diff --git a/remnawave_api/models/subscriptions_settings.py b/remnawave_api/models/subscriptions_settings.py new file mode 100644 index 0000000..c95d996 --- /dev/null +++ b/remnawave_api/models/subscriptions_settings.py @@ -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" + ) diff --git a/remnawave_api/models/subscriptions_template.py b/remnawave_api/models/subscriptions_template.py new file mode 100644 index 0000000..fb14fa8 --- /dev/null +++ b/remnawave_api/models/subscriptions_template.py @@ -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" + ) diff --git a/remnawave_api/models/system.py b/remnawave_api/models/system.py new file mode 100644 index 0000000..2a6bbd2 --- /dev/null +++ b/remnawave_api/models/system.py @@ -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") diff --git a/remnawave_api/models/users.py b/remnawave_api/models/users.py new file mode 100644 index 0000000..2f2bde4 --- /dev/null +++ b/remnawave_api/models/users.py @@ -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") diff --git a/remnawave_api/models/users_bulk_actions.py b/remnawave_api/models/users_bulk_actions.py new file mode 100644 index 0000000..c59c17a --- /dev/null +++ b/remnawave_api/models/users_bulk_actions.py @@ -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") diff --git a/remnawave_api/models/users_stats.py b/remnawave_api/models/users_stats.py new file mode 100644 index 0000000..e64f077 --- /dev/null +++ b/remnawave_api/models/users_stats.py @@ -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] diff --git a/remnawave_api/models/xray_config.py b/remnawave_api/models/xray_config.py new file mode 100644 index 0000000..a261929 --- /dev/null +++ b/remnawave_api/models/xray_config.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class ConfigResponseDto(BaseModel): + config: dict diff --git a/remnawave_api/rapid/__init__.py b/remnawave_api/rapid/__init__.py new file mode 100644 index 0000000..adf8fa2 --- /dev/null +++ b/remnawave_api/rapid/__init__.py @@ -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"] diff --git a/remnawave_api/rapid/annotations.py b/remnawave_api/rapid/annotations.py new file mode 100644 index 0000000..5a087af --- /dev/null +++ b/remnawave_api/rapid/annotations.py @@ -0,0 +1,5 @@ +from rapid_api_client import Body + + +class AttributeBody(Body): + pass diff --git a/remnawave_api/rapid/client.py b/remnawave_api/rapid/client.py new file mode 100644 index 0000000..ad081b3 --- /dev/null +++ b/remnawave_api/rapid/client.py @@ -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 diff --git a/remnawave_api/rapid/decorators.py b/remnawave_api/rapid/decorators.py new file mode 100644 index 0000000..dfa7b9e --- /dev/null +++ b/remnawave_api/rapid/decorators.py @@ -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") diff --git a/remnawave_api/utils/__init__.py b/remnawave_api/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/remnawave_api/utils/serializer.py b/remnawave_api/utils/serializer.py new file mode 100644 index 0000000..ea46189 --- /dev/null +++ b/remnawave_api/utils/serializer.py @@ -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 diff --git a/tests/.env.test b/tests/.env.test new file mode 100644 index 0000000..8b7fef8 --- /dev/null +++ b/tests/.env.test @@ -0,0 +1,7 @@ +REMNAWAVE_BASE_URL= +REMNAWAVE_TOKEN= +REMNAWAVE_ADMIN_USERNAME= +REMNAWAVE_ADMIN_PASSWORD= +REMNAWAVE_INBOUND_UUID= +REMNAWAVE_USER_UUID= +REMNAWAVE_SHORT_UUID= \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..7f1f09f --- /dev/null +++ b/tests/conftest.py @@ -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 diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..e477400 --- /dev/null +++ b/tests/test_auth.py @@ -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) diff --git a/tests/test_bandwidthstats.py b/tests/test_bandwidthstats.py new file mode 100644 index 0000000..20c26ac --- /dev/null +++ b/tests/test_bandwidthstats.py @@ -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) diff --git a/tests/test_hosts.py b/tests/test_hosts.py new file mode 100644 index 0000000..c9acb76 --- /dev/null +++ b/tests/test_hosts.py @@ -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 diff --git a/tests/test_inbounds.py b/tests/test_inbounds.py new file mode 100644 index 0000000..d0817d5 --- /dev/null +++ b/tests/test_inbounds.py @@ -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) diff --git a/tests/test_keygen.py b/tests/test_keygen.py new file mode 100644 index 0000000..6d25265 --- /dev/null +++ b/tests/test_keygen.py @@ -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) diff --git a/tests/test_nodes.py b/tests/test_nodes.py new file mode 100644 index 0000000..13ac4a7 --- /dev/null +++ b/tests/test_nodes.py @@ -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 diff --git a/tests/test_subscription.py b/tests/test_subscription.py new file mode 100644 index 0000000..1b29f14 --- /dev/null +++ b/tests/test_subscription.py @@ -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) diff --git a/tests/test_subscriptions_settings.py b/tests/test_subscriptions_settings.py new file mode 100644 index 0000000..80af449 --- /dev/null +++ b/tests/test_subscriptions_settings.py @@ -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) diff --git a/tests/test_subscriptions_template.py b/tests/test_subscriptions_template.py new file mode 100644 index 0000000..62df0db --- /dev/null +++ b/tests/test_subscriptions_template.py @@ -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 diff --git a/tests/test_system.py b/tests/test_system.py new file mode 100644 index 0000000..819d6b0 --- /dev/null +++ b/tests/test_system.py @@ -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) diff --git a/tests/test_users.py b/tests/test_users.py new file mode 100644 index 0000000..ad3fe46 --- /dev/null +++ b/tests/test_users.py @@ -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 diff --git a/tests/test_users_bulk_actions.py b/tests/test_users_bulk_actions.py new file mode 100644 index 0000000..945de14 --- /dev/null +++ b/tests/test_users_bulk_actions.py @@ -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) diff --git a/tests/test_users_stats.py b/tests/test_users_stats.py new file mode 100644 index 0000000..64d4994 --- /dev/null +++ b/tests/test_users_stats.py @@ -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) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..11e7f13 --- /dev/null +++ b/tests/utils.py @@ -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