From af86e210a94773ac8125a09137eeb84ee02b941c Mon Sep 17 00:00:00 2001 From: Maxim Mosin Date: Mon, 6 Mar 2023 01:10:29 +0900 Subject: [PATCH 1/7] Add mail to roadmap, add image_id to vds --- README.md | 1 + src/timeweb/async_api/vds.py | 35 ++++++++++++++++++----------------- src/timeweb/sync_api/vds.py | 17 ++++++++++++----- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 8ee29b1..146a807 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ print(account_status) - [x] Kubernetes - [x] S3-хранилище - [x] SSH-ключи + - [ ] Почта ## Etag Etag - это уникальный идентификатор, который используется для проверки изменений в API. Он будет использоваться чтобы определять текущею версию Swagger API, т.к. сейчас Swagger API Timeweb Cloud не имеет версионности и/или changelog'а. Получается он из запроса к спецификации OpenAPI по ссылке https://timeweb.cloud/api-docs-data/bundle.json. Сервер сам его возвращает и мы пока операемся на него. В будущем, когда/если Swagger API Timeweb Cloud будет иметь версионность и/или changelog, будем опираться на их версию Swagger API. \ No newline at end of file diff --git a/src/timeweb/async_api/vds.py b/src/timeweb/async_api/vds.py index 51b46a7..d34d08b 100644 --- a/src/timeweb/async_api/vds.py +++ b/src/timeweb/async_api/vds.py @@ -78,9 +78,9 @@ async def delete(self, server_id: int) -> bool: return status.is_success async def create( - self, name: str, os_id: int, is_ddos_guard: bool, - bandwidth: int, preset_id: int | None = None, - configurator: dict[str, int] | None = None, + self, name: str, is_ddos_guard: bool, bandwidth: int, + os_id: int | None = None, preset_id: int | None = None, + configurator: dict[str, int] | None = None, image_id: int | None = None, software_id: int | None = None, avatar_id: str | None = None, comment: str | None = None, ssh_keys_ids: list[int] | None = None, is_local_network: bool | None = None @@ -89,9 +89,10 @@ async def create( Args: name (str): Имя сервера. - os_id (int): UID ОС сервера. is_ddos_guard (bool): Защита от DDOS. bandwidth (int): Пропускная способность. + os_id (int | None, optional): UID ОС сервера. Defaults to None. + image_id (int | None, optional): UID образа. Defaults to None. preset_id (int | None, optional): UID тарифа. Defaults to None. configurator (dict[str, int] | None, optional): Объект конфигуратора. Defaults to None. software_id (int | None, optional): UID ПО сервера. Defaults to None. @@ -107,14 +108,11 @@ async def create( schemas.VDSResponse: Созданный облачный сервер. ''' if not configurator and not preset_id: - raise ValueError( - 'Обязательно нужно указать configurator или preset_id!') + raise ValueError('Обязательно нужно указать configurator или preset_id!') if configurator and preset_id: - raise ValueError( - 'Нельзя указать одновременно configurator и preset_id!') + raise ValueError('Нельзя указать одновременно configurator и preset_id!') if bandwidth not in range(100, 1100, 100): - raise ValueError( - 'Указаный bandwidth не подходит, только число от 100 до 1000 с шагом 100!') + raise ValueError('Указаный bandwidth не подходит, только число от 100 до 1000 с шагом 100!') if len(name) > 255: raise ValueError('name не может превышать 255 символов!') if comment and len(comment) > 255: @@ -132,10 +130,16 @@ async def create( raise ValueError( 'В объекте configurator отсутствуют необходимые значения!' ) + if os_id and image_id: + raise ValueError('Нельзя одновременно использовать "os_id" и "image_id"!') server_param = { - 'name': name, 'os_id': os_id, + 'name': name, 'is_ddos_guard': is_ddos_guard, 'bandwidth': bandwidth } + if os_id: + server_param['os_id'] = os_id + if image_id: + server_param['image_id'] = image_id if preset_id: server_param['preset_id'] = preset_id if configurator: @@ -591,18 +595,15 @@ async def change_autobackup_settings( else: data['creation_start_at'] = creation_start_at if copy_count not in range(1, 100): - raise ValueError( - '"copy_count" должен быть числом в интервале 1-99!') + raise ValueError('"copy_count" должен быть числом в интервале 1-99!') else: data['copy_count'] = copy_count if interval == 'week': if not day_of_week: - raise ValueError( - 'При значении "interval": "week", "day_of_week" обязателен!') + raise ValueError('При значении "interval": "week", "day_of_week" обязателен!') else: if day_of_week not in range(1, 8): - raise ValueError( - '"day_of_week" число в интервале 1-7!') + raise ValueError('"day_of_week" число в интервале 1-7!') else: data['day_of_week'] = day_of_week settings = await self._request( diff --git a/src/timeweb/sync_api/vds.py b/src/timeweb/sync_api/vds.py index dbff3a7..2af0517 100644 --- a/src/timeweb/sync_api/vds.py +++ b/src/timeweb/sync_api/vds.py @@ -78,9 +78,9 @@ def delete(self, server_id: int) -> bool: return status.is_success def create( - self, name: str, os_id: int, is_ddos_guard: bool, - bandwidth: int, preset_id: int | None = None, - configurator: dict[str, int] | None = None, + self, name: str, is_ddos_guard: bool, bandwidth: int, + os_id: int | None = None, preset_id: int | None = None, + configurator: dict[str, int] | None = None, image_id: int | None = None, software_id: int | None = None, avatar_id: str | None = None, comment: str | None = None, ssh_keys_ids: list[int] | None = None, is_local_network: bool | None = None @@ -89,9 +89,10 @@ def create( Args: name (str): Имя сервера. - os_id (int): UID ОС сервера. is_ddos_guard (bool): Защита от DDOS. bandwidth (int): Пропускная способность. + os_id (int | None, optional): UID ОС сервера. Defaults to None. + image_id (int | None, optional): UID образа. Defaults to None. preset_id (int | None, optional): UID тарифа. Defaults to None. configurator (dict[str, int] | None, optional): Объект конфигуратора. Defaults to None. software_id (int | None, optional): UID ПО сервера. Defaults to None. @@ -129,10 +130,16 @@ def create( raise ValueError( 'В объекте configurator отсутствуют необходимые значения!' ) + if os_id and image_id: + raise ValueError('Нельзя одновременно использовать "os_id" и "image_id"!') server_param = { - 'name': name, 'os_id': os_id, + 'name': name, 'is_ddos_guard': is_ddos_guard, 'bandwidth': bandwidth } + if os_id: + server_param['os_id'] = os_id + if image_id: + server_param['image_id'] = image_id if preset_id: server_param['preset_id'] = preset_id if configurator: From 3b36d4b4e9295e51308d22b082ca156974946883 Mon Sep 17 00:00:00 2001 From: Maxim Mosin Date: Mon, 6 Mar 2023 14:49:04 +0900 Subject: [PATCH 2/7] Add balancer delete confirmation --- src/timeweb/async_api/balancers.py | 37 +++++++++++++++++++--- src/timeweb/schemas/balancers/__init__.py | 3 +- src/timeweb/schemas/balancers/balancers.py | 7 +++- src/timeweb/sync_api/balancers.py | 37 +++++++++++++++++++--- 4 files changed, 74 insertions(+), 10 deletions(-) diff --git a/src/timeweb/async_api/balancers.py b/src/timeweb/async_api/balancers.py index 1341b1c..31192b9 100644 --- a/src/timeweb/async_api/balancers.py +++ b/src/timeweb/async_api/balancers.py @@ -5,6 +5,8 @@ Документация: https://timeweb.cloud/api-docs#tag/Balansirovshiki''' import logging +import warnings +from datetime import timedelta from ipaddress import IPv4Address, IPv6Address from httpx import AsyncClient @@ -176,19 +178,46 @@ async def update( ) return schemas.BalancerResponse(**balancer.json()) - async def delete(self, balancer_id: int) -> bool: + async def delete(self, balancer_id: int) -> bool | schemas.BalancerDelete: '''Удалить балансировщик. Args: balancer_id (int): UID балансировщика. Returns: - bool: Успешность удаления. + bool | schemas.BalancerDelete: Успешность удаления. Или хэш для подтверждения. ''' - await self._request( + status = await self._request( 'DELETE', f'/balancers/{balancer_id}' ) - return True + if status.status_code == 204: + return True + elif status.status_code == 200: + return schemas.BalancerDelete(**status.json()) + else: + return False + + async def confirm_delete( + self, balancer_id: int, hash: str, code: str + ) -> bool: + params = { + 'hash': hash, + 'code': code + } + status = await self._request( + 'DELETE', f'/balancers/{balancer_id}', + params=params + ) + if status.status_code == 204 and status.elapsed > timedelta(seconds=2): + return True + else: + if status.status_code == 204: + warnings.warn( + 'API слишком быстро подтвердил удаление. ' + 'Возможно он врёт. Проверьте хэш!' + ) + return True + return False async def get_balancer_ips(self, balancer_id: int) -> schemas.BalancerIPsResponse: '''Получить IP балансировщика. diff --git a/src/timeweb/schemas/balancers/__init__.py b/src/timeweb/schemas/balancers/__init__.py index 722c717..c702f70 100644 --- a/src/timeweb/schemas/balancers/__init__.py +++ b/src/timeweb/schemas/balancers/__init__.py @@ -8,5 +8,6 @@ Balancer, BalancerRule, BalancerStatus, BalancerAlgorithm, Protocol, BalancerResponse, BalancersResponse, BalancerRuleResponse, - BalancerRulesResponse, BalancerIPsResponse + BalancerRulesResponse, BalancerIPsResponse, + BalancerDelete ) diff --git a/src/timeweb/schemas/balancers/balancers.py b/src/timeweb/schemas/balancers/balancers.py index 0e35af5..7a85430 100644 --- a/src/timeweb/schemas/balancers/balancers.py +++ b/src/timeweb/schemas/balancers/balancers.py @@ -6,7 +6,7 @@ from pydantic import Field -from ..base import ResponseWithMeta, BaseResponse, BaseData +from ..base import ResponseWithMeta, BaseResponse, BaseData, BaseDelete class Protocol(str, Enum): @@ -107,3 +107,8 @@ class BalancerRulesResponse(ResponseWithMeta): class BalancerIPsResponse(ResponseWithMeta): '''Ответ со списком IP адресов балансировщика''' ips: list[str | IPv4Address | IPv6Address] = Field(..., description='Список IP адресов балансировщика.') + + +class BalancerDelete(BaseResponse): + '''Ответ с хэшом для подтверждения удаления балансировщика''' + balancer_delete: BaseDelete diff --git a/src/timeweb/sync_api/balancers.py b/src/timeweb/sync_api/balancers.py index b08cfb5..ca4404a 100644 --- a/src/timeweb/sync_api/balancers.py +++ b/src/timeweb/sync_api/balancers.py @@ -5,6 +5,8 @@ Документация: https://timeweb.cloud/api-docs#tag/Balansirovshiki''' import logging +import warnings +from datetime import timedelta from ipaddress import IPv4Address, IPv6Address from httpx import Client @@ -176,19 +178,46 @@ def update( ) return schemas.BalancerResponse(**balancer.json()) - def delete(self, balancer_id: int) -> bool: + def delete(self, balancer_id: int) -> bool | schemas.BalancerDelete: '''Удалить балансировщик. Args: balancer_id (int): UID балансировщика. Returns: - bool: Успешность удаления. + bool | schemas.BalancerDelete: Успешность удаления. Или хэш для подтверждения. ''' - self._request( + status = self._request( 'DELETE', f'/balancers/{balancer_id}' ) - return True + if status.status_code == 204: + return True + elif status.status_code == 200: + return schemas.BalancerDelete(**status.json()) + else: + return False + + def confirm_delete( + self, balancer_id: int, hash: str, code: str + ) -> bool: + params = { + 'hash': hash, + 'code': code + } + status = self._request( + 'DELETE', f'/balancers/{balancer_id}', + params=params + ) + if status.status_code == 204 and status.elapsed > timedelta(seconds=2): + return True + else: + if status.status_code == 204: + warnings.warn( + 'API слишком быстро подтвердил удаление. ' + 'Возможно он врёт. Проверьте хэш!' + ) + return True + return False def get_balancer_ips(self, balancer_id: int) -> schemas.BalancerIPsResponse: '''Получить IP балансировщика. From bae712ed24e088a44ad797800913c99dbb212602 Mon Sep 17 00:00:00 2001 From: Maxim Mosin Date: Mon, 6 Mar 2023 14:56:33 +0900 Subject: [PATCH 3/7] Add DB delete confirmation --- src/timeweb/async_api/dbs.py | 49 +++++++++++++++++++++++++++++----- src/timeweb/schemas/dbs/dbs.py | 7 ++++- src/timeweb/sync_api/dbs.py | 49 +++++++++++++++++++++++++++++----- 3 files changed, 92 insertions(+), 13 deletions(-) diff --git a/src/timeweb/async_api/dbs.py b/src/timeweb/async_api/dbs.py index 9f58741..70fd510 100644 --- a/src/timeweb/async_api/dbs.py +++ b/src/timeweb/async_api/dbs.py @@ -8,6 +8,8 @@ Документация: https://timeweb.cloud/api-docs#tag/Bazy-dannyh''' import logging +import warnings +from datetime import timedelta from httpx import AsyncClient @@ -134,19 +136,54 @@ async def update( ) return schemas.DatabaseResponse(**db.json()) - async def delete(self, db_id: int) -> bool: - '''Удалить базу данных. + async def delete(self, db_id: int) -> bool | schemas.DatabaseDelete: + '''Удалить БД. Args: - db_id (int): ID базы данных. + db_id (int): UID балансировщика. Returns: - bool: True, если база данных успешно удалена. + bool | schemas.DatabaseDelete: Успешность удаления. Или хэш для подтверждения. ''' - await self._request( + status = await self._request( 'DELETE', f'/dbs/{db_id}' ) - return True + if status.status_code == 204: + return True + elif status.status_code == 200: + return schemas.DatabaseDelete(**status.json()) + else: + return False + + async def confirm_delete(self, db_id: int, hash: str, code: str) -> bool: + '''Подтвердить удаление БД. + + Args: + db_id (int): UID базы данных. + hash (str): Хэш подтверждения удаление из `self.delete`. + code (str): Код подтверждения удаления. + + Returns: + bool: БД удалена? + ''' + params = { + 'hash': hash, + 'code': code + } + status = await self._request( + 'DELETE', f'/dbs/{db_id}', + params=params + ) + if status.status_code == 204 and status.elapsed > timedelta(seconds=2): + return True + else: + if status.status_code == 204: + warnings.warn( + 'API слишком быстро подтвердил удаление. ' + 'Возможно он врёт. Проверьте хэш!' + ) + return True + return False async def get_backups( self, db_id: int, limit: int = 100, offset: int = 0 diff --git a/src/timeweb/schemas/dbs/dbs.py b/src/timeweb/schemas/dbs/dbs.py index 43dc672..bb7e7c6 100644 --- a/src/timeweb/schemas/dbs/dbs.py +++ b/src/timeweb/schemas/dbs/dbs.py @@ -6,7 +6,7 @@ from pydantic import Field -from ..base import ResponseWithMeta, BaseResponse, BaseData +from ..base import ResponseWithMeta, BaseResponse, BaseData, BaseDelete class DBType(str, Enum): @@ -140,3 +140,8 @@ class DBArray(ResponseWithMeta): class DatabaseResponse(BaseResponse): """Response with database.""" db: Database = Field(..., description='База данных.') + + +class DatabaseDelete(BaseResponse): + '''Ответ с хэшом для подтверждения удаления БД''' + database_delete: BaseDelete diff --git a/src/timeweb/sync_api/dbs.py b/src/timeweb/sync_api/dbs.py index 1af4d46..a1738a4 100644 --- a/src/timeweb/sync_api/dbs.py +++ b/src/timeweb/sync_api/dbs.py @@ -8,6 +8,8 @@ Документация: https://timeweb.cloud/api-docs#tag/Bazy-dannyh''' import logging +import warnings +from datetime import timedelta from httpx import Client @@ -134,19 +136,54 @@ def update( ) return schemas.DatabaseResponse(**db.json()) - def delete(self, db_id: int) -> bool: - '''Удалить базу данных. + def delete(self, db_id: int) -> bool | schemas.DatabaseDelete: + '''Удалить БД. Args: - db_id (int): ID базы данных. + db_id (int): UID балансировщика. Returns: - bool: True, если база данных успешно удалена. + bool | schemas.DatabaseDelete: Успешность удаления. Или хэш для подтверждения. ''' - self._request( + status = self._request( 'DELETE', f'/dbs/{db_id}' ) - return True + if status.status_code == 204: + return True + elif status.status_code == 200: + return schemas.DatabaseDelete(**status.json()) + else: + return False + + def confirm_delete(self, db_id: int, hash: str, code: str) -> bool: + '''Подтвердить удаление БД. + + Args: + db_id (int): UID базы данных. + hash (str): Хэш подтверждения удаление из `self.delete`. + code (str): Код подтверждения удаления. + + Returns: + bool: БД удалена? + ''' + params = { + 'hash': hash, + 'code': code + } + status = self._request( + 'DELETE', f'/dbs/{db_id}', + params=params + ) + if status.status_code == 204 and status.elapsed > timedelta(seconds=2): + return True + else: + if status.status_code == 204: + warnings.warn( + 'API слишком быстро подтвердил удаление. ' + 'Возможно он врёт. Проверьте хэш!' + ) + return True + return False def get_backups( self, db_id: int, limit: int = 100, offset: int = 0 From 5da799d45796efd01d1a85ed2a4c1bbcd16b7e7b Mon Sep 17 00:00:00 2001 From: Maxim Mosin Date: Mon, 6 Mar 2023 14:58:09 +0900 Subject: [PATCH 4/7] Add docstring to balancers.confirm_delete --- src/timeweb/sync_api/balancers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/timeweb/sync_api/balancers.py b/src/timeweb/sync_api/balancers.py index ca4404a..6b177b4 100644 --- a/src/timeweb/sync_api/balancers.py +++ b/src/timeweb/sync_api/balancers.py @@ -200,6 +200,16 @@ def delete(self, balancer_id: int) -> bool | schemas.BalancerDelete: def confirm_delete( self, balancer_id: int, hash: str, code: str ) -> bool: + '''Подтвердить удаление балансировщика. + + Args: + balancer_id (int): UID балансировщика. + hash (str): Хэш подтверждения удаление из `self.delete`. + code (str): Код для подтверждения удаления. + + Returns: + bool: Балансировщик удалён? + ''' params = { 'hash': hash, 'code': code From d226a305955df34243878f4b35464d7bb9a879e6 Mon Sep 17 00:00:00 2001 From: Maxim Mosin Date: Mon, 6 Mar 2023 15:05:47 +0900 Subject: [PATCH 5/7] Add VDS delete confirmation --- src/timeweb/async_api/vds.py | 48 ++++++++++++++++--- src/timeweb/schemas/servers/__init__.py | 1 + src/timeweb/schemas/servers/cloud/__init__.py | 2 +- src/timeweb/schemas/servers/cloud/cloud.py | 7 ++- src/timeweb/sync_api/dbs.py | 2 +- src/timeweb/sync_api/vds.py | 48 ++++++++++++++++--- 6 files changed, 93 insertions(+), 15 deletions(-) diff --git a/src/timeweb/async_api/vds.py b/src/timeweb/async_api/vds.py index d34d08b..723a1a7 100644 --- a/src/timeweb/async_api/vds.py +++ b/src/timeweb/async_api/vds.py @@ -8,7 +8,8 @@ Документация: https://timeweb.cloud/api-docs#tag/Oblachnye-servery''' import logging -from datetime import datetime, date +import warnings +from datetime import datetime, date, timedelta from ipaddress import IPv4Address, IPv6Address from httpx import AsyncClient @@ -63,19 +64,54 @@ async def get(self, server_id: int) -> schemas.VDSResponse: ) return schemas.VDSResponse(**vds.json()) - async def delete(self, server_id: int) -> bool: - '''Удаление сервера. + async def delete(self, server_id: int) -> bool | schemas.VDSDelete: + '''Удалить облачный сервер. Args: - server_id (int): UID сервера. + server_id (int): UID облачного сервера. Returns: - bool: Сервер удалён? + bool | schemas.VDSDelete: Успешность удаления. Или хэш для подтверждения. ''' status = await self._request( 'DELETE', f'/servers/{server_id}' ) - return status.is_success + if status.status_code == 204: + return True + elif status.status_code == 200: + return schemas.VDSDelete(**status.json()) + else: + return False + + async def confirm_delete(self, server_id: int, hash: str, code: str) -> bool: + '''Подтвердить удаление облачный сервера. + + Args: + server_id (int): UID облачного сервера. + hash (str): Хэш подтверждения удаление из `self.delete`. + code (str): Код подтверждения удаления. + + Returns: + bool: БД удалена? + ''' + params = { + 'hash': hash, + 'code': code + } + status = await self._request( + 'DELETE', f'/servers/{server_id}', + params=params + ) + if status.status_code == 204 and status.elapsed > timedelta(seconds=2): + return True + else: + if status.status_code == 204: + warnings.warn( + 'API слишком быстро подтвердил удаление. ' + 'Возможно он врёт. Проверьте хэш!' + ) + return True + return False async def create( self, name: str, is_ddos_guard: bool, bandwidth: int, diff --git a/src/timeweb/schemas/servers/__init__.py b/src/timeweb/schemas/servers/__init__.py index 3c8db21..812054d 100644 --- a/src/timeweb/schemas/servers/__init__.py +++ b/src/timeweb/schemas/servers/__init__.py @@ -5,3 +5,4 @@ dedics - выделенные сервера. cloud - виртуальные сервера.''' from . import dedics +from . import cloud diff --git a/src/timeweb/schemas/servers/cloud/__init__.py b/src/timeweb/schemas/servers/cloud/__init__.py index fce839a..dc68755 100644 --- a/src/timeweb/schemas/servers/cloud/__init__.py +++ b/src/timeweb/schemas/servers/cloud/__init__.py @@ -2,5 +2,5 @@ # flake8: noqa '''Модели для работы с облачными серверами''' from .cloud import ( - VDS, VDSArray, VDSResponse + VDS, VDSArray, VDSResponse, VDSDelete ) diff --git a/src/timeweb/schemas/servers/cloud/cloud.py b/src/timeweb/schemas/servers/cloud/cloud.py index b4b9303..132fb38 100644 --- a/src/timeweb/schemas/servers/cloud/cloud.py +++ b/src/timeweb/schemas/servers/cloud/cloud.py @@ -6,7 +6,7 @@ from pydantic import Field -from ...base import ResponseWithMeta, BaseResponse, BaseData +from ...base import ResponseWithMeta, BaseResponse, BaseData, BaseDelete class OSNames(str, Enum): @@ -159,3 +159,8 @@ class VDSArray(ResponseWithMeta): class VDSResponse(BaseResponse): '''Ответ с сервером''' server: VDS = Field(..., description='Облачный сервер') + + +class VDSDelete(BaseResponse): + '''Ответ с хэшом для подтверждения удаления облачного сервера.''' + server_delete: BaseDelete diff --git a/src/timeweb/sync_api/dbs.py b/src/timeweb/sync_api/dbs.py index a1738a4..8565971 100644 --- a/src/timeweb/sync_api/dbs.py +++ b/src/timeweb/sync_api/dbs.py @@ -140,7 +140,7 @@ def delete(self, db_id: int) -> bool | schemas.DatabaseDelete: '''Удалить БД. Args: - db_id (int): UID балансировщика. + db_id (int): UID базы данных. Returns: bool | schemas.DatabaseDelete: Успешность удаления. Или хэш для подтверждения. diff --git a/src/timeweb/sync_api/vds.py b/src/timeweb/sync_api/vds.py index 2af0517..eb4ba9c 100644 --- a/src/timeweb/sync_api/vds.py +++ b/src/timeweb/sync_api/vds.py @@ -8,7 +8,8 @@ Документация: https://timeweb.cloud/api-docs#tag/Oblachnye-servery''' import logging -from datetime import datetime, date +import warnings +from datetime import datetime, date, timedelta from ipaddress import IPv4Address, IPv6Address from httpx import Client @@ -63,19 +64,54 @@ def get(self, server_id: int) -> schemas.VDSResponse: ) return schemas.VDSResponse(**vds.json()) - def delete(self, server_id: int) -> bool: - '''Удаление сервера. + def delete(self, server_id: int) -> bool | schemas.VDSDelete: + '''Удалить облачный сервер. Args: - server_id (int): UID сервера. + server_id (int): UID облачного сервера. Returns: - bool: Сервер удалён? + bool | schemas.VDSDelete: Успешность удаления. Или хэш для подтверждения. ''' status = self._request( 'DELETE', f'/servers/{server_id}' ) - return status.is_success + if status.status_code == 204: + return True + elif status.status_code == 200: + return schemas.VDSDelete(**status.json()) + else: + return False + + def confirm_delete(self, server_id: int, hash: str, code: str) -> bool: + '''Подтвердить удаление облачный сервера. + + Args: + server_id (int): UID облачного сервера. + hash (str): Хэш подтверждения удаление из `self.delete`. + code (str): Код подтверждения удаления. + + Returns: + bool: БД удалена? + ''' + params = { + 'hash': hash, + 'code': code + } + status = self._request( + 'DELETE', f'/servers/{server_id}', + params=params + ) + if status.status_code == 204 and status.elapsed > timedelta(seconds=2): + return True + else: + if status.status_code == 204: + warnings.warn( + 'API слишком быстро подтвердил удаление. ' + 'Возможно он врёт. Проверьте хэш!' + ) + return True + return False def create( self, name: str, is_ddos_guard: bool, bandwidth: int, From db5166ed4cf5dd778395fc0de049b0776f9391e9 Mon Sep 17 00:00:00 2001 From: Maxim Mosin Date: Mon, 6 Mar 2023 15:11:32 +0900 Subject: [PATCH 6/7] Add S3 delete confirmation --- src/timeweb/async_api/s3.py | 53 ++++++++++++++++++++++++++++++++---- src/timeweb/schemas/s3/s3.py | 7 ++++- src/timeweb/sync_api/s3.py | 53 ++++++++++++++++++++++++++++++++---- 3 files changed, 102 insertions(+), 11 deletions(-) diff --git a/src/timeweb/async_api/s3.py b/src/timeweb/async_api/s3.py index 59b6fd7..c7101ca 100644 --- a/src/timeweb/async_api/s3.py +++ b/src/timeweb/async_api/s3.py @@ -6,6 +6,8 @@ Документация: https://timeweb.cloud/api-docs#tag/S3-hranilishe''' import logging +import warnings +from datetime import timedelta from httpx import AsyncClient @@ -51,13 +53,54 @@ async def create( ) return schemas.BucketResponse(**bucket.json()) - async def delete(self, bucket_id: int) -> bool: - '''Удаление S3-хранилища + async def delete(self, bucket_id: int) -> bool | schemas.BucketDelete: + '''Удалить S3-хранилище. + Args: - bucket_id (int): ID хранилища. + bucket_id (int): UID хранилища. + + Returns: + bool | schemas.BucketDelete: Успешность удаления. Или хэш для подтверждения. ''' - await self._request('DELETE', f'/storages/buckets/{bucket_id}') - return True + status = await self._request( + 'DELETE', f'/storages/buckets/{bucket_id}' + ) + if status.status_code == 204: + return True + elif status.status_code == 200: + return schemas.BucketDelete(**status.json()) + else: + return False + + async def confirm_delete(self, bucket_id: int, hash: str, code: str) -> bool: + '''Подтвердить удаление S3-хранилища. + + Args: + bucket_id (int): UID хранилища. + hash (str): Хэш подтверждения удаление из `self.delete`. + code (str): Код подтверждения удаления. + + Returns: + bool: Хранилище удалено? + ''' + params = { + 'hash': hash, + 'code': code + } + status = await self._request( + 'DELETE', f'/storages/buckets/{bucket_id}', + params=params + ) + if status.status_code == 204 and status.elapsed > timedelta(seconds=2): + return True + else: + if status.status_code == 204: + warnings.warn( + 'API слишком быстро подтвердил удаление. ' + 'Возможно он врёт. Проверьте хэш!' + ) + return True + return False async def update( self, bucket_id: int, preset_id: int | None = None, diff --git a/src/timeweb/schemas/s3/s3.py b/src/timeweb/schemas/s3/s3.py index e5ea665..6c25372 100644 --- a/src/timeweb/schemas/s3/s3.py +++ b/src/timeweb/schemas/s3/s3.py @@ -4,7 +4,7 @@ from pydantic import Field -from ..base import ResponseWithMeta, BaseResponse, BaseData +from ..base import ResponseWithMeta, BaseResponse, BaseData, BaseDelete class BucketStatus(str, Enum): @@ -56,3 +56,8 @@ class BucketResponse(BaseResponse): class BucketArray(ResponseWithMeta): '''Модель ответа со списком S3-хранилищ''' buckets: list[Bucket] + + +class BucketDelete(BaseResponse): + '''Ответ с хэшом для подтверждения удаления S3-хранилища''' + bucket_delete: BaseDelete diff --git a/src/timeweb/sync_api/s3.py b/src/timeweb/sync_api/s3.py index 1a363bc..ca6568b 100644 --- a/src/timeweb/sync_api/s3.py +++ b/src/timeweb/sync_api/s3.py @@ -6,6 +6,8 @@ Документация: https://timeweb.cloud/api-docs#tag/S3-hranilishe''' import logging +import warnings +from datetime import timedelta from httpx import Client @@ -51,13 +53,54 @@ def create( ) return schemas.BucketResponse(**bucket.json()) - def delete(self, bucket_id: int) -> bool: - '''Удаление S3-хранилища + def delete(self, bucket_id: int) -> bool | schemas.BucketDelete: + '''Удалить S3-хранилище. + Args: - bucket_id (int): ID хранилища. + bucket_id (int): UID хранилища. + + Returns: + bool | schemas.BucketDelete: Успешность удаления. Или хэш для подтверждения. ''' - self._request('DELETE', f'/storages/buckets/{bucket_id}') - return True + status = self._request( + 'DELETE', f'/storages/buckets/{bucket_id}' + ) + if status.status_code == 204: + return True + elif status.status_code == 200: + return schemas.BucketDelete(**status.json()) + else: + return False + + def confirm_delete(self, bucket_id: int, hash: str, code: str) -> bool: + '''Подтвердить удаление S3-хранилища. + + Args: + bucket_id (int): UID хранилища. + hash (str): Хэш подтверждения удаление из `self.delete`. + code (str): Код подтверждения удаления. + + Returns: + bool: Хранилище удалено? + ''' + params = { + 'hash': hash, + 'code': code + } + status = self._request( + 'DELETE', f'/storages/buckets/{bucket_id}', + params=params + ) + if status.status_code == 204 and status.elapsed > timedelta(seconds=2): + return True + else: + if status.status_code == 204: + warnings.warn( + 'API слишком быстро подтвердил удаление. ' + 'Возможно он врёт. Проверьте хэш!' + ) + return True + return False def update( self, bucket_id: int, preset_id: int | None = None, From 795f6ab23ebfe89a6dce84a9156179c474bd716c Mon Sep 17 00:00:00 2001 From: Maxim Mosin Date: Mon, 6 Mar 2023 15:26:08 +0900 Subject: [PATCH 7/7] Up images API to latest version --- src/timeweb/async_api/images.py | 27 +++++++++++++++++++++------ src/timeweb/schemas/images/images.py | 8 +++++++- src/timeweb/sync_api/images.py | 27 +++++++++++++++++++++------ 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/timeweb/async_api/images.py b/src/timeweb/async_api/images.py index bb5859d..a0caba9 100644 --- a/src/timeweb/async_api/images.py +++ b/src/timeweb/async_api/images.py @@ -28,35 +28,50 @@ def __init__(self, token: str, client: AsyncClient | None = None): super().__init__(token, client) self.log = logging.getLogger('timeweb') - async def get_images(self, limit: int = 100, offset: int = 0) -> schemas.ImagesArray: + async def get_images( + self, limit: int = 100, offset: int = 0, with_deleted: bool = False + ) -> schemas.ImagesArray: '''Получение списка образов. Args: limit (int, optional): Количество элементов на странице. Defaults to 100. offset (int, optional): Смещение от начала списка. Defaults to 0. + with_deleted (bool, optional): Вернуть в том числе и удалённые? Default to False. Returns: ImagesArray: Список образов. ''' images = await self._request( 'GET', 'images', - params={'limit': limit, 'offset': offset} + params={ + 'limit': limit, 'offset': offset, 'with_deleted': with_deleted + } ) return schemas.ImagesArray(**images.json()) - async def create(self, description: str, disk_id: int) -> schemas.ImageResponse: + async def create( + self, name: str = '', description: str = '', disk_id: int | None = None + ) -> schemas.ImageResponse: '''Создание образа. Args: - description (str): Описание образа. - disk_id (int): Идентификатор диска, для которого создается образ + name (str, optional): Имя образа. Defaults to "". + description (str, optional): Описание образа. Defaults to "". + disk_id (int | None, optional): Идентификатор диска, для которого создается образ. Defaults to None. Returns: ImageResponse: Информация о созданном образе. ''' + data: dict[str, str | int] = {} + if name: + data['name'] = name + if description: + data['description'] = description + if disk_id: + data['disk_id'] = disk_id image = await self._request( 'POST', 'images', - json={'description': description, 'disk_id': disk_id} + json=data ) return schemas.ImageResponse(**image.json()) diff --git a/src/timeweb/schemas/images/images.py b/src/timeweb/schemas/images/images.py index 98a0eae..b304998 100644 --- a/src/timeweb/schemas/images/images.py +++ b/src/timeweb/schemas/images/images.py @@ -14,6 +14,7 @@ class ImageStatus(str, Enum): NEW = 'new' CREATED = 'created' FAILED = 'failed' + DELETED = 'deleted' class Image(BaseData): @@ -22,14 +23,18 @@ class Image(BaseData): status: ImageStatus = Field(..., description='Статус образа.') created_at: datetime = Field(..., description='Дата и время создания образа.') + deleted_at: datetime | None = None + size: int = Field(..., description='Размер образа в мегабайтах.') + name: str description: str = Field(..., description='Описание образа.') disk_id: int = Field( ..., description='Идентификатор связанного с образом диска.' ) - size: int = Field(..., description='Размер образа в мегабайтах.') location: str | None = Field( None, description='Локация, в которой создан образ' ) + os: str + progress: int class ImagesArray(ResponseWithMeta): @@ -67,6 +72,7 @@ class Download(BaseData): type: URLType = Field(..., description='Тип ссылки.') url: str = Field(..., description='Ссылка на скачивание.') status: URLStatus = Field(..., description='Статус создания.') + progress: int class DownloadsArray(ResponseWithMeta): diff --git a/src/timeweb/sync_api/images.py b/src/timeweb/sync_api/images.py index fe148a6..935e358 100644 --- a/src/timeweb/sync_api/images.py +++ b/src/timeweb/sync_api/images.py @@ -28,35 +28,50 @@ def __init__(self, token: str, client: Client | None = None): super().__init__(token, client) self.log = logging.getLogger('timeweb') - def get_images(self, limit: int = 100, offset: int = 0) -> schemas.ImagesArray: + def get_images( + self, limit: int = 100, offset: int = 0, with_deleted: bool = False + ) -> schemas.ImagesArray: '''Получение списка образов. Args: limit (int, optional): Количество элементов на странице. Defaults to 100. offset (int, optional): Смещение от начала списка. Defaults to 0. + with_deleted (bool, optional): Вернуть в том числе и удалённые? Default to False. Returns: ImagesArray: Список образов. ''' images = self._request( 'GET', 'images', - params={'limit': limit, 'offset': offset} + params={ + 'limit': limit, 'offset': offset, 'with_deleted': with_deleted + } ) return schemas.ImagesArray(**images.json()) - def create(self, description: str, disk_id: int) -> schemas.ImageResponse: + def create( + self, name: str = '', description: str = '', disk_id: int | None = None + ) -> schemas.ImageResponse: '''Создание образа. Args: - description (str): Описание образа. - disk_id (int): Идентификатор диска, для которого создается образ + name (str, optional): Имя образа. Defaults to "". + description (str, optional): Описание образа. Defaults to "". + disk_id (int | None, optional): Идентификатор диска, для которого создается образ. Defaults to None. Returns: ImageResponse: Информация о созданном образе. ''' + data: dict[str, str | int] = {} + if name: + data['name'] = name + if description: + data['description'] = description + if disk_id: + data['disk_id'] = disk_id image = self._request( 'POST', 'images', - json={'description': description, 'disk_id': disk_id} + json=data ) return schemas.ImageResponse(**image.json())