commit 346ffd4255322a853e44f84bcfbb7f3f8ac700df Author: Dmitriy Smirnov Date: Fri Mar 21 17:47:09 2025 +0300 1.0.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..71ca4d1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: end-of-file-fixer + - id: file-contents-sorter + files: .*mapping.yml + args: + - --unique + - id: no-commit-to-branch + name: no-commit-to-branch (main, master, dev_*) + args: + - --pattern + - dev_.* + - repo: https://github.com/pycqa/flake8 + rev: 7.1.2 + hooks: + - id: flake8 + name: flake8 (1/2) + files: src/dynamix_sdk/types\.py|.*__init__\.py|.*_api\.py + args: + - --extend-ignore=F401,F403,F405 + - id: flake8 + name: flake8 (2/2) + exclude: src/dynamix_sdk/types\.py|.*__init__\.py|.*_api\.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d1ea0a0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Список изменений в версии 1.0.0 + +## Добавлено + +### Глобально +| Идентификатор
задачи | Описание | +| --- | --- | +| BPYS-3 | Добавлена возможность авторизации через DECS3O. | +| BPYS-7 | Добавлена возможность авторизации через BVS. | + +### Функциональный интерфейс +| Идентификатор
задачи | Описание | +| --- | --- | +| BPYS-5 | Добавлена функция API `/system/usermanager/whoami`. | +| BPYS-4 | Добавлена функция API `/cloudapi/compute/get`. | +| BPYS-6 | Добавлена функция API `/cloudapi/kvmx86/create`. | +| BPYS-12 | Добавлена функция API `/cloudapi/compute/delete`. | +| BPYS-21 | Добавлена функция API `/cloudapi/user/get`. | +| BPYS-15 | Добавлена функция API `/cloudapi/compute/update`. | +| BPYS-29 | Добавлена функция API `/cloudapi/account/updateUser`. | +| BPYS-33 | Добавлена функция API `/cloudapi/account/enable`. | +| BPYS-32 | Добавлена функция API `/cloudapi/account/disable`. | +| BPYS-45 | Добавлена функция API `/cloudapi/rg/list`. | +| BPYS-20 | Добавлена функция API `/cloudapi/account/addUser`. | +| BPYS-30 | Добавлена функция API `/cloudapi/account/deleteUser`. | +| BPYS-51 | Добавлена функция API `/cloudapi/rg/get`. | +| BPYS-52 | Добавлена функция API `/cloudapi/rg/create`. | +| BPYS-35 | Добавлена функция API `/cloudapi/compute/list`. | diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..cd801aa --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +dev: + pip install -e . + pip install -r requirements-dev.txt + pre-commit install diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef70803 --- /dev/null +++ b/README.md @@ -0,0 +1,536 @@ +# Dynamix Python SDK + +- [Описание](#описание) +- [Системные требования](#системные-требования) + - [Соответствие версий платформы версиям SDK](#соответствие-версий-платформы-версиям-sdk) + - [Зависимости](#зависимости) +- [Установка](#установка) +- [Использование](#использование) + - [Конфигурация](#конфигурация) + - [Функциональный интерфейс](#функциональный-интерфейс) + - [Вызов функции](#вызов-функции) + - [Результат выполнения функции](#результат-выполнения-функции) + - [Соответствие названий](#соответствие-названий) + - [Типы данных SDK](#типы-данных-sdk) + - [Ошибки и исключения (exceptions)](#ошибки-и-исключения-exceptions) + - [Ошибки валидации данных](#ошибки-валидации-данных) + - [Ошибки HTTP](#ошибки-http) +- [Доступный функционал](#доступный-функционал) + - [Способы авторизации](#способы-авторизации) + - [Функции API](#функции-api) + - [Cloudapi](#cloudapi) + - [System](#system) + +## Описание + +**Dynamix Python SDK** предоставляет удобный интерфейс для интеграции взаимодействия с API **Dynamix Enterprise** в программный код на языке Python. + +**Примечание:** проект находится в стадии активной разработки, [доступный функционал](#доступный-функционал) будет расширяться с каждой новой версией. + +## Системные требования + +### Соответствие версий платформы версиям SDK + +| Версия платформы | Версия SDK | +| --- | --- | +| 4.2.0 | 1.0.x | + +### Зависимости + +Зависимости указаны в конфигурационном файле проекта [pyproject.toml](/pyproject.toml): + - версия Python: раздел `[project]`, ключ `requires-python` + - библиотеки: раздел `[project]`, ключ `dependencies` + +**Важно:** использование более поздних версий Python и/или библиотек допустимо, но работоспособность не гарантируется, так как зависит от обратной совместимости с указанными версиями. + +## Установка + +Установка с помощью системы управления пакетами Python (pip), где `version` - тег с номером версии SDK (см. доступные теги в репозитории): +```sh +pip install git+https://repository.basistech.ru/BASIS/dynamix-python-sdk.git@version +``` + +Посмотреть версию установленного пакета: +```sh +pip show dynamix-sdk +``` + +## Использование + +### Конфигурация + +Для работы с SDK необходимо создать экземпляр класса **Dynamix**, конструктор которого принимает на вход параметры конфигурации SDK. + +
Авторизация с JWT + +```python +from dynamix_sdk import Dynamix + + +JWT = '...' + +dx = Dynamix( + url='https://...', + auth=JWT, +) +``` + +
+ +
Авторизация через DECS3O + +```python +from dynamix_sdk import Dynamix, DECS3OAuth + + +dx = Dynamix( + url='https://...', + auth=DECS3OAuth( + url='https://...', + client_id='...', + client_secret='...', + ), +) +``` + +
+ +
Авторизация через BVS + +```python +from dynamix_sdk import Dynamix, BVSAuth + + +dx = Dynamix( + url='https://...', + auth=BVSAuth( + url='https://...', + domain='...', + client_id='...', + client_secret='...', + username='...', + password='...', + ), +) +``` + +
+ +
Настройка повторных попыток при 503 + +По умолчанию SDK при получении кода ответа 503 производит 10 повторных попыток с интервалом 5 секунд. Данное поведение можно скорректировать: + +```python +from dynamix_sdk import Dynamix, DECS3OAuth, BVSAuth + +# Для HTTP-запросов к Dynamix +Dynamix( + ..., + http503_attempts = 10, # количество повторных попыток при 503 + http503_attempts_interval = 5, # интервал в секундах между повторными попытками +) + +# Для HTTP-запросов к DECS3O +DECS3OAuth( + ..., + http503_attempts = 10, # количество повторных попыток при 503 + http503_attempts_interval = 5, # интервал в секундах между повторными попытками +) + +# Для HTTP-запросов к BVS +BVSAuth( + ..., + http503_attempts = 10, # количество повторных попыток при 503 + http503_attempts_interval = 5, # интервал в секундах между повторными попытками +) + +``` + +
+ +
Отключение проверки сертификата SSL + +**Важно:** отключение проверки сертификата SSL даёт возможность произвести атаку типа MitM, поэтому пользоваться данной возможостью допустимо только в защищённой среде. + +```python +import urllib3 + +from dynamix_sdk import Dynamix, DECS3OAuth, BVSAuth + + +urllib3.disable_warnings() # отключение вывода предупреждений безопасности SSL + + +# Для HTTP-запросов к Dynamix +Dynamix( + ..., + verify_ssl=False, # отключение проверки сертификата SSL +) + +# Для HTTP-запросов к DECS3O +DECS3OAuth( + ..., + verify_ssl=False, # отключение проверки сертификата SSL +) + +# Для HTTP-запросов к BVS +BVSAuth( + ..., + verify_ssl=False, # отключение проверки сертификата SSL +) + +``` + +
+ +### Функциональный интерфейс + +Функциональный интерфейс предоставляет функции, соответствующие функциям API платформы. + +#### Вызов функции + +
Выбор функции, передача параметров и сохранение результата + +![](/demo/select_api_function_and_passing_params_and_save_result.gif) + +
+ +
Передача вложенных параметров + +```python +from dynamix_sdk import Dynamix, types + + +dx = Dynamix(...) + +data_disk_1 = types.DiskAPIParamsNM(name='data_disk_1', size=10) + +# /cloudapi/kvmx86/create +new_vm_id = dx.api.cloudapi.kvmx86.create( + rg_id=123, + name='new_vm', + cpu=1, + ram=1024, + image_id=456, + data_disks=[data_disk_1], +) +``` + +
+ +
Передача параметров перечисляемого типа (enum) + +```python +from dynamix_sdk import Dynamix, types + + +dx = Dynamix(...) + +# /cloudapi/kvmx86/create +new_vm_id = dx.api.cloudapi.kvmx86.create( + rg_id=123, + name='new_vm', + cpu=1, + ram=1024, + image_id=456, + chipset=types.Chipset.Q35, # enum + numa_affinity=types.NumaAffinity.none, # enum +) + +``` + +
+ +#### Результат выполнения функции + +Тип данных, возвращаемый функцией API SDK зависит от типа данных, возвращаемого соответствующей функцией API платформы. + +Если функция API платформы возвращает структуру данных в виде набора пар ключ/значение (с заранее известными ключами), то функция API SDK возвращает модель Pydantic, которая описывает эту структуру. + +
Модель + +```python +import pydantic +from dynamix_sdk import Dynamix + + +dx = Dynamix(...) + +# /cloudapi/compute/get +get_result = dx.api.cloudapi.compute.get(compute_id=1) + +print(type(get_result)) # + +print(isinstance(get_result, pydantic.BaseModel)) # True + +``` + +
+ +
Обращение к полю модели + +```python +from dynamix_sdk import Dynamix + + +dx = Dynamix(...) + +# /cloudapi/compute/get +vm_name = dx.api.cloudapi.compute.get(compute_id=1).name + +``` + +
+ +Вложенные структуры данных также представлены в виде моделей Pydantic: + +
Вложенная модель + +```python +import pydantic +from dynamix_sdk import Dynamix + + +dx = Dynamix(...) + +# /cloudapi/compute/get +vm_disk1 = dx.api.cloudapi.compute.get(compute_id=1).disks[0] + +print(type(vm_disk1)) # + +print(isinstance(vm_disk1, pydantic.BaseModel)) # True + +``` + +
+ +
Обращение к полю вложенной модели + +```python +from dynamix_sdk import Dynamix + + +dx = Dynamix(...) + +# /cloudapi/compute/get +vm_disk1_id = dx.api.cloudapi.compute.get(compute_id=1).disks[0].id + +``` + +
+ +Некоторые поля моделей автоматически генерируются на основе полученных данных от API платформы.
+Например, при наличии в модели поля, содержащего временную метку (timestamp), для удобства генерируется поле, содержащее дату и время в виде `datetime`. + +
Генерируемое поле + +```python +from dynamix_sdk import Dynamix + + +dx = Dynamix(...) + +# /cloudapi/compute/get +get_result = dx.api.cloudapi.compute.get(compute_id=1) + +# Поле, возвращаемое платформой +print(type(get_result.created_timestamp)) # + +# Генерируемое поле +print(type(get_result.created_datetime)) # + +``` + +
+ +Если функция API платформы возвращает примитивный тип данных (строка, число), то функция API SDK возвращает экземпляр класса, наследуемый от класса этого типа данных. + +
Класс, наследуемый от примитивного типа данных + +```python +from dynamix_sdk import Dynamix + + +dx = Dynamix(...) + +# /cloudapi/kvmx86/create +new_vm_id = dx.api.cloudapi.kvmx86.create(...) + +print(type(new_vm_id)) # + +print(isinstance(new_vm_id, int)) # True + +``` + +
+ +Исключением является булев тип данных. Так как наследование от класса `bool` в Python недопустимо, функция API SDK возвращает экземпляр класса, который обладает функциональностью для использования в булевых выражениях (реализован метод `__bool__`), но само булево значение хранится в атрибуте `value`: + +
Класс с булевым значением + +```python +from dynamix_sdk import Dynamix + + +dx = Dynamix(...) + +# /cloudapi/compute/delete +delete_result = dx.api.cloudapi.compute.delete(compute_id=1) + +print(type(delete_result)) # +print(type(delete_result.value)) # + +``` + +Использование в булевом выражении: + +```python +from dynamix_sdk import Dynamix + + +dx = Dynamix(...) + +# /cloudapi/compute/delete +if dx.api.cloudapi.compute.delete(compute_id=1): + print('The VM has been deleted.') + +``` + +
+ +Все объекты, возвращаемые функциями API SDK, обладают общими специальными атрибутами: + +| Атрибут | Описание | +| --- | --- | +| `_api_params` | Модель Pydantic, описывающая параметры, переданные в функцию. | +| `_http_response` | HTTP-ответ API платформы в виде экземпляра класса `requests.Response`. | + +#### Соответствие названий + +В SDK названия из API платформы приведены к стилю "snake_case", принятому в Python, а в некоторых случаях имеют иные отличия, при этом сохраняя смысл исходного названия в API платформы. + +Для соответствия названий в SDK используются таблицы соответствия, с помощью которых можно узнать исходное название в API платформы. +
Каждая такая таблица хранится в виде словаря в файле YAML: +- [api/path_mapping.yml](/src/dynamix_sdk/api/path_mapping.yml) - таблица соответствия названий частей маршрута (URL path) к функции API. +
Данная таблица содержит только те названия, которые отличаются от названий в API платформы. + +- [api/name_mapping.yml](/src/dynamix_sdk/api/name_mapping.yml) - таблица соответствия названий параметров функций и полей возвращаемых данных. +
Данная таблица содержит все названия, в том числе и те, которые совпадают с названиями в API платформы. +
Кроме общих соответствий, данная таблица также содержит индивидуальные соответствия, которые применяются только к классу модели, указанному в формате `sdk_name__model_class_name`. + +### Типы данных SDK + +Все типы данных (классы) SDK, которые могут пригодиться при разработке ПО с использованием SDK, доступны через модуль `dynamix_sdk.types`. + +### Ошибки и исключения (exceptions) + +SDK может вызывать исключения, которые должны быть обработаны в соответствии с требованиями к разрабатываемому вами ПО. + +#### Ошибки валидации данных + +SDK производит валидацию параметров функций API с помощью библиотеки **Pydantic**.
+Поэтому, при ошибке валидации, будет вызвано исключение `pydantic.ValidationError`. + +Таким же образом валидируются данные HTTP-ответов API платформы. Причиной ошибки валидации данных HTTP-ответа API платформы может быть: +1. Несоответствие версии SDK и версии платформы. +1. Программная ошибка, допущенная при разработке SDK или платформы. Получить подтверждение наличия такой ошибки, а также ускорить её устранение, можно сообщив о ней. + +Получить подробную информацию об исключении `pydantic.ValidationError` можно [в соответствующем разделе официальной документации Pydantic](https://docs.pydantic.dev/latest/errors/errors/) (ссылка ведёт на документацию для последней версии, поэтому важно выбрать версию, соответствующую используемой в SDK). + +#### Ошибки HTTP + +SDK для выполнения HTTP-запросов использует библиотеку **Requests**.
+Поэтому, при ошибке HTTP-подключения, будет вызвано соответствующее исключение библиотеки **Requests**. + +Для проверки соответствует ли код ответа успешному выполнению запроса, SDK вызывает метод `requests.Response.raise_for_status()`, поэтому при неуспешном HTTP-запросе также будет вызвано исключение библиотеки **Requests**. + +**Примечание:** если код ответа 503, то SDK предпримет несколько повторных попыток. Подробнее в разделе [Конфигурация](#конфигурация). + +
Обработка ошибок HTTP + +```python +from requests.exceptions import RequestException, HTTPError +from dynamix_sdk import Dynamix + + +dx = Dynamix(...) + +vm_id = 1 + +try: + get_result = dx.api.cloudapi.compute.get(compute_id=vm_id) +except HTTPError as e: + resp = e.response + if resp.status_code == 404: + print(f'The VM ID={vm_id} not found.') + else: + print(f'{e}: {resp.text}') +except RequestException as e: + print(e) + +``` + +
+ +Подробную информацию об исключениях библиотеки **Requests** можно получить в [соответствующем разделе официальной документации Requests](https://requests.readthedocs.io/en/latest/user/quickstart/#errors-and-exceptions). + +## Доступный функционал + +### Способы авторизации + +- с JWT +- через DECS3O +- через BVS + +Подробнее в разделе [Конфигурация](#конфигурация). + +### Функции API + +#### Cloudapi + +
account + +- /cloudapi/account/addUser +- /cloudapi/account/deleteUser +- /cloudapi/account/disable +- /cloudapi/account/enable +- /cloudapi/account/updateUser + +
+ +
compute + +- /cloudapi/compute/delete +- /cloudapi/compute/get +- /cloudapi/compute/list +- /cloudapi/compute/update + +
+ +
kvmx86 + +- /cloudapi/kvmx86/create + +
+ +
rg + +- /cloudapi/rg/create +- /cloudapi/rg/get +- /cloudapi/rg/list + +
+ +
user + +- /cloudapi/user/get + +
+ +#### System + +
usermanager + +- /system/usermanager/whoami + +
diff --git a/demo/select_api_function_and_passing_params_and_save_result.gif b/demo/select_api_function_and_passing_params_and_save_result.gif new file mode 100644 index 0000000..012a614 Binary files /dev/null and b/demo/select_api_function_and_passing_params_and_save_result.gif differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1476e2a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "dynamix-sdk" +authors = [{name = "Dmitriy Smirnov"}] +version = "1.0.0" + +readme = "README.md" + +requires-python = ">=3.10.12" + +dependencies = [ + "requests>=2.32.3", + "pydantic>=2.10.5", + "pyyaml>=6.0.2", +] + +[project.urls] +Repository = "https://repository.basistech.ru/BASIS/dynamix-python-sdk" + +[build-system] +requires = [ + "setuptools", + "setuptools-scm", +] +build-backend = "setuptools.build_meta" + +[tool.setuptools.package-data] +src = ["*.yml"] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..40a16fa --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +pre-commit==4.1.0 diff --git a/src/dynamix_sdk/__init__.py b/src/dynamix_sdk/__init__.py new file mode 100644 index 0000000..b6d6179 --- /dev/null +++ b/src/dynamix_sdk/__init__.py @@ -0,0 +1,3 @@ +from dynamix_sdk.decs3o import DECS3OAuth +from dynamix_sdk.bvs import BVSAuth +from dynamix_sdk.dynamix import Dynamix diff --git a/src/dynamix_sdk/api/__init__.py b/src/dynamix_sdk/api/__init__.py new file mode 100644 index 0000000..f8d8bb7 --- /dev/null +++ b/src/dynamix_sdk/api/__init__.py @@ -0,0 +1,2 @@ +from ._api import * +from ._nested import * diff --git a/src/dynamix_sdk/api/_api.py b/src/dynamix_sdk/api/_api.py new file mode 100644 index 0000000..61279ff --- /dev/null +++ b/src/dynamix_sdk/api/_api.py @@ -0,0 +1,8 @@ +import dynamix_sdk.base as _base +from .cloudapi import * +from .system import * + + +class API(_base.BaseAPI): + cloudapi: CloudapiAPI + system: SystemAPI diff --git a/src/dynamix_sdk/api/_nested/__init__.py b/src/dynamix_sdk/api/_nested/__init__.py new file mode 100644 index 0000000..40743d9 --- /dev/null +++ b/src/dynamix_sdk/api/_nested/__init__.py @@ -0,0 +1,3 @@ +from .params import * +from .result import * +from .enums import * diff --git a/src/dynamix_sdk/api/_nested/enums.py b/src/dynamix_sdk/api/_nested/enums.py new file mode 100644 index 0000000..37dd767 --- /dev/null +++ b/src/dynamix_sdk/api/_nested/enums.py @@ -0,0 +1,161 @@ +from dynamix_sdk.utils import ( + AutoNameEnum as _AutoNameEnum, + enum_auto as _enum_auto, +) + + +class AccessType(str, _AutoNameEnum): + ARCXDU = _enum_auto() + CXDRAU = _enum_auto() + RCX = _enum_auto() + R = _enum_auto() + + +class AffinityMode(str, _AutoNameEnum): + ANY = _enum_auto() + EQ = _enum_auto() + NE = _enum_auto() + + +class AffinityPolicy(str, _AutoNameEnum): + RECOMMENDED = _enum_auto() + REQUIRED = _enum_auto() + + +class AffinityTopology(str, _AutoNameEnum): + compute = _enum_auto() + node = _enum_auto() + + +class BootDevice(str, _AutoNameEnum): + cdrom = _enum_auto() + hd = _enum_auto() + network = _enum_auto() + + +class Chipset(str, _AutoNameEnum): + Q35 = _enum_auto() + i440fx = _enum_auto() + + +class Driver(str, _AutoNameEnum): + KVM_X86 = _enum_auto() + SVA_KVM_X86 = _enum_auto() + + +class NetType(str, _AutoNameEnum): + DPDK = _enum_auto() + EMPTY = _enum_auto() + EXTNET = _enum_auto() + VFNIC = _enum_auto() + VINS = _enum_auto() + + +class NumaAffinity(str, _AutoNameEnum): + none = _enum_auto() + strict = _enum_auto() + loose = _enum_auto() + + +class TXMode(str, _AutoNameEnum): + iothread = _enum_auto() + selected_by_hypervisor = 'selected by hypervisor' + timer = _enum_auto() + + +class Ioeventfd(str, _AutoNameEnum): + off = _enum_auto() + on = _enum_auto() + selected_by_hypervisor = 'selected by hypervisor' + + +class EventIdx(str, _AutoNameEnum): + off = _enum_auto() + on = _enum_auto() + selected_by_hypervisor = 'selected by hypervisor' + + +class ResourceGroupStatus(str, _AutoNameEnum): + CREATED = _enum_auto() + DELETED = _enum_auto() + DESTROYED = _enum_auto() + DESTROYING = _enum_auto() + DISABLED = _enum_auto() + DISABLING = _enum_auto() + ENABLED = _enum_auto() + ENABLING = _enum_auto() + MODELED = _enum_auto() + RESTORING = _enum_auto() + + +class LockStatus(str, _AutoNameEnum): + LOCKED = _enum_auto() + UNLOCKED = _enum_auto() + + +class ComputeFeature(str, _AutoNameEnum): + cpupin = _enum_auto() + dpdk = _enum_auto() + hugepages = _enum_auto() + numa = _enum_auto() + vfnic = _enum_auto() + + +class RGDefaultNetType(str, _AutoNameEnum): + NONE = _enum_auto() + PRIVATE = _enum_auto() + PUBLIC = _enum_auto() + + +class RGResourceType(str, _AutoNameEnum): + compute = _enum_auto() + k8s = _enum_auto() + lb = _enum_auto() + vins = _enum_auto() + + +class DiskStatus(str, _AutoNameEnum): + ASSIGNED = _enum_auto() + CREATED = _enum_auto() + DELETED = _enum_auto() + DESTROYED = _enum_auto() + DESTROYING = _enum_auto() + MODELED = _enum_auto() + PURGED = _enum_auto() + REPLICATION = _enum_auto() + + +class DiskTechStatus(str, _AutoNameEnum): + ALLOCATED = _enum_auto() + UNALLOCATED = _enum_auto() + + +class VMStatus(str, _AutoNameEnum): + CREATED = _enum_auto() + DELETED = _enum_auto() + DELETING = _enum_auto() + DESTROYED = _enum_auto() + DESTROYING = _enum_auto() + DISABLED = _enum_auto() + ENABLED = _enum_auto() + MODELED = _enum_auto() + REDEPLOYING = _enum_auto() + + +class VMTechStatus(str, _AutoNameEnum): + BACKUP_RUNNING = _enum_auto() + BACKUP_STOPPED = _enum_auto() + DOWN = _enum_auto() + MIGRATING = _enum_auto() + PAUSED = _enum_auto() + PAUSING = _enum_auto() + SCHEDULED = _enum_auto() + STARTED = _enum_auto() + STARTING = _enum_auto() + STOPPED = _enum_auto() + STOPPING = _enum_auto() + + +class DiskType(str, _AutoNameEnum): + B = _enum_auto() + D = _enum_auto() diff --git a/src/dynamix_sdk/api/_nested/params.py b/src/dynamix_sdk/api/_nested/params.py new file mode 100644 index 0000000..52a5b47 --- /dev/null +++ b/src/dynamix_sdk/api/_nested/params.py @@ -0,0 +1,20 @@ +import dynamix_sdk.base as _base +from . import enums as _enums + + +class DiskAPIParamsNM(_base.BaseAPIParamsNestedModel): + name: str + size: int + + description: None | str = None + image_id: None | int = None + sep_id: None | int = None + sep_pool_name: None | str = None + + +class InterfaceAPIParamsNM(_base.BaseAPIParamsNestedModel): + net_id: int + net_type: _enums.NetType + + ip_addr: None | str = None + mtu: None | int = None diff --git a/src/dynamix_sdk/api/_nested/result.py b/src/dynamix_sdk/api/_nested/result.py new file mode 100644 index 0000000..48e21ae --- /dev/null +++ b/src/dynamix_sdk/api/_nested/result.py @@ -0,0 +1,343 @@ +import dynamix_sdk.base as _base +from dynamix_sdk.utils import EmptyStr as _EmptyStr +from . import enums as _enums + + +class AccessAPIResultNM(_base.BaseAPIResultNestedModel): + explicit: bool + guid: str + access_type: _enums.AccessType + status: str + type: str + user_group_id: str + + +class AffinityRuleAPIResultNM(_base.BaseAPIResultNestedModel): + guid: str + key: str + mode: _enums.AffinityMode + policy: _enums.AffinityPolicy + topology: _enums.AffinityTopology + value: str + + +class QOSAPIResultNM(_base.BaseAPIResultNestedModel): + egress_rate: int + guid: str + ingress_burst: int + ingress_rate: int + + +class VMACLAPIResultNM(_base.BaseAPIResultNestedModel): + account: list[AccessAPIResultNM] + compute: list[AccessAPIResultNM] + rg: list[AccessAPIResultNM] + + +class LibvirtSettingsAPIResultNM(_base.BaseAPIResultNestedModel): + event_idx: _EmptyStr | _enums.EventIdx + guid: str + ioeventfd: _EmptyStr | _enums.Ioeventfd + queues: int + rx_queue_size: int + tx_mode: _EmptyStr | _enums.TXMode + tx_queue_size: int + + +class BaseInterfaceAPIResultNM(_base.BaseAPIResultNestedModel): + bus_number: int + conn_id: int + conn_type: str + default_gw: str + enabled: bool + flip_group_id: int + guid: str + ip_addr: str + libvirt_settings: LibvirtSettingsAPIResultNM + listen_ssh: bool + mac: str + mtu: int + name: str + net_id: int + net_mask: int + net_type: _enums.NetType + node_id: int + pci_slot: int + target: str + type: str + vnf_ids: list[int] + + +class InterfaceAPIResultNM(BaseInterfaceAPIResultNM): + qos: QOSAPIResultNM + + +class OSUserAPIResultNM(_base.BaseAPIResultNestedModel): + login: str + password: str + pub_key: str + guid: str + + +class SnapshotSetAPIResultNM(_base.BaseAPIResultNestedModel): + disks: list[int] + guid: str + label: str + timestamp: int + + @property + def datetime(self): + return self._get_datetime_from_timestamp(self.timestamp) + + +class IOTuneAPIResultNM(_base.BaseAPIResultNestedModel): + read_bytes_sec: None | int = None + read_bytes_sec_max: None | int = None + read_iops_sec: None | int = None + read_iops_sec_max: None | int = None + size_iops_sec: None | int = None + total_bytes_sec: None | int = None + total_bytes_sec_max: None | int = None + total_iops_sec: None | int = None + total_iops_sec_max: None | int = None + write_bytes_sec: None | int = None + write_bytes_sec_max: None | int = None + write_iops_sec: None | int = None + write_iops_sec_max: None | int = None + + +class SnapshotAPIResultNM(_base.BaseAPIResultNestedModel): + guid: str + label: str + reference_id: str + res_id: str + snapshot_set_guid: str + snapshot_set_timestamp: int + timestamp: int + + @property + def datetime(self): + return self._get_datetime_from_timestamp(self.timestamp) + + @property + def snapshot_set_datetime(self): + return self._get_datetime_from_timestamp(self.snapshot_set_timestamp) + + +class ReplicationAPIResultNM(_base.BaseAPIResultNestedModel): + disk_id: int + pool_id: str + role: str + self_volume_id: str + storage_id: str + volume_id: str + + +class BaseDiskAPIResultNM(_base.BaseAPIResultNestedModel): + bus_number: int + id: int + pci_slot: int + + +class DiskAPIResultNM(BaseDiskAPIResultNM): + account_id: int + acl: dict + boot_partition: int + created_timestamp: int + deleted_timestamp: int + description: str + destruction_timestamp: int + disk_path: str + grid_id: int + guid: int + image_id: int + images: list + io_tune: IOTuneAPIResultNM + iqn: str + login: str + milestones: int + name: str + order: int + params: str + parent_id: int + password: str + present_to: list[int] + purge_timestamp: int + reality_device_number: int + replication_dict: dict + res_id: str + role: str + sep_id: int + sep_pool_name: str + shareable: bool + size_max: int + size_used: float + snapshots: list[SnapshotAPIResultNM] + status: _enums.DiskStatus + tech_status: _enums.DiskTechStatus + type: _enums.DiskType + + updated_timestamp: int = 0 + + @property + def created_datetime(self): + return self._get_datetime_from_timestamp(self.created_timestamp) + + @property + def deleted_datetime(self): + return self._get_datetime_from_timestamp(self.deleted_timestamp) + + @property + def destruction_datetime(self): + return self._get_datetime_from_timestamp(self.destruction_timestamp) + + @property + def updated_datetime(self): + return self._get_datetime_from_timestamp(self.updated_timestamp) + + @property + def purge_datetime(self): + return self._get_datetime_from_timestamp(self.purge_timestamp) + + @property + def replication(self): + if self.replication_dict: + return ReplicationAPIResultNM(**self.replication_dict) + + +class TenantQuotasAPIResultNM(_base.BaseAPIResultNestedModel): + cpu: int + disk_size: int + ext_traffic: int + gpu: int + public_ip: int + ram: float | int + storage: int + + +class ResourceGroupAPIResultNM(_base.BaseAPIResultNestedModel): + account_id: int + account_name: str + acl: list[AccessAPIResultNM] + compute_features: list[_enums.ComputeFeature] + cpu_allocation_parameter: str + cpu_allocation_ratio: int + created_by: str + created_timestamp: int + default_net_id: int + default_net_type: _enums.RGDefaultNetType + deleted_by: str + deleted_timestamp: int + description: str + grid_id: int + guid: int + id: int + lock_status: _enums.LockStatus + milestones: int + name: str + quotas: TenantQuotasAPIResultNM + register_computes: bool + resource_types: list[_enums.RGResourceType] + secret: str + status: _enums.ResourceGroupStatus + uniq_pools: list[str] + updated_by: str + updated_timestamp: int + vins_ids: list[int] + vm_ids: list[int] + + dirty: bool | None = None + + @property + def created_datetime(self): + return self._get_datetime_from_timestamp(self.created_timestamp) + + @property + def deleted_datetime(self): + return self._get_datetime_from_timestamp(self.deleted_timestamp) + + @property + def updated_datetime(self): + return self._get_datetime_from_timestamp(self.updated_timestamp) + + +class BaseVMAPIResultNM(_base.BaseAPIResultNestedModel): + account_id: int + account_name: str + affinity_label: str + affinity_rules: list[AffinityRuleAPIResultNM] + affinity_weight: int + anti_affinity_rules: list[AffinityRuleAPIResultNM] + arch: str + auto_start: bool + boot_disk_size: int + boot_order: list[_enums.BootDevice] + cd_image_id: int + chipset: _enums.Chipset + clone_ids: list[int] + clone_reference: int + compute_ci_id: int + cpu: int + cpu_pin: bool + created_by: str + created_timestamp: int + custom_fields: dict + deleted_by: str + deleted_timestamp: int + description: str + devices: dict + driver: _enums.Driver + gpu_ids: list[int] + grid_id: int + guid: int + hp_backed: bool + id: int + image_id: int + lock_status: _enums.LockStatus + manager_id: int + manager_type: str + migration_job: int + milestones: int + name: str + need_reboot: bool + numa_affinity: _enums.NumaAffinity + numa_node_id: int + pinned: bool + preferred_cpu_cores: list[int] + ram: int + reference_id: str + registered: bool + res_name: str + reserved_cpu_cores: list[int] + rg_id: int + rg_name: str + snapshot_sets: list[SnapshotSetAPIResultNM] + stateless_sep_id: int + stateless_sep_type: str + status: _enums.VMStatus + tags: dict + tech_status: _enums.VMTechStatus + updated_by: str + updated_timestamp: int + user_managed: bool + virtual_image_id: int + + @property + def created_datetime(self): + return self._get_datetime_from_timestamp(self.created_timestamp) + + @property + def deleted_datetime(self): + return self._get_datetime_from_timestamp(self.deleted_timestamp) + + @property + def updated_datetime(self): + return self._get_datetime_from_timestamp(self.updated_timestamp) + + +class VMAPIResultNM(BaseVMAPIResultNM): + acl: list # BDX-7995 + disks: list[BaseDiskAPIResultNM] + interfaces: list[BaseInterfaceAPIResultNM] + total_disks_size: int + vins_count: int diff --git a/src/dynamix_sdk/api/cloudapi/__init__.py b/src/dynamix_sdk/api/cloudapi/__init__.py new file mode 100644 index 0000000..24a9928 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/__init__.py @@ -0,0 +1 @@ +from ._api import * diff --git a/src/dynamix_sdk/api/cloudapi/_api.py b/src/dynamix_sdk/api/cloudapi/_api.py new file mode 100644 index 0000000..f6dcf2d --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/_api.py @@ -0,0 +1,14 @@ +import dynamix_sdk.base as _base +from .account import * +from .compute import * +from .kvmx86 import * +from .rg import * +from .user import * + + +class CloudapiAPI(_base.BaseAPI): + account: CloudapiAccountAPI + compute: CloudapiComputeAPI + kvmx86: CloudapiKvmx86API + rg: CloudapiRgAPI + user: CloudapiUserAPI diff --git a/src/dynamix_sdk/api/cloudapi/account/__init__.py b/src/dynamix_sdk/api/cloudapi/account/__init__.py new file mode 100644 index 0000000..24a9928 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/account/__init__.py @@ -0,0 +1 @@ +from ._api import * diff --git a/src/dynamix_sdk/api/cloudapi/account/_api.py b/src/dynamix_sdk/api/cloudapi/account/_api.py new file mode 100644 index 0000000..4dc1952 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/account/_api.py @@ -0,0 +1,17 @@ +import dynamix_sdk.base as _base +from .add_user import * +from .delete_user import * +from .disable import * +from .enable import * +from .update_user import * + + +class CloudapiAccountAPI( + _base.BaseAPI, + CloudapiAccountAddUserProtocol, + CloudapiAccountDeleteUserProtocol, + CloudapiAccountDisableProtocol, + CloudapiAccountEnableProtocol, + CloudapiAccountUpdateUserProtocol, +): + pass diff --git a/src/dynamix_sdk/api/cloudapi/account/add_user.py b/src/dynamix_sdk/api/cloudapi/account/add_user.py new file mode 100644 index 0000000..544ec75 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/account/add_user.py @@ -0,0 +1,17 @@ +import dynamix_sdk.base as _base +import dynamix_sdk.api._nested as _nested + + +class CloudapiAccountAddUserResultBool(_base.BaseAPIResultBool): + pass + + +class CloudapiAccountAddUserProtocol(_base.BasePostAPIFunctionProtocol): + def add_user( + self, + *, + account_id: int, + access_type: _nested.AccessType, + user_id: str, + ) -> CloudapiAccountAddUserResultBool: + ... diff --git a/src/dynamix_sdk/api/cloudapi/account/delete_user.py b/src/dynamix_sdk/api/cloudapi/account/delete_user.py new file mode 100644 index 0000000..0af9705 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/account/delete_user.py @@ -0,0 +1,15 @@ +import dynamix_sdk.base as _base + + +class CloudapiAccountDeleteUserResultBool(_base.BaseAPIResultBool): + pass + + +class CloudapiAccountDeleteUserProtocol(_base.BasePostAPIFunctionProtocol): + def delete_user( + self, + *, + account_id: int, + user_id: str, + ) -> CloudapiAccountDeleteUserResultBool: + ... diff --git a/src/dynamix_sdk/api/cloudapi/account/disable.py b/src/dynamix_sdk/api/cloudapi/account/disable.py new file mode 100644 index 0000000..a93f379 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/account/disable.py @@ -0,0 +1,14 @@ +import dynamix_sdk.base as _base + + +class CloudapiAccountDisableResultBool(_base.BaseAPIResultBool): + pass + + +class CloudapiAccountDisableProtocol(_base.BasePostAPIFunctionProtocol): + def disable( + self, + *, + account_id: int, + ) -> CloudapiAccountDisableResultBool: + ... diff --git a/src/dynamix_sdk/api/cloudapi/account/enable.py b/src/dynamix_sdk/api/cloudapi/account/enable.py new file mode 100644 index 0000000..34e0b28 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/account/enable.py @@ -0,0 +1,14 @@ +import dynamix_sdk.base as _base + + +class CloudapiAccountEnableResultBool(_base.BaseAPIResultBool): + pass + + +class CloudapiAccountEnableProtocol(_base.BasePostAPIFunctionProtocol): + def enable( + self, + *, + account_id: int, + ) -> CloudapiAccountEnableResultBool: + ... diff --git a/src/dynamix_sdk/api/cloudapi/account/update_user.py b/src/dynamix_sdk/api/cloudapi/account/update_user.py new file mode 100644 index 0000000..d945308 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/account/update_user.py @@ -0,0 +1,17 @@ +import dynamix_sdk.base as _base +import dynamix_sdk.api._nested as _nested + + +class CloudapiAccountUpdateUserResultBool(_base.BaseAPIResultBool): + pass + + +class CloudapiAccountUpdateUserProtocol(_base.BasePostAPIFunctionProtocol): + def update_user( + self, + *, + account_id: int, + access_type: _nested.AccessType, + user_id: str, + ) -> CloudapiAccountUpdateUserResultBool: + ... diff --git a/src/dynamix_sdk/api/cloudapi/compute/__init__.py b/src/dynamix_sdk/api/cloudapi/compute/__init__.py new file mode 100644 index 0000000..24a9928 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/compute/__init__.py @@ -0,0 +1 @@ +from ._api import * diff --git a/src/dynamix_sdk/api/cloudapi/compute/_api.py b/src/dynamix_sdk/api/cloudapi/compute/_api.py new file mode 100644 index 0000000..2a0d036 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/compute/_api.py @@ -0,0 +1,15 @@ +import dynamix_sdk.base as _base +from .delete import * +from .get import * +from .list import * +from .update import * + + +class CloudapiComputeAPI( + _base.BaseAPI, + CloudapiComputeDeleteProtocol, + CloudapiComputeGetProtocol, + CloudapiComputeListProtocol, + CloudapiComputeUpdateProtocol, +): + pass diff --git a/src/dynamix_sdk/api/cloudapi/compute/delete.py b/src/dynamix_sdk/api/cloudapi/compute/delete.py new file mode 100644 index 0000000..70dd602 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/compute/delete.py @@ -0,0 +1,17 @@ +import dynamix_sdk.base as _base + + +class CloudapiComputeDeleteResultBool(_base.BaseAPIResultBool): + pass + + +class CloudapiComputeDeleteProtocol(_base.BasePostAPIFunctionProtocol): + def delete( + self, + *, + compute_id: int, + + detach_disks: bool = False, + permanently: bool = False, + ) -> CloudapiComputeDeleteResultBool: + ... diff --git a/src/dynamix_sdk/api/cloudapi/compute/get.py b/src/dynamix_sdk/api/cloudapi/compute/get.py new file mode 100644 index 0000000..51a3549 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/compute/get.py @@ -0,0 +1,31 @@ +import dynamix_sdk.base as _base +import dynamix_sdk.api._nested as _nested + + +class CloudapiComputeGetResultModel( + _base.BaseAPIResultModel, + _nested.BaseVMAPIResultNM, +): + acl: _nested.VMACLAPIResultNM + ci_user_data: dict + disks: list[_nested.DiskAPIResultNM] + image_name: None | str + interfaces: list[_nested.InterfaceAPIResultNM] + os_users: list[_nested.OSUserAPIResultNM] + virtual_image_name: None | str + vns_password: str + + natable_vins_id: int = 0 + natable_vins_ip: str = '' + natable_vins_name: str = '' + natable_vins_network: str = '' + natable_vins_network_name: str = '' + + +class CloudapiComputeGetProtocol(_base.BasePostAPIFunctionProtocol): + def get( + self, + *, + compute_id: int, + ) -> CloudapiComputeGetResultModel: + ... diff --git a/src/dynamix_sdk/api/cloudapi/compute/list.py b/src/dynamix_sdk/api/cloudapi/compute/list.py new file mode 100644 index 0000000..1275be9 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/compute/list.py @@ -0,0 +1,29 @@ +import dynamix_sdk.base as _base +import dynamix_sdk.api._nested as _nested + + +class CloudapiComputeListResultModel(_base.BaseAPIResultModel): + data: list[_nested.VMAPIResultNM] + entry_count: int + + +class CloudapiComputeListProtocol(_base.BasePostAPIFunctionProtocol): + def list( + self, + *, + account_id: int | None = None, + ext_net_id: int | None = None, + ext_net_name: str | None = None, + id: int | None = None, + include_deleted: bool = False, + ip_addr: str | None = None, + name: str | None = None, + page_number: int | None = None, + page_size: int | None = None, + rg_id: int | None = None, + rg_name: str | None = None, + sort_by: str | None = None, + status: _nested.VMStatus | None = None, + tech_status: _nested.VMTechStatus | None = None, + ) -> CloudapiComputeListResultModel: + ... diff --git a/src/dynamix_sdk/api/cloudapi/compute/update.py b/src/dynamix_sdk/api/cloudapi/compute/update.py new file mode 100644 index 0000000..7e113a1 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/compute/update.py @@ -0,0 +1,23 @@ +import dynamix_sdk.base as _base +import dynamix_sdk.api._nested as _nested + + +class CloudapiComputeUpdateResultBool(_base.BaseAPIResultBool): + pass + + +class CloudapiComputeUpdateProtocol(_base.BasePostAPIFunctionProtocol): + def update( + self, + *, + compute_id: int, + auto_start: None | bool = None, + chipset: None | _nested.Chipset = None, + cpu_pin: None | bool = None, + description: None | str = None, + hp_backed: None | bool = None, + name: None | str = None, + numa_affinity: None | _nested.NumaAffinity = None, + preferred_cpu_cores: None | list[int] = None, + ) -> CloudapiComputeUpdateResultBool: + ... diff --git a/src/dynamix_sdk/api/cloudapi/kvmx86/__init__.py b/src/dynamix_sdk/api/cloudapi/kvmx86/__init__.py new file mode 100644 index 0000000..24a9928 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/kvmx86/__init__.py @@ -0,0 +1 @@ +from ._api import * diff --git a/src/dynamix_sdk/api/cloudapi/kvmx86/_api.py b/src/dynamix_sdk/api/cloudapi/kvmx86/_api.py new file mode 100644 index 0000000..d254db5 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/kvmx86/_api.py @@ -0,0 +1,9 @@ +import dynamix_sdk.base as _base +from .create import * + + +class CloudapiKvmx86API( + _base.BaseAPI, + CloudapiKvmx86CreateProtocol, +): + pass diff --git a/src/dynamix_sdk/api/cloudapi/kvmx86/create.py b/src/dynamix_sdk/api/cloudapi/kvmx86/create.py new file mode 100644 index 0000000..be67602 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/kvmx86/create.py @@ -0,0 +1,37 @@ +import dynamix_sdk.base as _base +import dynamix_sdk.api._nested as _nested + + +class CloudapiKvmx86CreateResultInt(_base.BaseAPIResultInt): + pass + + +class CloudapiKvmx86CreateProtocol(_base.BasePostAPIFunctionProtocol): + def create( + self, + *, + cpu: int, + name: str, + ram: int, + rg_id: int, + boot_disk_size: None | int = None, + chipset: _nested.Chipset = _nested.Chipset.i440fx, + ci_user_data: None | dict = None, + cpu_pin: bool = False, + custom_fields: None | str = None, + data_disks: None | list[_nested.DiskAPIParamsNM] = None, + description: None | str = None, + driver: _nested.Driver = _nested.Driver.KVM_X86, + hp_backed: bool = False, + image_id: None | int = None, + interfaces: None | list[_nested.InterfaceAPIParamsNM] = None, + ipa_type: None | str = None, + numa_affinity: _nested.NumaAffinity = _nested.NumaAffinity.none, + preferred_cpu_cores: None | list[int] = None, + sep_id: None | int = None, + sep_pool_name: None | str = None, + start: bool = True, + system_name: None | str = None, + without_boot_disk: bool = False, + ) -> CloudapiKvmx86CreateResultInt: + ... diff --git a/src/dynamix_sdk/api/cloudapi/rg/__init__.py b/src/dynamix_sdk/api/cloudapi/rg/__init__.py new file mode 100644 index 0000000..24a9928 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/rg/__init__.py @@ -0,0 +1 @@ +from ._api import * diff --git a/src/dynamix_sdk/api/cloudapi/rg/_api.py b/src/dynamix_sdk/api/cloudapi/rg/_api.py new file mode 100644 index 0000000..8f9516f --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/rg/_api.py @@ -0,0 +1,13 @@ +import dynamix_sdk.base as _base +from .create import * +from .get import * +from .list import * + + +class CloudapiRgAPI( + _base.BaseAPI, + CloudapiRgCreateProtocol, + CloudapiRgGetProtocol, + CloudapiRgListProtocol, +): + pass diff --git a/src/dynamix_sdk/api/cloudapi/rg/create.py b/src/dynamix_sdk/api/cloudapi/rg/create.py new file mode 100644 index 0000000..8c3ccb3 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/rg/create.py @@ -0,0 +1,31 @@ +import dynamix_sdk.base as _base +import dynamix_sdk.api._nested as _nested + + +class CloudapiRgCreateResultBoolInt(_base.BaseAPIResultInt): + pass + + +class CloudapiRgCreateProtocol(_base.BasePostAPIFunctionProtocol): + def create( + self, + *, + account_id: int, + grid_id: int, + name: str, + cpu_quota: int = -1, + default_net_type: _nested.RGDefaultNetType = ( + _nested.RGDefaultNetType.PRIVATE + ), + description: str | None = None, + ext_net_id: int = 0, + ext_net_ip: str | None = None, + ext_traffic_quota: int = -1, + ip_cidr: str | None = None, + owner: str | None = None, + public_ip_quota: int = -1, + ram_quota: int = -1, + register_computes: bool = False, + storage_quota: int = -1, + ) -> CloudapiRgCreateResultBoolInt: + ... diff --git a/src/dynamix_sdk/api/cloudapi/rg/get.py b/src/dynamix_sdk/api/cloudapi/rg/get.py new file mode 100644 index 0000000..7619a74 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/rg/get.py @@ -0,0 +1,18 @@ +import dynamix_sdk.base as _base +import dynamix_sdk.api._nested as _nested + + +class CloudapiRgGetResultModel( + _base.BaseAPIResultModel, + _nested.ResourceGroupAPIResultNM, +): + pass + + +class CloudapiRgGetProtocol(_base.BasePostAPIFunctionProtocol): + def get( + self, + *, + rg_id: int, + ) -> CloudapiRgGetResultModel: + ... diff --git a/src/dynamix_sdk/api/cloudapi/rg/list.py b/src/dynamix_sdk/api/cloudapi/rg/list.py new file mode 100644 index 0000000..ee12ff8 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/rg/list.py @@ -0,0 +1,27 @@ +import dynamix_sdk.base as _base +import dynamix_sdk.api._nested as _nested + + +class CloudapiRgListResultModel(_base.BaseAPIResultModel): + data: list[_nested.ResourceGroupAPIResultNM] + entry_count: int + + +class CloudapiRgListProtocol(_base.BasePostAPIFunctionProtocol): + def list( + self, + *, + account_id: int | None = None, + account_name: str | None = None, + created_after_timestamp: int | None = None, + created_before_timestamp: int | None = None, + id: int | None = None, + include_deleted: bool = False, + lock_status: _nested.LockStatus | None = None, + name: str | None = None, + page_number: int | None = None, + page_size: int | None = None, + sort_by: str | None = None, + status: _nested.ResourceGroupStatus | None = None, + ) -> CloudapiRgListResultModel: + ... diff --git a/src/dynamix_sdk/api/cloudapi/user/__init__.py b/src/dynamix_sdk/api/cloudapi/user/__init__.py new file mode 100644 index 0000000..24a9928 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/user/__init__.py @@ -0,0 +1 @@ +from ._api import * diff --git a/src/dynamix_sdk/api/cloudapi/user/_api.py b/src/dynamix_sdk/api/cloudapi/user/_api.py new file mode 100644 index 0000000..755728a --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/user/_api.py @@ -0,0 +1,9 @@ +import dynamix_sdk.base as _base +from .get import * + + +class CloudapiUserAPI( + _base.BaseAPI, + CloudapiUserGetProtocol, +): + pass diff --git a/src/dynamix_sdk/api/cloudapi/user/get.py b/src/dynamix_sdk/api/cloudapi/user/get.py new file mode 100644 index 0000000..6982f36 --- /dev/null +++ b/src/dynamix_sdk/api/cloudapi/user/get.py @@ -0,0 +1,12 @@ +import dynamix_sdk.base as _base + + +class CloudapiUserGetResultModel(_base.BaseAPIResultModel): + data: dict + email_addresses: list[str] + user_name: str + + +class CloudapiUserGetProtocol(_base.BasePostAPIFunctionProtocol): + def get(self, *, user_name: str) -> CloudapiUserGetResultModel: + ... diff --git a/src/dynamix_sdk/api/name_mapping.yml b/src/dynamix_sdk/api/name_mapping.yml new file mode 100644 index 0000000..dd8684d --- /dev/null +++ b/src/dynamix_sdk/api/name_mapping.yml @@ -0,0 +1,224 @@ +# sdk_name[__model_class_name]: dynamix_name +access_type: accesstype +access_type__AccessAPIResultNM: right +account: accountAcl +account_id: accountId +account_name: accountName +acl: acl +acl__CloudapiComputeGetResultModel: ACL +admin: admin +affinity_label: affinityLabel +affinity_rules: affinityRules +affinity_weight: affinityWeight +anti_affinity_rules: antiAffinityRules +arch: arch +auto_start: autoStart +boot_disk_size: bootdiskSize +boot_disk_size__CloudapiKvmx86CreateParamsModel: bootDisk +boot_order: bootOrder +boot_partition: bootPartition +bus_number: bus_number +cd_image_id: cdImageId +chipset: chipset +ci_user_data: userdata +clone_ids: clones +clone_reference: cloneReference +compute: computeAcl +compute_ci_id: computeciId +compute_features: computeFeatures +compute_id: computeId +conn_id: connId +conn_type: connType +cpu: cpu +cpu__CloudapiComputeGetResultModel: cpus +cpu__TenantQuotasAPIResultNM: CU_C +cpu__VMAPIResultNM: cpus +cpu_allocation_parameter: cpu_allocation_parameter +cpu_allocation_ratio: cpu_allocation_ratio +cpu_pin: cpupin +cpu_quota: maxCPUCapacity +created_after_timestamp: createdAfter +created_before_timestamp: createdBefore +created_by: createdBy +created_timestamp: createdTime +custom_fields: customFields +data: data +data_disks: dataDisks +default_gw: defGw +default_net_id: def_net_id +default_net_type: def_net_type +default_net_type__CloudapiRgCreateParamsModel: def_net +deleted_by: deletedBy +deleted_timestamp: deletedTime +description: desc +destruction_timestamp: destructionTime +detach_disks: detachDisks +devices: devices +dirty: dirty +disk_id: diskId +disk_path: diskPath +disk_size: CU_D +disks: disks +driver: driver +egress_rate: eRate +email_addresses: emailaddresses +enabled: enabled +entry_count: entryCount +event_idx: event_idx +explicit: explicit +ext_net_id: extNetId +ext_net_ip: extIp +ext_net_name: extNetName +ext_traffic: CU_NP +ext_traffic_quota: maxNetworkPeerTransfer +flip_group_id: flipgroupId +gpu: gpu_units +gpu_ids: vgpus +grid_id: gid +guid: guid +hp_backed: hpBacked +id: id +id__CloudapiComputeListParamsModel: by_id +id__CloudapiRgListParamsModel: by_id +image_id: imageId +image_name: imageName +images: images +include_deleted: includedeleted +ingress_burst: inBurst +ingress_rate: inRate +interfaces: interfaces +io_tune: iotune +ioeventfd: ioeventfd +ip_addr: ipAddress +ip_addr__InterfaceAPIParamsNM: ipAddr +ip_cidr: ipcidr +ipa_type: ipaType +iqn: iqn +key: key +label: label +libvirt_settings: libvirtSettings +listen_ssh: listenSsh +lock_status: lockStatus +login: login +mac: mac +manager_id: managerId +manager_type: managerType +migration_job: migrationjob +milestones: milestones +mode: mode +mtu: mtu +name: name +name__DiskAPIParamsNM: diskName +natable_vins_id: natableVinsId +natable_vins_ip: natableVinsIp +natable_vins_name: natableVinsName +natable_vins_network: natableVinsNetwork +natable_vins_network_name: natableVinsNetworkName +need_reboot: needReboot +net_id: netId +net_mask: netMask +net_type: netType +node_id: nodeId +numa_affinity: numaAffinity +numa_node_id: numaNodeId +order: order +os_users: osUsers +owner: owner +page_number: page +page_size: size +params: params +parent_id: parentId +password: password +password__DiskAPIResultNM: passwd +pci_slot: pciSlot +permanently: permanently +pinned: pinned +policy: policy +pool_id: poolId +preferred_cpu_cores: preferredCpu +present_to: presentTo +pub_key: pubkey +public_ip: CU_I +public_ip_quota: maxNumPublicIP +purge_timestamp: purgeTime +qos: qos +queues: queues +quotas: resourceLimits +ram: ram +ram__TenantQuotasAPIResultNM: CU_M +ram_quota: maxMemoryCapacity +read_bytes_sec: read_bytes_sec +read_bytes_sec_max: read_bytes_sec_max +read_iops_sec: read_iops_sec +read_iops_sec_max: read_iops_sec_max +reality_device_number: realityDeviceNumber +reference_id: referenceId +register_computes: registerComputes +registered: registered +replication_dict: replication +res_id: resId +res_name: resName +reserved_cpu_cores: reservedNodeCpus +resource_types: resourceTypes +rg: rgAcl +rg_id: rgId +rg_name: rgName +role: role +roles: roles +rx_queue_size: rx_queue_size +secret: secret +self_volume_id: selfVolumeId +sep_id: sepId +sep_pool_name: pool +shareable: shareable +size__DiskAPIParamsNM: size +size_iops_sec: size_iops_sec +size_max: sizeMax +size_used: sizeUsed +snapshot_set_guid: snapSetGuid +snapshot_set_timestamp: snapSetTime +snapshot_sets: snapSets +snapshots: snapshots +sort_by: sortBy +start: start +stateless_sep_id: statelessSepId +stateless_sep_type: statelessSepType +status: status +storage: CU_DM +storage_id: storageId +storage_quota: maxVDiskCapacity +system_name: IS +tags: tags +target: target +tech_status: techStatus +timestamp: timestamp +topology: topology +total_bytes_sec: total_bytes_sec +total_bytes_sec_max: total_bytes_sec_max +total_disks_size: totalDisksSize +total_iops_sec: total_iops_sec +total_iops_sec_max: total_iops_sec_max +tx_mode: txmode +tx_queue_size: tx_queue_size +type: type +uniq_pools: uniqPools +updated_by: updatedBy +updated_timestamp: updatedTime +user_group_id: userGroupId +user_id: userId +user_managed: userManaged +user_name: username +value: value +vins_count: vinsConnected +vins_ids: vins +virtual_image_id: virtualImageId +virtual_image_name: virtualImageName +vm_ids: vms +vnf_ids: vnfs +vns_password: vncPasswd +volume_id: volumeId +without_boot_disk: withoutBootDisk +write_bytes_sec: write_bytes_sec +write_bytes_sec_max: write_bytes_sec_max +write_iops_sec: write_iops_sec +write_iops_sec_max: write_iops_sec_max diff --git a/src/dynamix_sdk/api/path_mapping.yml b/src/dynamix_sdk/api/path_mapping.yml new file mode 100644 index 0000000..8ef42fa --- /dev/null +++ b/src/dynamix_sdk/api/path_mapping.yml @@ -0,0 +1,4 @@ +# sdk_path_part: dynamix_path_part +add_user: addUser +delete_user: deleteUser +update_user: updateUser diff --git a/src/dynamix_sdk/api/system/__init__.py b/src/dynamix_sdk/api/system/__init__.py new file mode 100644 index 0000000..24a9928 --- /dev/null +++ b/src/dynamix_sdk/api/system/__init__.py @@ -0,0 +1 @@ +from ._api import * diff --git a/src/dynamix_sdk/api/system/_api.py b/src/dynamix_sdk/api/system/_api.py new file mode 100644 index 0000000..bc93d79 --- /dev/null +++ b/src/dynamix_sdk/api/system/_api.py @@ -0,0 +1,6 @@ +import dynamix_sdk.base as _base +from .usermanager import * + + +class SystemAPI(_base.BaseAPI): + usermanager: SystemUsermanagerAPI diff --git a/src/dynamix_sdk/api/system/usermanager/__init__.py b/src/dynamix_sdk/api/system/usermanager/__init__.py new file mode 100644 index 0000000..24a9928 --- /dev/null +++ b/src/dynamix_sdk/api/system/usermanager/__init__.py @@ -0,0 +1 @@ +from ._api import * diff --git a/src/dynamix_sdk/api/system/usermanager/_api.py b/src/dynamix_sdk/api/system/usermanager/_api.py new file mode 100644 index 0000000..3a32bdd --- /dev/null +++ b/src/dynamix_sdk/api/system/usermanager/_api.py @@ -0,0 +1,9 @@ +import dynamix_sdk.base as _base +from .whoami import * + + +class SystemUsermanagerAPI( + _base.BaseAPI, + SystemUsermanagerWhoamiProtocol, +): + pass diff --git a/src/dynamix_sdk/api/system/usermanager/whoami.py b/src/dynamix_sdk/api/system/usermanager/whoami.py new file mode 100644 index 0000000..4536ee5 --- /dev/null +++ b/src/dynamix_sdk/api/system/usermanager/whoami.py @@ -0,0 +1,12 @@ +import dynamix_sdk.base as _base + + +class SystemUsermanagerWhoamiResultModel(_base.BaseAPIResultModel): + admin: bool + name: str + roles: list[str] + + +class SystemUsermanagerWhoamiProtocol(_base.BasePostAPIFunctionProtocol): + def whoami(self) -> SystemUsermanagerWhoamiResultModel: + ... diff --git a/src/dynamix_sdk/base.py b/src/dynamix_sdk/base.py new file mode 100644 index 0000000..b37f252 --- /dev/null +++ b/src/dynamix_sdk/base.py @@ -0,0 +1,490 @@ +from abc import ABC, abstractmethod +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime +import inspect +import os +import re +import time +from types import GenericAlias, UnionType +from typing import Any, Literal, ParamSpec, Protocol, TypeVar, get_args + +import requests +from pydantic import ( + AliasGenerator, + BaseModel as PydanticBaseModel, + ConfigDict, + PrivateAttr, + create_model, +) +import yaml + +from dynamix_sdk.config import Config, ConfigWithAuth +from dynamix_sdk.utils import HTTPMethod, gen_cls_name_from_url_path + + +NAME_MAPPING_FILE_PATH = os.path.join( + os.path.dirname(__file__), + 'api', + 'name_mapping.yml', +) +PATH_MAPPING_FILE_PATH = os.path.join( + os.path.dirname(__file__), + 'api', + 'path_mapping.yml', +) + + +def read_mapping_file(file_path: str): + with open(file_path) as file: + parsed_data: Any = yaml.safe_load(file) + + if not isinstance(parsed_data, dict): + raise TypeError + result_dict: dict[Any, Any] = parsed_data + + with open(file_path) as file: + name_mapping_file_lines_amount = sum( + 1 for s in file if s.lstrip() and not s.startswith('#') + ) + + if len(result_dict) < name_mapping_file_lines_amount: + raise AssertionError( + f'File {file_path} contains more code lines than' + f' keys in the parsed data. Check the file for duplicate keys.' + ) + + for k, v in result_dict.items(): + if not isinstance(k, str) or not isinstance(v, str): + raise TypeError + result_str_dict: dict[str, str] = result_dict + + return result_str_dict + + +path_mapping_dict = read_mapping_file(PATH_MAPPING_FILE_PATH) + + +name_mapping_dict = read_mapping_file(NAME_MAPPING_FILE_PATH) +common_mappings_values = [ + v for k, v in name_mapping_dict.items() if '__' not in k +] +if len(common_mappings_values) > len(set(common_mappings_values)): + raise AssertionError( + f'File {NAME_MAPPING_FILE_PATH} can contain duplicate values' + f' only for individual mapping (attr_name__model_class_name),' + f' not common. Check common mappings for duplicate values.' + ) + + +class BaseAPIFunctionProtocol(Protocol): + pass + + +class BasePostAPIFunctionProtocol(BaseAPIFunctionProtocol): + pass + + +class BaseGetAPIFunctionProtocol(BaseAPIFunctionProtocol): + pass + + +class BaseModel(PydanticBaseModel, ABC): + @staticmethod + def _get_datetime_from_timestamp(timestamp: float) -> None | datetime: + if timestamp > 0: + return datetime.fromtimestamp(timestamp) + + +class BaseModelWithFrozenStrictExtraForbid(BaseModel, ABC): + model_config = ConfigDict( + extra='forbid', + strict=True, + frozen=True, + ) + + +class BaseAPIParamsModel(BaseModelWithFrozenStrictExtraForbid, ABC): + pass + + +class BaseAPIParamsNestedModel(BaseModelWithFrozenStrictExtraForbid, ABC): + def __init_subclass__( + cls, + *args: tuple[Any], + **kwargs: dict[str, Any], + ) -> None: + super().__init_subclass__(*args, **kwargs) + + postfix = 'APIParamsNM' + if not cls.__qualname__.endswith(postfix): + raise ValueError( + f'Name of {cls} must end with {postfix}.', + ) + + +@dataclass +class APIResultContext: + api_params: BaseAPIParamsModel + http_response: requests.Response + + +class BaseAPIResult(ABC): + _api_params: BaseAPIParamsModel + _http_response: requests.Response + + +class BaseAPIResultModel( + BaseModelWithFrozenStrictExtraForbid, + BaseAPIResult, + ABC, +): + _api_params: BaseAPIParamsModel = PrivateAttr() + _http_response: requests.Response = PrivateAttr() + + def model_post_init(self, __context: APIResultContext): + self._api_params = __context.api_params + self._http_response = __context.http_response + + +class BaseAPIResultNestedModel(BaseModelWithFrozenStrictExtraForbid, ABC): + pass + + +class BaseAPIResultBasicType(BaseAPIResult, ABC): + _frozen: bool = False + + @abstractmethod + def __new__(cls, value: Any, context: APIResultContext): + return super().__new__(cls) + + def __setattr__(self, name: str, value: Any) -> None: + if self._frozen: + raise AttributeError( + 'Instance is frozen.', + ) + return super().__setattr__(name, value) + + +class BaseAPIResultStr(str, BaseAPIResultBasicType, ABC): + def __new__(cls, value: str, context: APIResultContext): + instance = super().__new__(cls, value) + instance._api_params = context.api_params + instance._http_response = context.http_response + instance._frozen = True + return instance + + +class BaseAPIResultInt(int, BaseAPIResultBasicType, ABC): + def __new__(cls, value: int, context: APIResultContext): + instance = super().__new__(cls, value) + instance._api_params = context.api_params + instance._http_response = context.http_response + instance._frozen = True + return instance + + +class BaseAPIResultBool(BaseAPIResultBasicType, ABC): + value: bool + + def __bool__(self): + return self.value + + def __str__(self): + return f'{self.__class__.__qualname__}: {self.value}' + + def __new__(cls, value: bool, context: APIResultContext): + if not isinstance(value, bool): + raise TypeError + instance = super().__new__(cls, value, context) + instance.value = value + instance._api_params = context.api_params + instance._http_response = context.http_response + instance._frozen = True + return instance + + +def set_alias_gen( + model_cls: type[BaseModel], + alias_type: Literal['validation', 'serialization'], + name_mapping_dict: dict[str, str] +): + if model_cls.model_config.get('alias_generator'): + return + + def get_model_classes_from_annotation( + annotation: type[Any] | None, + classes: list[type[BaseModel]] | None = None, + ): + _classes = classes or [] + if annotation: + if isinstance(annotation, (UnionType, GenericAlias)): + for cls in get_args(annotation): + _classes += get_model_classes_from_annotation(cls) + elif issubclass(annotation, BaseModel): + _classes.append(annotation) + return _classes + + for field in model_cls.model_fields.values(): + for cls in get_model_classes_from_annotation(field.annotation): + set_alias_gen( + model_cls=cls, + alias_type=alias_type, + name_mapping_dict=name_mapping_dict, + ) + + def alias_gen(field_name: str): + individual_alias = name_mapping_dict.get( + f'{field_name}__{model_cls.__qualname__}', + ) + if individual_alias: + return individual_alias + else: + if field_name in name_mapping_dict: + return name_mapping_dict[field_name] + else: + raise KeyError( + f'{field_name} not found in name mapping dictionary.' + f' Class model: {model_cls.__name__}.' + ) + + kwargs = { + f'{alias_type}_alias': alias_gen + } + + model_cls.model_config['alias_generator'] = AliasGenerator(**kwargs) + model_cls.model_rebuild(force=True) + + +params_model_classes: dict[str, type[BaseAPIParamsModel]] = {} + + +APIParamsP = ParamSpec('APIParamsP') +APIResultT = TypeVar( + 'APIResultT', + bound=BaseAPIResultModel | BaseAPIResultBasicType, +) + + +class BaseAPI(ABC): + _config: Config + _base_api_path: None | str + _path_mapping_dict: dict[str, str] = path_mapping_dict + _last_call_api_path: None | str = None + _name_mapping_dict: dict[str, str] = name_mapping_dict + _post_json: bool = True + + def __init_subclass__( + cls, + path_mapping_dict: None | dict[str, str] = None, + name_mapping_dict: None | dict[str, str] = None, + post_json: None | bool = None, + ) -> None: + if path_mapping_dict: + cls._path_mapping_dict = path_mapping_dict + if name_mapping_dict: + cls._name_mapping_dict = name_mapping_dict + if post_json is not None: + cls._post_json = post_json + + def __init__( + self, + config: Config, + base_api_path: None | str = None + ): + self._config = config + self._base_api_path = base_api_path + + def __getattribute__(self, name: str) -> Any: + if name.startswith('_'): + return super().__getattribute__(name) + else: + path_part = self._path_mapping_dict.get(name, name) + if name in self.__annotations__: + annotation = self.__annotations__[name] + if issubclass(annotation, BaseAPI): + api_cls = annotation + return api_cls( + config=self._config, + base_api_path=( + f'{self._base_api_path or ""}/{path_part}' + ) + ) + else: + self._last_call_api_path = ( + f'{self._base_api_path or ""}/{path_part}' + ) + + attr_value = super().__getattribute__(name) + if not inspect.ismethod(attr_value): + raise ValueError + + return self._make_api_function(attr_value) + + def _make_api_function( + self, + protocol_method: Callable[APIParamsP, APIResultT], + /, + ) -> Callable[APIParamsP, APIResultT]: + return_type = inspect.signature(protocol_method).return_annotation + if not issubclass(return_type, get_args(APIResultT.__bound__)): + raise TypeError( + f'Return type annotation of {protocol_method}' + f' must be subclass of {get_args(APIResultT.__bound__)}.' + ) + api_result_cls: type[APIResultT] = return_type + + def api_function( + *args: APIParamsP.args, + **kwargs: APIParamsP.kwargs, + ): + config = self._config + + if self._last_call_api_path: + api_path = self._last_call_api_path + else: + raise ValueError + + api_params_cls_name = gen_cls_name_from_url_path( + url_path=re.sub(r'[\[\]]', '', api_path), + postfix=BaseAPIParamsModel.__qualname__.removeprefix( + 'BaseAPI' + ), + ) + + api_params_cls = params_model_classes.get( + api_params_cls_name, + None, + ) + if not api_params_cls: + field_definitions = {} + parameters = inspect.signature(protocol_method).parameters + inspect_empty = inspect.Parameter.empty + for p in parameters.values(): + field_definitions[p.name] = ( + p.annotation, + ... if p.default is inspect_empty else p.default, + ) + + api_params_cls = create_model( + api_params_cls_name, + __base__=BaseAPIParamsModel, + __module__=api_result_cls.__module__, + **field_definitions, + ) + params_model_classes[api_params_cls_name] = api_params_cls + + set_alias_gen( + model_cls=api_params_cls, + alias_type='serialization', + name_mapping_dict=self._name_mapping_dict, + ) + + api_params = api_params_cls(*args, **kwargs) + + req_headers = None + if isinstance(config, ConfigWithAuth): + req_headers = { + 'Authorization': f'bearer {config.jwt}', + } + + model_data = api_params.model_dump( + by_alias=True, + exclude_none=True, + ) + + for api_mixin_cls in self.__class__.__bases__[1:]: + if hasattr(api_mixin_cls, protocol_method.__name__): + if issubclass(api_mixin_cls, BasePostAPIFunctionProtocol): + http_method = HTTPMethod.POST + break + elif issubclass(api_mixin_cls, BaseGetAPIFunctionProtocol): + http_method = HTTPMethod.GET + break + else: + raise RuntimeError + + req_params = None + req_json = None + req_data = None + match http_method: + case HTTPMethod.GET: + req_params = model_data + case HTTPMethod.POST: + if self._post_json: + req_json = model_data + else: + req_data = model_data + case _: + raise RuntimeError + + http_request = requests.Request( + method=http_method, + url=config.get_api_url(api_path=api_path), + headers=req_headers, + params=req_params, + json=req_json, + data=req_data, + ).prepare() + + attempts = config.http503_attempts + with requests.Session() as session: + while True: + http_response = session.send( + request=http_request, + verify=config.verify_ssl, + ) + + if http_response.status_code == 503: + if attempts < 1: + break + else: + attempts -= 1 + time.sleep(config.http503_attempts_interval) + else: + break + + http_response.raise_for_status() + + api_result_context = APIResultContext( + api_params=api_params, + http_response=http_response, + ) + + if issubclass(api_result_cls, BaseAPIResultModel): + set_alias_gen( + model_cls=api_result_cls, + alias_type='validation', + name_mapping_dict=self._name_mapping_dict, + ) + + result_extra_allow = ( + api_result_cls.model_config.get('extra') == 'allow' + ) + if config.result_extra_allow != result_extra_allow: + if config.result_extra_allow: + api_result_cls.model_config['extra'] = 'allow' + else: + api_result_cls.model_config['extra'] = ( + BaseAPIResultModel.model_config.get('extra') + ) + api_result_cls.model_rebuild(force=True) + + api_result = api_result_cls.model_validate_json( + json_data=http_response.content, + context=api_result_context, + ) + else: + try: + decoded_content = http_response.json() + except requests.JSONDecodeError: + decoded_content = http_response.text + + api_result = api_result_cls( + value=decoded_content, + context=api_result_context, + ) + + return api_result + + return api_function diff --git a/src/dynamix_sdk/bvs.py b/src/dynamix_sdk/bvs.py new file mode 100644 index 0000000..45d92dc --- /dev/null +++ b/src/dynamix_sdk/bvs.py @@ -0,0 +1,92 @@ +from dynamix_sdk import base, config + + +class BVSAuth(config.AuthenticatorProtocol): + _config: config.BaseConfigWithDomain + client_id: str + client_secret: str + password: str + user_name: str + + def __init__( + self, + url: str, + domain: str, + client_id: str, + client_secret: str, + user_name: str, + password: str, + verify_ssl: bool = True, + http503_attempts: int = 10, + http503_attempts_interval: int = 5, + result_extra_allow: bool = True, + ): + self._config = config.BaseConfigWithDomain( + url=url, + verify_ssl=verify_ssl, + domain=domain, + http503_attempts=http503_attempts, + http503_attempts_interval=http503_attempts_interval, + result_extra_allow=result_extra_allow, + ) + + self.client_id = client_id + self.client_secret = client_secret + self.user_name = user_name + self.password = password + + @property + def api(self): + return BVSAPI(config=self._config) + + def get_jwt(self): + return self.api.token( + client_id=self.client_id, + client_secret=self.client_secret, + grant_type='password', + password=self.password, + response_type='token', + scope='openid', + user_name=self.user_name, + ).access_token + + +class BVSTokenResult(base.BaseAPIResultModel): + access_token: str + + +class BVSTokenProtocol(base.BasePostAPIFunctionProtocol): + def token( + self, + *, + client_id: str, + client_secret: str, + grant_type: str, + password: str, + response_type: str, + scope: str, + user_name: str, + ) -> BVSTokenResult: + ... + + +class BVSAPI( + base.BaseAPI, + BVSTokenProtocol, + path_mapping_dict={ + 'token': 'realms/[domain]/protocol/openid-connect/token', + }, + name_mapping_dict={ + 'access_token': 'access_token', + 'client_id': 'client_id', + 'client_secret': 'client_secret', + 'grant_type': 'grant_type', + 'password': 'password', + 'response_type': 'response_type', + 'scope': 'scope', + 'user_name': 'username', + 'user_name': 'username', + }, + post_json=False, +): + pass diff --git a/src/dynamix_sdk/config.py b/src/dynamix_sdk/config.py new file mode 100644 index 0000000..5f92b00 --- /dev/null +++ b/src/dynamix_sdk/config.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +import re +from typing import Protocol + + +@dataclass(kw_only=True) +class Config: + url: str + base_api_path: str = '' + verify_ssl: bool + http503_attempts: int = 10 + http503_attempts_interval: int = 5 + result_extra_allow: bool = False + + def get_api_url(self, api_path: str): + substitutions = re.findall(r'\[\w+\]', api_path) + _api_path = api_path[:] + for s in substitutions: + attr_name = f'{s.strip("[]")}' + attr_value = getattr(self, attr_name) + _api_path = _api_path.replace(s, attr_value) + + return f'{self.url}{self.base_api_path}{_api_path}' + + +class AuthenticatorProtocol(Protocol): + def get_jwt(self) -> str: + ... + + +@dataclass(kw_only=True) +class ConfigWithAuth(Config): + auth: str | AuthenticatorProtocol + + @property + def jwt(self): + if isinstance(self.auth, str): + return self.auth + else: + return self.auth.get_jwt() + + +@dataclass(kw_only=True) +class BaseConfigWithDomain(Config): + domain: str diff --git a/src/dynamix_sdk/decs3o.py b/src/dynamix_sdk/decs3o.py new file mode 100644 index 0000000..cdac3c5 --- /dev/null +++ b/src/dynamix_sdk/decs3o.py @@ -0,0 +1,74 @@ +from dynamix_sdk import base, config + + +class DECS3OAuth(config.AuthenticatorProtocol): + _config: config.Config + client_id: str + client_secret: str + + def __init__( + self, + url: str, + client_id: str, + client_secret: str, + verify_ssl: bool = True, + http503_attempts: int = 10, + http503_attempts_interval: int = 5, + result_extra_allow: bool = True, + ): + self._config = config.Config( + base_api_path='/v1', + url=url, + verify_ssl=verify_ssl, + http503_attempts=http503_attempts, + http503_attempts_interval=http503_attempts_interval, + result_extra_allow=result_extra_allow, + ) + + self.client_id = client_id + self.client_secret = client_secret + + @property + def api(self): + return DECS3OAPI(config=self._config) + + def get_jwt(self): + return self.api.oauth__access_token( + client_id=self.client_id, + client_secret=self.client_secret, + grant_type='client_credentials', + response_type='id_token', + ) + + +class DECS3OOauthAccesstokenResultStr(base.BaseAPIResultStr): + pass + + +class DECS3OOauthAccesstokenProtocol(base.BasePostAPIFunctionProtocol): + def oauth__access_token( + self, + *, + client_id: str, + client_secret: str, + grant_type: None | str = None, + response_type: None | str = None, + validity: None | int = None, + ) -> DECS3OOauthAccesstokenResultStr: + ... + + +class DECS3OAPI( + base.BaseAPI, + DECS3OOauthAccesstokenProtocol, + path_mapping_dict={'oauth__access_token': 'oauth/access_token'}, + name_mapping_dict={ + 'client_id': 'client_id', + 'client_secret': 'client_secret', + 'grant_type': 'grant_type', + 'response_type': 'response_type', + 'validity': 'validity', + }, + post_json=False, +): + pass diff --git a/src/dynamix_sdk/dynamix.py b/src/dynamix_sdk/dynamix.py new file mode 100644 index 0000000..2d91835 --- /dev/null +++ b/src/dynamix_sdk/dynamix.py @@ -0,0 +1,30 @@ +from dynamix_sdk import config +from .api import API + + +class Dynamix: + _config: config.ConfigWithAuth + + def __init__( + self, + *, + url: str, + auth: str | config.AuthenticatorProtocol, + verify_ssl: bool = True, + http503_attempts: int = 10, + http503_attempts_interval: int = 5, + result_extra_allow: bool = False, + ): + self._config = config.ConfigWithAuth( + base_api_path='/restmachine', + url=url, + auth=auth, + verify_ssl=verify_ssl, + http503_attempts=http503_attempts, + http503_attempts_interval=http503_attempts_interval, + result_extra_allow=result_extra_allow, + ) + + @property + def api(self): + return API(config=self._config) diff --git a/src/dynamix_sdk/types.py b/src/dynamix_sdk/types.py new file mode 100644 index 0000000..d3b0405 --- /dev/null +++ b/src/dynamix_sdk/types.py @@ -0,0 +1,10 @@ +from .base import ( + BaseAPIParamsModel, + BaseAPIParamsNestedModel, + BaseAPIResultModel, + BaseAPIResultNestedModel, + BaseAPIResultStr, + BaseAPIResultInt, + BaseAPIResultBool, +) +from .api import * diff --git a/src/dynamix_sdk/utils.py b/src/dynamix_sdk/utils.py new file mode 100644 index 0000000..718f7bd --- /dev/null +++ b/src/dynamix_sdk/utils.py @@ -0,0 +1,21 @@ +from enum import Enum, auto as enum_auto + + +class AutoNameEnum(Enum): + def _generate_next_value_(name, start, count, last_values): + return name + + +class EmptyStr(str, Enum): + EMPTY_STR = '' + + +class HTTPMethod(str, AutoNameEnum): + POST = enum_auto() + GET = enum_auto() + + +def gen_cls_name_from_url_path(url_path: str, postfix: str = ''): + parts = url_path.strip('/').replace('-', '').split('/') + base = ''.join(f'{p[0].upper()}{p[1:]}' for p in parts) + return f'{base}{postfix}'