From 819ca44ad0d1195da970b93b9114f713f6681883 Mon Sep 17 00:00:00 2001 From: Yufei Li Date: Wed, 11 Mar 2020 18:54:53 +0800 Subject: [PATCH] Release/0.8.1 (#14) FEATURES: - Add ssl options of client (#14) - Add `ufs` product (#14) ENHANCEMENTS: - Add testing report use ucloud test framework (#14) - Update `DescribeSubnet` api (#14) --- Makefile | 10 +- docs/core.rst | 70 +++--- docs/helpers.rst | 26 +- docs/services.rst | 196 ++++++++------- docs/usage.rst | 192 +++++++-------- examples/two-tier/README.md | 38 +-- examples/uhost/README.md | 38 +-- tests/__init__.py | 2 +- tests/test_acceptance/conftest.py | 39 +++ tests/test_services/__init__.py | 2 +- tests/test_services/conftest.py | 56 ++--- tests/test_unit/__init__.py | 1 + tests/test_unit/test_core/__init__.py | 1 + tests/test_unit/test_core/test_auth.py | 26 ++ tests/test_unit/test_core/test_client.py | 77 ++++++ tests/test_unit/test_core/test_encoder.py | 23 ++ tests/test_unit/test_core/test_exc.py | 15 ++ tests/test_unit/test_core/test_fields.py | 60 +++++ tests/test_unit/test_core/test_schema.py | 189 ++++++++++++++ tests/test_unit/test_core/test_transport.py | 72 ++++++ tests/test_unit/test_helpers/__init__.py | 1 + tests/test_unit/test_helpers/test_utils.py | 13 + tests/test_unit/test_helpers/test_wait.py | 20 ++ tests/test_unit/test_testing/__init__.py | 1 + tests/test_unit/test_testing/test_utest.py | 35 +++ ucloud/client.py | 28 +++ ucloud/core/client/_cfg.py | 16 ++ ucloud/core/client/_client.py | 21 +- ucloud/core/transport/__init__.py | 4 +- ucloud/core/transport/_requests.py | 26 +- ucloud/core/transport/http.py | 12 + ucloud/core/utils/deco.py | 31 +++ ucloud/core/utils/middleware.py | 13 + ucloud/services/ipsecvpn/__init__.py | 1 + ucloud/services/ipsecvpn/client.py | 114 +++++++++ ucloud/services/ipsecvpn/schemas/__init__.py | 1 + ucloud/services/ipsecvpn/schemas/apis.py | 74 ++++++ ucloud/services/ipsecvpn/schemas/models.py | 104 ++++++++ ucloud/services/ufs/__init__.py | 1 + ucloud/services/ufs/client.py | 121 +++++++++ ucloud/services/ufs/schemas/__init__.py | 1 + ucloud/services/ufs/schemas/apis.py | 126 ++++++++++ ucloud/services/ufs/schemas/models.py | 29 +++ ucloud/services/uhub/__init__.py | 1 + ucloud/services/uhub/client.py | 188 ++++++++++++++ ucloud/services/uhub/schemas/__init__.py | 1 + ucloud/services/uhub/schemas/apis.py | 203 +++++++++++++++ ucloud/services/uhub/schemas/models.py | 42 ++++ ucloud/services/vpc/client.py | 244 ++++++++----------- ucloud/services/vpc/schemas/apis.py | 3 + ucloud/services/vpc/schemas/models.py | 6 +- ucloud/testing/driver/__init__.py | 7 + ucloud/testing/driver/_scenario.py | 94 +++++++ ucloud/testing/driver/_specification.py | 59 +++++ ucloud/testing/driver/_step.py | 150 ++++++++++++ ucloud/version.py | 2 +- 56 files changed, 2473 insertions(+), 453 deletions(-) create mode 100644 tests/test_acceptance/conftest.py create mode 100644 tests/test_unit/__init__.py create mode 100644 tests/test_unit/test_core/__init__.py create mode 100644 tests/test_unit/test_core/test_auth.py create mode 100644 tests/test_unit/test_core/test_client.py create mode 100644 tests/test_unit/test_core/test_encoder.py create mode 100644 tests/test_unit/test_core/test_exc.py create mode 100644 tests/test_unit/test_core/test_fields.py create mode 100644 tests/test_unit/test_core/test_schema.py create mode 100644 tests/test_unit/test_core/test_transport.py create mode 100644 tests/test_unit/test_helpers/__init__.py create mode 100644 tests/test_unit/test_helpers/test_utils.py create mode 100644 tests/test_unit/test_helpers/test_wait.py create mode 100644 tests/test_unit/test_testing/__init__.py create mode 100644 tests/test_unit/test_testing/test_utest.py create mode 100644 ucloud/core/utils/deco.py create mode 100644 ucloud/services/ipsecvpn/__init__.py create mode 100644 ucloud/services/ipsecvpn/client.py create mode 100644 ucloud/services/ipsecvpn/schemas/__init__.py create mode 100644 ucloud/services/ipsecvpn/schemas/apis.py create mode 100644 ucloud/services/ipsecvpn/schemas/models.py create mode 100644 ucloud/services/ufs/__init__.py create mode 100644 ucloud/services/ufs/client.py create mode 100644 ucloud/services/ufs/schemas/__init__.py create mode 100644 ucloud/services/ufs/schemas/apis.py create mode 100644 ucloud/services/ufs/schemas/models.py create mode 100644 ucloud/services/uhub/__init__.py create mode 100644 ucloud/services/uhub/client.py create mode 100644 ucloud/services/uhub/schemas/__init__.py create mode 100644 ucloud/services/uhub/schemas/apis.py create mode 100644 ucloud/services/uhub/schemas/models.py create mode 100644 ucloud/testing/driver/__init__.py create mode 100644 ucloud/testing/driver/_scenario.py create mode 100644 ucloud/testing/driver/_specification.py create mode 100644 ucloud/testing/driver/_step.py diff --git a/Makefile b/Makefile index 227eb03..ce548ab 100644 --- a/Makefile +++ b/Makefile @@ -80,10 +80,10 @@ clean-test: migrate: git clone https://github.com/ucloud/ucloud-sdk-python3.git .migrate - PYTHONPATH=. python scripts/migrate --source .migrate/ucloud --output ucloud - PYTHONPATH=. python scripts/migrate --source .migrate/tests --output tests - PYTHONPATH=. python scripts/migrate --source .migrate/docs --output docs - PYTHONPATH=. python scripts/migrate --source .migrate/examples --output examples - PYTHONPATH=. python scripts/migrate --source .migrate/README.md --output README.md + PYTHONPATH=. python3 scripts/migrate --source .migrate/ucloud --output ucloud + PYTHONPATH=. python3 scripts/migrate --source .migrate/tests --output tests + PYTHONPATH=. python3 scripts/migrate --source .migrate/docs --output docs + PYTHONPATH=. python3 scripts/migrate --source .migrate/examples --output examples + PYTHONPATH=. python3 scripts/migrate --source .migrate/README.md --output README.md sed -i 's/unicode/unicode # noqa: F821/g' ucloud/core/utils/compat.py rm -rf .migrate diff --git a/docs/core.rst b/docs/core.rst index e6545e5..bbaea47 100644 --- a/docs/core.rst +++ b/docs/core.rst @@ -1,35 +1,35 @@ -Core -==== - -Config ------- - -.. autoclass:: ucloud.core.auth.Credential - :members: - -.. autoclass:: ucloud.core.client.Config - :members: - -Client ------- - -.. autoclass:: ucloud.core.client.Client - :members: - -Transport ---------- - -.. autoclass:: ucloud.core.transport.RequestsTransport - :members: - -Middleware ----------- - -.. autoclass:: ucloud.core.utils.middleware.Middleware - :members: - -Testing -------- - -.. automodule:: ucloud.core.testing.env - :members: +Core +==== + +Config +------ + +.. autoclass:: ucloud.core.auth.Credential + :members: + +.. autoclass:: ucloud.core.client.Config + :members: + +Client +------ + +.. autoclass:: ucloud.core.client.Client + :members: + +Transport +--------- + +.. autoclass:: ucloud.core.transport.RequestsTransport + :members: + +Middleware +---------- + +.. autoclass:: ucloud.core.utils.middleware.Middleware + :members: + +Testing +------- + +.. automodule:: ucloud.core.testing.env + :members: diff --git a/docs/helpers.rst b/docs/helpers.rst index e8192e1..d9d40d4 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -1,13 +1,13 @@ -Helpers -======= - -Wait Resource State -------------------- - -.. autofunction:: ucloud.helpers.wait.wait_for_state - -Utilities ---------- - -.. automodule:: ucloud.helpers.utils - :members: +Helpers +======= + +Wait Resource State +------------------- + +.. autofunction:: ucloud.helpers.wait.wait_for_state + +Utilities +--------- + +.. automodule:: ucloud.helpers.utils + :members: diff --git a/docs/services.rst b/docs/services.rst index 7f9e0fa..9da0fce 100644 --- a/docs/services.rst +++ b/docs/services.rst @@ -1,86 +1,110 @@ -UCloud SDK Services -=================== - -PathX ------ - -.. autoclass:: ucloud.services.pathx.client.PathXClient - :members: - -StepFlow --------- - -.. autoclass:: ucloud.services.stepflow.client.StepFlowClient - :members: - -UAccount --------- - -.. autoclass:: ucloud.services.uaccount.client.UAccountClient - :members: - -UCDN ----- - -.. autoclass:: ucloud.services.ucdn.client.UCDNClient - :members: - -UDB ---- - -.. autoclass:: ucloud.services.udb.client.UDBClient - :members: - -UDPN ----- - -.. autoclass:: ucloud.services.udpn.client.UDPNClient - :members: - -UDisk ------ - -.. autoclass:: ucloud.services.udisk.client.UDiskClient - :members: - -UHost ------ - -.. autoclass:: ucloud.services.uhost.client.UHostClient - :members: - -ULB ---- - -.. autoclass:: ucloud.services.ulb.client.ULBClient - :members: - -UMem ----- - -.. autoclass:: ucloud.services.umem.client.UMemClient - :members: - -UNet ----- - -.. autoclass:: ucloud.services.unet.client.UNetClient - :members: - -UPHost ------- - -.. autoclass:: ucloud.services.uphost.client.UPHostClient - :members: - -USMS ----- - -.. autoclass:: ucloud.services.usms.client.USMSClient - :members: - -VPC ---- - -.. autoclass:: ucloud.services.vpc.client.VPCClient - :members: +UCloud SDK Services +=================== + +PathX +----- + +.. autoclass:: ucloud.services.pathx.client.PathXClient + :members: + +StepFlow +-------- + +.. autoclass:: ucloud.services.stepflow.client.StepFlowClient + :members: + +UAccount +-------- + +.. autoclass:: ucloud.services.uaccount.client.UAccountClient + :members: + +UCDN +---- + +.. autoclass:: ucloud.services.ucdn.client.UCDNClient + :members: + +UDB +--- + +.. autoclass:: ucloud.services.udb.client.UDBClient + :members: + +UDPN +---- + +.. autoclass:: ucloud.services.udpn.client.UDPNClient + :members: + +UDisk +----- + +.. autoclass:: ucloud.services.udisk.client.UDiskClient + :members: + +UHost +----- + +.. autoclass:: ucloud.services.uhost.client.UHostClient + :members: + +ULB +--- + +.. autoclass:: ucloud.services.ulb.client.ULBClient + :members: + +UMem +---- + +.. autoclass:: ucloud.services.umem.client.UMemClient + :members: + +UNet +---- + +.. autoclass:: ucloud.services.unet.client.UNetClient + :members: + +UPHost +------ + +.. autoclass:: ucloud.services.uphost.client.UPHostClient + :members: + +USMS +---- + +.. autoclass:: ucloud.services.usms.client.USMSClient + :members: + + + +IPSecVPN +-------- + +.. autoclass:: ucloud.services.ipsecvpn.client.IPSecVPNClient + :members: + + +UFS +--- + +.. autoclass:: ucloud.services.ufs.client.UFSClient + :members: + + +UHub +---- + +.. autoclass:: ucloud.services.uhub.client.UHubClient + :members: + + +VPC +--- + +.. autoclass:: ucloud.services.vpc.client.VPCClient + :members: + diff --git a/docs/usage.rst b/docs/usage.rst index 04986e5..bc0f2ed 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,96 +1,96 @@ -Usage -===== - -Type System ------------ - -UCloud Python SDK has type system for runtime checking. - -For example: - -.. code-block:: python - - client.uhost().create_uhost_instance({ - 'CPU': "i am not a integer", - }) - -it will raise :class:`ValidationException` with invalid integer and some required field is miss matched. - -Wait State Changed ------------------- - -SDK also provide state waiter helper to improver usage experience. - -**When using it?** - -Waiter can wait for remote state is achieved to target state. such as, - -- create and wait for resource state is completed. -- invoke/start/stop a resource and wait for it is finished. -- custom retry policies and wait for retrying is finished. - -For example: - -.. code-block:: python - - def mget_uhost_states(uhost_ids): - resp = client.uhost().describe_uhost_instance({'UHostIds': uhost_ids}) - return [inst.get('State') for inst in resp.get('UHostSet')] - - # Stop all instances - for uhost_id in uhost_ids: - client.uhost().stop_uhost_instance({'UHostId': uhost_id}) - - # Wait all instances is stopped - wait.wait_for_state( - target=['stopped'], pending=['pending'], - timeout=300, # wait 5min - refresh=lambda: ( - 'stopped' if all([state == 'Stopped' for state in mget_uhost_states(uhost_ids)]) else 'pending' - ), - ) - - # Remove all instances - for uhost_id in uhost_ids: - client.uhost().terminate_uhost_instance({'UHostId': uhost_id}) - -By the default, waiter will use exponential back-off delay between twice request. -it will raise :class:`WaitTimeoutException` when timeout is reached. - -Client/Transport Middleware ---------------------------- - -UCloud SDK provide middleware feature to client or transport level request. - -It allowed to add custom logic into the lifecycle of request/response. - -For example: - -.. code-block:: python - - @client.middleware.request - def log_params(req): - print('[REQ]', req) - - @client.middleware.response - def log_response(resp): - print('[RESP]', resp) - - -or transport: - -.. code-block:: python - - from ucloud.core.transport import RequestsTransport - - transport = RequestsTransport() - - @transport.middleware.request - def log_request(req): - print('[REQ]', req) - - @transport.middleware.response - def log_response(resp): - print('[RESP]', resp) - - Client({'Region': 'cn-bj2'}, transport=transport) +Usage +===== + +Type System +----------- + +UCloud Python SDK has type system for runtime checking. + +For example: + +.. code-block:: python + + client.uhost().create_uhost_instance({ + 'CPU': "i am not a integer", + }) + +it will raise :class:`ValidationException` with invalid integer and some required field is miss matched. + +Wait State Changed +------------------ + +SDK also provide state waiter helper to improver usage experience. + +**When using it?** + +Waiter can wait for remote state is achieved to target state. such as, + +- create and wait for resource state is completed. +- invoke/start/stop a resource and wait for it is finished. +- custom retry policies and wait for retrying is finished. + +For example: + +.. code-block:: python + + def mget_uhost_states(uhost_ids): + resp = client.uhost().describe_uhost_instance({'UHostIds': uhost_ids}) + return [inst.get('State') for inst in resp.get('UHostSet')] + + # Stop all instances + for uhost_id in uhost_ids: + client.uhost().stop_uhost_instance({'UHostId': uhost_id}) + + # Wait all instances is stopped + wait.wait_for_state( + target=['stopped'], pending=['pending'], + timeout=300, # wait 5min + refresh=lambda: ( + 'stopped' if all([state == 'Stopped' for state in mget_uhost_states(uhost_ids)]) else 'pending' + ), + ) + + # Remove all instances + for uhost_id in uhost_ids: + client.uhost().terminate_uhost_instance({'UHostId': uhost_id}) + +By the default, waiter will use exponential back-off delay between twice request. +it will raise :class:`WaitTimeoutException` when timeout is reached. + +Client/Transport Middleware +--------------------------- + +UCloud SDK provide middleware feature to client or transport level request. + +It allowed to add custom logic into the lifecycle of request/response. + +For example: + +.. code-block:: python + + @client.middleware.request + def log_params(req): + print('[REQ]', req) + + @client.middleware.response + def log_response(resp): + print('[RESP]', resp) + + +or transport: + +.. code-block:: python + + from ucloud.core.transport import RequestsTransport + + transport = RequestsTransport() + + @transport.middleware.request + def log_request(req): + print('[REQ]', req) + + @transport.middleware.response + def log_response(resp): + print('[RESP]', resp) + + Client({'Region': 'cn-bj2'}, transport=transport) diff --git a/examples/two-tier/README.md b/examples/two-tier/README.md index 371c7d4..7f8aab0 100644 --- a/examples/two-tier/README.md +++ b/examples/two-tier/README.md @@ -1,19 +1,19 @@ -# UCloud SDK Two-Tier Example - -## What is the goal - -Build a two-tier architecture with ulb and uhost, and remove all example data. - -## Setup Environment - -```go -export UCLOUD_PUBLIC_KEY="your public key" -export UCLOUD_PRIVATE_KEY="your private key" -export UCLOUD_PROJECT_ID="your project id" -``` - -## How to run - -```sh -python main.py -``` +# UCloud SDK Two-Tier Example + +## What is the goal + +Build a two-tier architecture with ulb and uhost, and remove all example data. + +## Setup Environment + +```go +export UCLOUD_PUBLIC_KEY="your public key" +export UCLOUD_PRIVATE_KEY="your private key" +export UCLOUD_PROJECT_ID="your project id" +``` + +## How to run + +```sh +python main.py +``` diff --git a/examples/uhost/README.md b/examples/uhost/README.md index 9aabc7e..e4553bc 100644 --- a/examples/uhost/README.md +++ b/examples/uhost/README.md @@ -1,19 +1,19 @@ -# UCloud SDK UHost Example - -## What is the goal - -Create an uhost, wait it created, and remove all example data. - -## Setup Environment - -```go -export UCLOUD_PUBLIC_KEY="your public key" -export UCLOUD_PRIVATE_KEY="your private key" -export UCLOUD_PROJECT_ID="your project id" -``` - -## How to run - -```sh -python main.py -``` +# UCloud SDK UHost Example + +## What is the goal + +Create an uhost, wait it created, and remove all example data. + +## Setup Environment + +```go +export UCLOUD_PUBLIC_KEY="your public key" +export UCLOUD_PRIVATE_KEY="your private key" +export UCLOUD_PROJECT_ID="your project id" +``` + +## How to run + +```sh +python main.py +``` diff --git a/tests/__init__.py b/tests/__init__.py index 4c48b5a..40a96af 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- diff --git a/tests/test_acceptance/conftest.py b/tests/test_acceptance/conftest.py new file mode 100644 index 0000000..ea2af57 --- /dev/null +++ b/tests/test_acceptance/conftest.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +import json +import os +import pytest +from ucloud.testing.driver import spec +from ucloud.client import Client + + +@pytest.fixture(scope="session", autouse=True, name="client") +def client_factory(): + return Client( + { + "region": "cn-bj2", + "project_id": os.getenv("UCLOUD_PROJECT_ID"), + "public_key": os.getenv("UCLOUD_PUBLIC_KEY"), + "private_key": os.getenv("UCLOUD_PRIVATE_KEY"), + "max_retries": 10, + "timeout": 60, + } + ) + + +@pytest.fixture(scope="module", autouse=True, name="variables") +def variables_factory(): + return { + "Region": "cn-bj2", + "Zone": "cn-bj2-05", + "ProjectId": os.getenv("UCLOUD_PROJECT_ID"), + } + + +@pytest.fixture(scope="session", autouse=True) +def save_report(request): + def save_report_handler(): + with open("./report.json", "w") as f: + json.dump(spec.json(), f) + + request.addfinalizer(save_report_handler) diff --git a/tests/test_services/__init__.py b/tests/test_services/__init__.py index 4c48b5a..40a96af 100644 --- a/tests/test_services/__init__.py +++ b/tests/test_services/__init__.py @@ -1 +1 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- diff --git a/tests/test_services/conftest.py b/tests/test_services/conftest.py index 194e927..5e4dca4 100644 --- a/tests/test_services/conftest.py +++ b/tests/test_services/conftest.py @@ -1,28 +1,28 @@ -# -*- coding: utf-8 -*- - -import os -import pytest -from ucloud.client import Client - - -@pytest.fixture(scope="session", autouse=True, name="client") -def client_factory(): - return Client( - { - "region": "cn-bj2", - "project_id": os.getenv("UCLOUD_PROJECT_ID"), - "public_key": os.getenv("UCLOUD_PUBLIC_KEY"), - "private_key": os.getenv("UCLOUD_PRIVATE_KEY"), - "max_retries": 10, - "timeout": 60, - } - ) - - -@pytest.fixture(scope="module", autouse=True, name="variables") -def variables_factory(): - return { - "Region": "cn-bj2", - "Zone": "cn-bj2-05", - "ProjectId": os.getenv("UCLOUD_PROJECT_ID"), - } +# -*- coding: utf-8 -*- + +import os +import pytest +from ucloud.client import Client + + +@pytest.fixture(scope="session", autouse=True, name="client") +def client_factory(): + return Client( + { + "region": "cn-bj2", + "project_id": os.getenv("UCLOUD_PROJECT_ID"), + "public_key": os.getenv("UCLOUD_PUBLIC_KEY"), + "private_key": os.getenv("UCLOUD_PRIVATE_KEY"), + "max_retries": 10, + "timeout": 60, + } + ) + + +@pytest.fixture(scope="module", autouse=True, name="variables") +def variables_factory(): + return { + "Region": "cn-bj2", + "Zone": "cn-bj2-05", + "ProjectId": os.getenv("UCLOUD_PROJECT_ID"), + } diff --git a/tests/test_unit/__init__.py b/tests/test_unit/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tests/test_unit/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/test_unit/test_core/__init__.py b/tests/test_unit/test_core/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tests/test_unit/test_core/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/test_unit/test_core/test_auth.py b/tests/test_unit/test_core/test_auth.py new file mode 100644 index 0000000..4128ff3 --- /dev/null +++ b/tests/test_unit/test_core/test_auth.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from ucloud.core import auth + + +def test_verify_ac(): + d = { + "Action": "CreateUHostInstance", + "CPU": 2, + "ChargeType": "Month", + "DiskSpace": 10, + "ImageId": "f43736e1-65a5-4bea-ad2e-8a46e18883c2", + "LoginMode": "Password", + "Memory": 2048, + "Name": "Host01", + "Password": "VUNsb3VkLmNu", + "PublicKey": "ucloudsomeone@example.com1296235120854146120", + "Quantity": 1, + "Region": "cn-bj2", + "Zone": "cn-bj2-04", + } + cred = auth.Credential( + "ucloudsomeone@example.com1296235120854146120", + "46f09bb9fab4f12dfc160dae12273d5332b5debe", + ) + assert cred.verify_ac(d) == "4f9ef5df2abab2c6fccd1e9515cb7e2df8c6bb65" diff --git a/tests/test_unit/test_core/test_client.py b/tests/test_unit/test_core/test_client.py new file mode 100644 index 0000000..4549389 --- /dev/null +++ b/tests/test_unit/test_core/test_client.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +import os +import pytest +import logging +from ucloud.client import Client +from ucloud.core import exc +from ucloud.testing.mock import MockedTransport + +logger = logging.getLogger(__name__) + + +@pytest.fixture(scope="session", autouse=True) +def client(): + return Client( + { + "region": "cn-bj2", + "public_key": "foo", + "private_key": "foo", + "timeout": 10, + "max_retries": 3, + "ssl_verify": False, + } + ) + + +@pytest.fixture(scope="function", autouse=True) +def transport(): + return MockedTransport() + + +class TestClient(object): + def test_client_invoke(self, client, transport): + transport.mock_data(lambda _: {"RetCode": 0, "Action": "Foo"}) + client.transport = transport + assert client.invoke("Foo") == {"RetCode": 0, "Action": "Foo"} + + def test_client_invoke_code_error(self, client, transport): + transport.mock_data(lambda _: {"RetCode": 171, "Action": "Foo"}) + client.transport = transport + with pytest.raises(exc.RetCodeException): + try: + client.invoke("Foo") + except exc.RetCodeException as e: + assert str(e) + expected = {"RetCode": 171, "Action": "Foo", "Message": ""} + assert e.json() == expected + raise e + + def test_client_invoke_with_retryable_error(self, client, transport): + transport.mock_data(lambda _: {"RetCode": 10000, "Action": "Foo"}) + client.transport = transport + with pytest.raises(exc.RetCodeException): + client.invoke("Foo") + + def test_client_invoke_with_unexpected_error(self, client, transport): + def raise_error(_): + raise ValueError("temporary error") + + transport.mock_data(raise_error) + client.transport = transport + with pytest.raises(ValueError): + client.invoke("Foo") + + def test_client_try_import(self, client): + assert client.pathx() + assert client.stepflow() + assert client.uaccount() + assert client.udb() + assert client.udpn() + assert client.udisk() + assert client.uhost() + assert client.ulb() + assert client.umem() + assert client.unet() + assert client.uphost() + assert client.vpc() diff --git a/tests/test_unit/test_core/test_encoder.py b/tests/test_unit/test_core/test_encoder.py new file mode 100644 index 0000000..083bf5a --- /dev/null +++ b/tests/test_unit/test_core/test_encoder.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +import pytest +from ucloud.core.typesystem import encoder + + +@pytest.mark.parametrize( + "input_vector,expected", + [ + ({"foo": "bar"}, {"foo": "bar"}), + ({"foo": 42}, {"foo": "42"}), + ({"foo": 42.42}, {"foo": "42.42"}), + ({"foo": 42.0}, {"foo": "42"}), + ({"foo": True}, {"foo": "true"}), + ({"foo": False}, {"foo": "false"}), + ({"IP": ["127.0.0.1"]}, {"IP.0": "127.0.0.1"}), + ({"IP": ["foo", "bar"]}, {"IP.0": "foo", "IP.1": "bar"}), + ({"IP": [{"foo": "bar"}]}, {"IP.0.foo": "bar"}), + ], +) +def test_params_encode(input_vector, expected): + result = encoder.encode(input_vector) + assert result == expected diff --git a/tests/test_unit/test_core/test_exc.py b/tests/test_unit/test_core/test_exc.py new file mode 100644 index 0000000..8777fd4 --- /dev/null +++ b/tests/test_unit/test_core/test_exc.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +import pytest +from ucloud.core import exc + + +def test_ret_code_error(): + assert not exc.UCloudException().retryable + code_error = exc.RetCodeException("Foo", 1, "") + assert str(code_error) + assert not code_error.retryable + assert code_error.json() == {"Action": "Foo", "Message": "", "RetCode": 1} + validate_error = exc.ValidationException(ValueError("invalid type")) + assert not validate_error.retryable + assert str(validate_error) diff --git a/tests/test_unit/test_core/test_fields.py b/tests/test_unit/test_core/test_fields.py new file mode 100644 index 0000000..4742d07 --- /dev/null +++ b/tests/test_unit/test_core/test_fields.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +import logging +import pytest +from ucloud.core import exc +from ucloud.core.typesystem import fields + +logger = logging.getLogger(__name__) + + +def test_field_str(): + s = fields.Str() + assert s.dumps(s.loads("foo")) == "foo" + with pytest.raises(exc.ValidationException): + fields.Str(strict=True).loads(42) + + +def test_field_int(): + i = fields.Int() + assert i.dumps(i.loads("42")) == 42 + with pytest.raises(exc.ValidationException): + fields.Int().dumps("foo") + with pytest.raises(exc.ValidationException): + fields.Int(strict=True).loads("42") + + +def test_field_float(): + f = fields.Float() + assert f.dumps(f.loads("42.0")) == 42.0 + with pytest.raises(exc.ValidationException): + fields.Float().dumps("foo") + with pytest.raises(exc.ValidationException): + fields.Float(strict=True).loads("42.0") + + +def test_field_bool(): + b = fields.Bool() + assert b.dumps(b.loads("true")) + assert not b.dumps(b.loads("false")) + with pytest.raises(exc.ValidationException): + fields.Bool().dumps("foo") + with pytest.raises(exc.ValidationException): + fields.Bool(strict=True).loads("true") + + +def test_field_list(): + l = fields.List(fields.Int()) + assert l.dumps(l.loads(["42"])) == [42] + with pytest.raises(exc.ValidationException): + fields.List(fields.Int(), strict=True).dumps(42) + with pytest.raises(exc.ValidationException): + fields.List(fields.Int()).dumps(["foo"]) + with pytest.raises(exc.ValidationException): + fields.List(fields.Int(strict=True)).loads(["42"]) + + +def test_field_base64(): + b64 = fields.Base64() + assert b64.loads("Zm9v") == "foo" + assert b64.dumps("foo") == "Zm9v" diff --git a/tests/test_unit/test_core/test_schema.py b/tests/test_unit/test_core/test_schema.py new file mode 100644 index 0000000..b696d24 --- /dev/null +++ b/tests/test_unit/test_core/test_schema.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- + +import logging +import pytest +from ucloud.core import exc +from ucloud.core.typesystem import fields, schema + +logger = logging.getLogger(__name__) + + +def test_request_basic(): + class Schema(schema.RequestSchema): + fields = { + "Foo": fields.Int(required=True), + "Bar": fields.Int(required=False), + } + + assert Schema().dumps({"Foo": "42"}) == {"Foo": 42} + assert Schema().dumps({"Foo": "42"}) == {"Foo": 42} + with pytest.raises(exc.ValidationException): + Schema().dumps({}) + with pytest.raises(exc.ValidationException): + Schema().dumps({"Foo": "bar"}) + + +def test_request_array(): + class Schema(schema.RequestSchema): + fields = {"IP": fields.List(fields.Str())} + + d = Schema().dumps({"IP": ["127.0.0.1"]}) + assert d == {"IP": ["127.0.0.1"]} + d = Schema().dumps({}) + assert d == {"IP": []} + + +def test_request_array_with_default(): + class Schema(schema.RequestSchema): + fields = {"IP": fields.List(fields.Str(), default=["127.0.0.1"])} + + d = Schema().dumps({"IP": ["192.168.0.1"]}) + assert d == {"IP": ["192.168.0.1"]} + d = Schema().dumps({}) + assert d == {"IP": ["127.0.0.1"]} + + +def test_request_object_model(): + class Schema(schema.RequestSchema): + fields = {"IP": fields.List(fields.Str())} + + class NestedObjectSchema(schema.RequestSchema): + fields = {"Interface": Schema()} + + d = NestedObjectSchema().dumps({"Interface": {"IP": ["127.0.0.1"]}}) + assert d == {"Interface": {"IP": ["127.0.0.1"]}} + with pytest.raises(exc.ValidationException): + NestedObjectSchema().dumps({"Interface": 1}) + + +def test_request_array_model_with_default(): + class Schema(schema.RequestSchema): + fields = {"IP": fields.List(fields.Str())} + + class NestedArraySchema(schema.RequestSchema): + fields = { + "Interface": fields.List( + Schema(default=lambda: "127.0.0.1"), + default=lambda: [{"IP": ["192.168.1.1"]}], + ) + } + + d = NestedArraySchema().dumps({}) + assert d == {"Interface": [{"IP": ["192.168.1.1"]}]} + d = { + "Interface": [ + {"IP": ["127.0.0.1", "192.168.0.1"]}, + {"IP": ["172.16.0.1"]}, + ] + } + d = NestedArraySchema().dumps(d) + assert d == { + "Interface": [ + {"IP": ["127.0.0.1", "192.168.0.1"]}, + {"IP": ["172.16.0.1"]}, + ] + } + + +def test_response_basic(): + class Schema(schema.ResponseSchema): + fields = { + "Foo": fields.Int(required=True), + "Bar": fields.Int(required=False), + "Default": fields.Int(default=42), + "Call": fields.List(fields.Int(), default=list), + } + + assert Schema().dumps({"Foo": "42"}) == { + "Foo": 42, + "Default": 42, + "Call": [], + } + assert Schema().loads({"Foo": "42"}) == { + "Foo": 42, + "Default": 42, + "Call": [], + } + with pytest.raises(exc.ValidationException): + Schema().dumps({}) + with pytest.raises(exc.ValidationException): + Schema().dumps({"Foo": "bar"}) + with pytest.raises(exc.ValidationException): + Schema().dumps({}) + with pytest.raises(exc.ValidationException): + Schema().dumps({"Foo": "bar"}) + + +def test_response_array(): + class Schema(schema.ResponseSchema): + fields = {"IP": fields.List(fields.Str())} + + d = Schema().loads({}) + assert d == {"IP": []} + d = Schema().loads({"IP": ["127.0.0.1"]}) + assert d == {"IP": ["127.0.0.1"]} + with pytest.raises(exc.ValidationException): + Schema().loads({"IP": 1}) + + +def test_response_array_with_default(): + class Schema(schema.ResponseSchema): + fields = {"IP": fields.List(fields.Str(), default=["127.0.0.1"])} + + d = Schema().dumps({"IP": ["192.168.0.1"]}) + assert d == {"IP": ["192.168.0.1"]} + d = Schema().dumps({}) + assert d == {"IP": ["127.0.0.1"]} + + +def test_response_object_model(): + class Schema(schema.ResponseSchema): + fields = {"IP": fields.List(fields.Str())} + + class NestedObjectSchema(schema.ResponseSchema): + fields = {"EIP": Schema()} + + d = NestedObjectSchema().loads({"EIP": {"IP": ["127.0.0.1"]}}) + assert d == {"EIP": {"IP": ["127.0.0.1"]}} + d = NestedObjectSchema().loads({}) + assert d == {"EIP": {"IP": []}} + + +def test_response_object_model_case_insensitive(): + class Schema(schema.ResponseSchema): + fields = {"IP": fields.List(fields.Str())} + + class NestedObjectSchema(schema.ResponseSchema): + fields = {"EIP": Schema()} + + d = NestedObjectSchema().loads({"eip": {"Ip": ["127.0.0.1"]}}) + assert d == {"EIP": {"IP": ["127.0.0.1"]}} + + +def test_response_array_model_with_default(): + class Schema(schema.ResponseSchema): + fields = {"IP": fields.List(fields.Str())} + + class NestedArraySchema(schema.ResponseSchema): + fields = { + "Interface": fields.List( + Schema(default=lambda: {"IP": ["127.0.0.1"]}), + default=lambda: [{"IP": ["192.168.1.1"]}], + ) + } + + d = NestedArraySchema().dumps({}) + assert d == {"Interface": [{"IP": ["192.168.1.1"]}]} + d = { + "Interface": [ + {"IP": ["127.0.0.1", "192.168.0.1"]}, + {"IP": ["172.16.0.1"]}, + ] + } + d = NestedArraySchema().dumps(d) + assert d == { + "Interface": [ + {"IP": ["127.0.0.1", "192.168.0.1"]}, + {"IP": ["172.16.0.1"]}, + ] + } diff --git a/tests/test_unit/test_core/test_transport.py b/tests/test_unit/test_core/test_transport.py new file mode 100644 index 0000000..ec519fb --- /dev/null +++ b/tests/test_unit/test_core/test_transport.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +import pytest +import logging +from ucloud.core.transport import RequestsTransport, Request, Response, utils + +logger = logging.getLogger(__name__) + + +@pytest.fixture(scope="function", autouse=True) +def transport(): + return RequestsTransport() + + +class TestTransport(object): + def test_transport_send(self, transport): + req = Request( + url="http://httpbin.org/anything", + method="post", + json={"foo": "bar"}, + ) + resp = transport.send(req) + assert resp.text + assert resp.json()["json"] == {"foo": "bar"} + + def test_transport_handler(self, transport): + global_env = {} + + def request_handler(r): + global_env["req"] = r + return r + + def response_handler(r): + global_env["resp"] = r + return r + + transport.middleware.request(handler=request_handler) + transport.middleware.response(handler=response_handler) + req = Request( + url="http://httpbin.org/anything", + method="post", + json={"foo": "bar"}, + ) + resp = transport.send(req) + assert resp.text + assert resp.json()["json"] == {"foo": "bar"} + assert "req" in global_env + assert "resp" in global_env + + +class TestResponse(object): + def test_guess_json_utf(self): + import json + + encodings = [ + "utf-32", + "utf-8-sig", + "utf-16", + "utf-8", + "utf-16-be", + "utf-16-le", + "utf-32-be", + "utf-32-le", + ] + for e in encodings: + s = json.dumps("表意字符").encode(e) + assert utils.guess_json_utf(s) == e + + def test_response_empty_content(self): + r = Response("http://foo.bar", "post") + assert not r.text + assert r.json() is None diff --git a/tests/test_unit/test_helpers/__init__.py b/tests/test_unit/test_helpers/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tests/test_unit/test_helpers/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/test_unit/test_helpers/test_utils.py b/tests/test_unit/test_helpers/test_utils.py new file mode 100644 index 0000000..496beb8 --- /dev/null +++ b/tests/test_unit/test_helpers/test_utils.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +from ucloud.helpers import utils + + +def test_b64encode(): + encoded = utils.b64encode("foobar42") + assert encoded == "Zm9vYmFyNDI=" + + +def test_b64decode(): + decoded = utils.b64decode("Zm9vYmFyNDI=") + assert decoded == "foobar42" diff --git a/tests/test_unit/test_helpers/test_wait.py b/tests/test_unit/test_helpers/test_wait.py new file mode 100644 index 0000000..1d16a86 --- /dev/null +++ b/tests/test_unit/test_helpers/test_wait.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- + +import pytest +from ucloud.helpers import wait + + +def test_wait(): + with pytest.raises(wait.WaitTimeoutException): + wait.wait_for_state( + pending=["pending"], + target=["running"], + refresh=lambda: "pending", + timeout=0.5, + ) + wait.wait_for_state( + pending=["pending"], + target=["running"], + refresh=lambda: "running", + timeout=0.5, + ) diff --git a/tests/test_unit/test_testing/__init__.py b/tests/test_unit/test_testing/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tests/test_unit/test_testing/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tests/test_unit/test_testing/test_utest.py b/tests/test_unit/test_testing/test_utest.py new file mode 100644 index 0000000..7b6ebb1 --- /dev/null +++ b/tests/test_unit/test_testing/test_utest.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +import pytest +from ucloud.testing import utest, env + + +@pytest.mark.skipif(env.is_ut(), reason=env.get_skip_reason()) +def test_acc_pre_check(self, client): + assert str(client) + assert client.config.to_dict() + assert client.credential.to_dict() + env.pre_check_env() + + +def test_value_at_path(): + d = { + "Action": "DescribeEIPResponse", + "EIPSet": [ + { + "Resource": { + "ResourceID": "uhost-war3png3", + "ResourceName": "eip-s1-bgp", + "ResourceType": "uhost", + "Zone": "cn-bj2-05", + } + } + ], + "RetCode": 0, + "TotalBandwidth": 6, + "TotalCount": 3, + } + assert ( + utest.value_at_path(d, "EIPSet.0.Resource.ResourceID") + == "uhost-war3png3" + ) diff --git a/ucloud/client.py b/ucloud/client.py index 8a4093f..d5fcd86 100644 --- a/ucloud/client.py +++ b/ucloud/client.py @@ -99,3 +99,31 @@ def usms(self): return USMSClient( self._config, self.transport, self.middleware, self.logger ) + + def ipsecvpn(self): + from ucloud.services.ipsecvpn.client import IPSecVPNClient + + return IPSecVPNClient( + self._config, self.transport, self.middleware, self.logger + ) + + def ufs(self): + from ucloud.services.ufs.client import UFSClient + + return UFSClient( + self._config, self.transport, self.middleware, self.logger + ) + + def uhub(self): + from ucloud.services.uhub.client import UHubClient + + return UHubClient( + self._config, self.transport, self.middleware, self.logger + ) + + def vpc(self): + from ucloud.services.vpc.client import VPCClient + + return VPCClient( + self._config, self.transport, self.middleware, self.logger + ) diff --git a/ucloud/core/client/_cfg.py b/ucloud/core/client/_cfg.py index 353664b..2f8219d 100644 --- a/ucloud/core/client/_cfg.py +++ b/ucloud/core/client/_cfg.py @@ -14,6 +14,10 @@ class ConfigSchema(schema.Schema): "max_retries": fields.Int(default=3), "log_level": fields.Int(default=logging.INFO), "validate_request": fields.Bool(default=True), + "ssl_verify": fields.Bool(default=True), + "ssl_cacert": fields.Str(), + "ssl_cert": fields.Str(), + "ssl_key": fields.Str(), } @@ -54,6 +58,10 @@ def __init__( timeout=30, max_retries=3, log_level=logging.INFO, + ssl_verify=True, + ssl_cacert=None, + ssl_cert=None, + ssl_key=None, **kwargs ): self.region = region @@ -63,6 +71,10 @@ def __init__( self.timeout = timeout self.max_retries = max_retries self.log_level = log_level + self.ssl_verify = ssl_verify + self.ssl_cacert = ssl_cacert + self.ssl_cert = ssl_cert + self.ssl_key = ssl_key @classmethod def from_dict(cls, d): @@ -78,4 +90,8 @@ def to_dict(self): "timeout": self.timeout, "max_retries": self.max_retries, "log_level": self.log_level, + "ssl_verify": self.ssl_verify, + "ssl_cacert": self.ssl_cacert, + "ssl_cert": self.ssl_cert, + "ssl_key": self.ssl_key, } diff --git a/ucloud/core/client/_client.py b/ucloud/core/client/_client.py index 7456551..bed8035 100644 --- a/ucloud/core/client/_client.py +++ b/ucloud/core/client/_client.py @@ -4,7 +4,12 @@ import sys from ucloud import version from ucloud.core.client._cfg import Config -from ucloud.core.transport import Transport, RequestsTransport, Request +from ucloud.core.transport import ( + Transport, + RequestsTransport, + Request, + SSLOption, +) from ucloud.core.typesystem import encoder from ucloud.core.utils import log from ucloud.core.utils.middleware import Middleware @@ -39,6 +44,8 @@ def invoke(self, action, args=None, **options): try: return self._send(action, args or {}, **options) except exc.UCloudException as e: + for handler in self.middleware.exception_handlers: + handler(e) if e.retryable and retries != max_retries: logging.info( "Retrying {action}: {args}".format( @@ -49,6 +56,8 @@ def invoke(self, action, args=None, **options): continue raise e except Exception as e: + for handler in self.middleware.exception_handlers: + handler(e) raise e @property @@ -77,7 +86,15 @@ def _send(self, action, args, **options): max_retries = options.get("max_retries") or self.config.max_retries timeout = options.get("timeout") or self.config.timeout resp = self.transport.send( - req, timeout=timeout, max_retries=max_retries + req, + ssl_option=SSLOption( + self.config.ssl_verify, + self.config.ssl_cacert, + self.config.ssl_cert, + self.config.ssl_key, + ), + timeout=timeout, + max_retries=max_retries, ).json() for handler in self.middleware.response_handlers: resp = handler(resp) diff --git a/ucloud/core/transport/__init__.py b/ucloud/core/transport/__init__.py index 0980f2a..3784e50 100644 --- a/ucloud/core/transport/__init__.py +++ b/ucloud/core/transport/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from ucloud.core.transport.http import Request, Response, Transport +from ucloud.core.transport.http import Request, Response, Transport, SSLOption from ucloud.core.transport._requests import RequestsTransport -__all__ = ["Request", "Response", "Transport", "RequestsTransport"] +__all__ = ["Request", "Response", "Transport", "RequestsTransport", "SSLOption"] diff --git a/ucloud/core/transport/_requests.py b/ucloud/core/transport/_requests.py index bed881e..483f258 100644 --- a/ucloud/core/transport/_requests.py +++ b/ucloud/core/transport/_requests.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- +import time import requests from urllib3.util.retry import Retry from requests.adapters import HTTPAdapter from ucloud.core.transport import http -from ucloud.core.transport.http import Request, Response +from ucloud.core.transport.http import Request, Response, SSLOption from ucloud.core.utils.middleware import Middleware @@ -40,7 +41,12 @@ def send(self, req, **options): """ for handler in self.middleware.request_handlers: req = handler(req) - resp = self._send(req, **options) + try: + resp = self._send(req, **options) + except Exception as e: + for handler in self.middleware.exception_handlers: + handler(e) + raise e for handler in self.middleware.response_handlers: resp = handler(resp) return resp @@ -58,6 +64,9 @@ def _send(self, req, **options): adapter = self._load_adapter(options.get("max_retries")) session.mount("http://", adapter=adapter) session.mount("https://", adapter=adapter) + ssl_option = options.get("ssl_option") + kwargs = self._build_ssl_option(ssl_option) if ssl_option else {} + req.request_time = time.time() session_resp = session.request( method=req.method.upper(), url=req.url, @@ -65,11 +74,24 @@ def _send(self, req, **options): data=req.data, params=req.params, headers=req.headers, + **kwargs ) resp = self.convert_response(session_resp) resp.request = req + resp.response_time = time.time() return resp + @staticmethod + def _build_ssl_option(ssl_option): + kwargs = {"verify": ssl_option.ssl_verify and ssl_option.ssl_cacert} + if not ssl_option.ssl_cert: + return kwargs + if ssl_option.ssl_key: + kwargs["cert"] = ssl_option.ssl_cert, ssl_option.ssl_key + else: + kwargs["cert"] = ssl_option.ssl_cert + return kwargs + def _load_adapter(self, max_retries=None): if max_retries is None and self._adapter is not None: return self._adapter diff --git a/ucloud/core/transport/http.py b/ucloud/core/transport/http.py index d6d3182..730c977 100644 --- a/ucloud/core/transport/http.py +++ b/ucloud/core/transport/http.py @@ -25,6 +25,7 @@ def __init__( self.data = data self.json = json self.headers = headers + self.request_time = 0 def payload(self): payload = (self.params or {}).copy() @@ -54,6 +55,7 @@ def __init__( self.headers = headers self.content = content self.encoding = encoding + self.response_time = 0 def json(self, **kwargs): """ json will return the bytes of content @@ -82,6 +84,16 @@ def text(self): return content +class SSLOption(object): + def __init__( + self, ssl_verify=True, ssl_cacert=None, ssl_cert=None, ssl_key=None + ): + self.ssl_verify = ssl_verify + self.ssl_cacert = ssl_cacert + self.ssl_cert = ssl_cert + self.ssl_key = ssl_key + + class Transport(object): """ the abstract class of transport implementation """ diff --git a/ucloud/core/utils/deco.py b/ucloud/core/utils/deco.py new file mode 100644 index 0000000..6503db6 --- /dev/null +++ b/ucloud/core/utils/deco.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +import functools +import logging + +logger = logging.getLogger("ucloud") + + +def deprecated(instead_of="", message=""): + """ deprecated is a decorator to mark a function is deprecated. + it will logging warning when this function called + + >>> @deprecated(instead_of="new_function") + ... def an_old_function(): + ... pass + """ + + def deco(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + msg = ["this function/method {} is deprecated"] + instead_of and msg.append( + "please use {} instead".format(fn.__name__, instead_of) + ) + message and msg.append(message) + logger.warning(",".join(msg)) + return fn(*args, **kwargs) + + return wrapper + + return deco diff --git a/ucloud/core/utils/middleware.py b/ucloud/core/utils/middleware.py index 9cfa8e1..5f4f1c6 100644 --- a/ucloud/core/utils/middleware.py +++ b/ucloud/core/utils/middleware.py @@ -27,6 +27,7 @@ class Middleware(object): def __init__(self): self.request_handlers = [] self.response_handlers = [] + self.exception_handlers = [] def request(self, handler, index=-1): """ request is the request handler register to add request handler. @@ -51,3 +52,15 @@ def response(self, handler, index=-1): """ self.response_handlers.insert(index, handler) return handler + + def exception(self, handler, index=-1): + """ exception is the exception handler register to add exception handler. + + :param handler: exception handler function, receive exception object + and raise a new exception or ignore it + :param int index: the position of handler in the handler list, + default is append it to end + :return: + """ + self.exception_handlers.insert(index, handler) + return handler diff --git a/ucloud/services/ipsecvpn/__init__.py b/ucloud/services/ipsecvpn/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/ucloud/services/ipsecvpn/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/ucloud/services/ipsecvpn/client.py b/ucloud/services/ipsecvpn/client.py new file mode 100644 index 0000000..50fa7d1 --- /dev/null +++ b/ucloud/services/ipsecvpn/client.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +""" Code is generated by ucloud-model, DO NOT EDIT IT. """ +from ucloud.core.client import Client +from ucloud.services.ipsecvpn.schemas import apis + + +class IPSecVPNClient(Client): + def __init__(self, config, transport=None, middleware=None, logger=None): + super(IPSecVPNClient, self).__init__( + config, transport, middleware, logger + ) + + def describe_remote_vpn_gateway(self, req=None, **kwargs): + """ DescribeRemoteVPNGateway - 获取客户VPN网关信息 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **Limit** (int) - 数据分页值, 默认为20 + - **Offset** (int) - 数据偏移量, 默认为0 + - **RemoteVPNGatewayIds** (list) - 客户VPN网关的资源ID,例如RemoteVPNGatewayIds.0代表希望获取客户VPN网关1的信息,RemoteVPNGatewayIds.1代表客户VPN网关2,如果为空,则返回当前Region中所有客户VPN网关实例的信息 + - **Tag** (str) - 业务组名称,若指定则返回业务组下所有客户VPN网关信息 + + **Response** + + - **DataSet** (list) - 见 **RemoteVPNGatewayDataSet** 模型定义 + - **TotalCount** (int) - 符合条件的客户VPN网关总数 + + **Response Model** + + **RemoteVPNGatewayDataSet** + + - **ActiveTunnels** (str) - 活跃的隧道id + - **CreateTime** (int) - 创建时间 + - **Remark** (str) - 备注 + - **RemoteVPNGatewayAddr** (str) - 客户网关IP地址 + - **RemoteVPNGatewayId** (str) - 客户网关ID + - **RemoteVPNGatewayName** (str) - 客户网关名称 + - **Tag** (str) - 用户组 + - **TunnelCount** (int) - 活跃的隧道数量 + + """ + d = {"ProjectId": self.config.project_id, "Region": self.config.region} + req and d.update(req) + d = apis.DescribeRemoteVPNGatewayRequestSchema().dumps(d) + resp = self.invoke("DescribeRemoteVPNGateway", d, **kwargs) + return apis.DescribeRemoteVPNGatewayResponseSchema().loads(resp) + + def describe_vpn_tunnel(self, req=None, **kwargs): + """ DescribeVPNTunnel - 获取VPN隧道信息 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **Limit** (int) - 数据分页值, 默认为20 + - **Offset** (int) - 数据偏移量, 默认为0 + - **Tag** (str) - 业务组名称,若指定则返回指定的业务组下的所有VPN网关的信息 + - **VPNTunnelIds** (list) - VPN隧道的资源ID,例如VPNTunnelIds.0代表希望获取信息的VPN隧道1,VPNTunneIds.1代表VPN隧道2,如果为空,则返回当前Region中所有的VPN隧道实例 + + **Response** + + - **DataSet** (list) - 见 **VPNTunnelDataSet** 模型定义 + - **TotalCount** (int) - VPN隧道总数 + + **Response Model** + + **IPSecData** + + - **IPSecAuthenticationAlgorithm** (str) - IPSec通道中使用的认证算法 + - **IPSecEncryptionAlgorithm** (str) - IPSec通道中使用的加密算法 + - **IPSecLocalSubnetIds** (list) - 指定VPN连接的本地子网,用逗号分隔 + - **IPSecPFSDhGroup** (str) - 是否开启PFS功能,Disable表示关闭,数字表示DH组 + - **IPSecProtocol** (str) - 使用的安全协议,ESP或AH + - **IPSecRemoteSubnets** (list) - 指定VPN连接的客户网段,用逗号分隔 + - **IPSecSALifetime** (str) - IPSec中SA的生存时间 + - **IPSecSALifetimeBytes** (str) - IPSec中SA的生存时间(以字节计) + + **IKEData** + + - **IKEAuthenticationAlgorithm** (str) - IKE认证算法 + - **IKEDhGroup** (str) - IKEDH组 + - **IKEEncryptionAlgorithm** (str) - IKE加密算法 + - **IKEExchangeMode** (str) - IKEv1协商模式 + - **IKELocalId** (str) - IKE本地ID标识 + - **IKEPreSharedKey** (str) - IKE预共享秘钥 + - **IKERemoteId** (str) - IKE对端ID标识 + - **IKESALifetime** (str) - IKE秘钥生存时间 + - **IKEVersion** (str) - IKE版本 + + **VPNTunnelDataSet** + + - **CreateTime** (int) - 创建时间 + - **IKEData** (dict) - 见 **IKEData** 模型定义 + - **IPSecData** (dict) - 见 **IPSecData** 模型定义 + - **Remark** (str) - 备注 + - **RemoteVPNGatewayId** (str) - 对端网关Id + - **RemoteVPNGatewayName** (str) - 对端网关名字 + - **Tag** (str) - 用户组 + - **VPCId** (str) - 所属VPCId + - **VPCName** (str) - 所属VOC名字 + - **VPNGatewayId** (str) - 所属VPN网关id + - **VPNGatewayName** (str) - VPN网关名字 + - **VPNTunnelId** (str) - 隧道id + - **VPNTunnelName** (str) - 隧道名称 + + """ + d = {"ProjectId": self.config.project_id, "Region": self.config.region} + req and d.update(req) + d = apis.DescribeVPNTunnelRequestSchema().dumps(d) + resp = self.invoke("DescribeVPNTunnel", d, **kwargs) + return apis.DescribeVPNTunnelResponseSchema().loads(resp) diff --git a/ucloud/services/ipsecvpn/schemas/__init__.py b/ucloud/services/ipsecvpn/schemas/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/ucloud/services/ipsecvpn/schemas/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/ucloud/services/ipsecvpn/schemas/apis.py b/ucloud/services/ipsecvpn/schemas/apis.py new file mode 100644 index 0000000..674c305 --- /dev/null +++ b/ucloud/services/ipsecvpn/schemas/apis.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- + +""" Code is generated by ucloud-model, DO NOT EDIT IT. """ +from ucloud.core.typesystem import schema, fields +from ucloud.services.ipsecvpn.schemas import models + +""" IPSecVPN API Schema +""" +""" +API: DescribeRemoteVPNGateway + +获取客户VPN网关信息 +""" + + +class DescribeRemoteVPNGatewayRequestSchema(schema.RequestSchema): + """ DescribeRemoteVPNGateway - 获取客户VPN网关信息 + """ + + fields = { + "Limit": fields.Int(required=False, dump_to="Limit"), + "Offset": fields.Int(required=False, dump_to="Offset"), + "ProjectId": fields.Str(required=True, dump_to="ProjectId"), + "Region": fields.Str(required=True, dump_to="Region"), + "RemoteVPNGatewayIds": fields.List(fields.Str()), + "Tag": fields.Str(required=False, dump_to="Tag"), + } + + +class DescribeRemoteVPNGatewayResponseSchema(schema.ResponseSchema): + """ DescribeRemoteVPNGateway - 获取客户VPN网关信息 + """ + + fields = { + "DataSet": fields.List( + models.RemoteVPNGatewayDataSetSchema(), + required=False, + load_from="DataSet", + ), + "TotalCount": fields.Int(required=False, load_from="TotalCount"), + } + + +""" +API: DescribeVPNTunnel + +获取VPN隧道信息 +""" + + +class DescribeVPNTunnelRequestSchema(schema.RequestSchema): + """ DescribeVPNTunnel - 获取VPN隧道信息 + """ + + fields = { + "Limit": fields.Int(required=False, dump_to="Limit"), + "Offset": fields.Int(required=False, dump_to="Offset"), + "ProjectId": fields.Str(required=True, dump_to="ProjectId"), + "Region": fields.Str(required=True, dump_to="Region"), + "Tag": fields.Str(required=False, dump_to="Tag"), + "VPNTunnelIds": fields.List(fields.Str()), + } + + +class DescribeVPNTunnelResponseSchema(schema.ResponseSchema): + """ DescribeVPNTunnel - 获取VPN隧道信息 + """ + + fields = { + "DataSet": fields.List( + models.VPNTunnelDataSetSchema(), required=False, load_from="DataSet" + ), + "TotalCount": fields.Int(required=False, load_from="TotalCount"), + } diff --git a/ucloud/services/ipsecvpn/schemas/models.py b/ucloud/services/ipsecvpn/schemas/models.py new file mode 100644 index 0000000..820b870 --- /dev/null +++ b/ucloud/services/ipsecvpn/schemas/models.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +""" Code is generated by ucloud-model, DO NOT EDIT IT. """ +from ucloud.core.typesystem import schema, fields + + +class RemoteVPNGatewayDataSetSchema(schema.ResponseSchema): + """ RemoteVPNGatewayDataSet - DescribeRemoteVPNGateway返回参数 + """ + + fields = { + "ActiveTunnels": fields.Str(required=False, load_from="ActiveTunnels"), + "CreateTime": fields.Int(required=False, load_from="CreateTime"), + "Remark": fields.Str(required=False, load_from="Remark"), + "RemoteVPNGatewayAddr": fields.Str( + required=False, load_from="RemoteVPNGatewayAddr" + ), + "RemoteVPNGatewayId": fields.Str( + required=False, load_from="RemoteVPNGatewayId" + ), + "RemoteVPNGatewayName": fields.Str( + required=False, load_from="RemoteVPNGatewayName" + ), + "Tag": fields.Str(required=False, load_from="Tag"), + "TunnelCount": fields.Int(required=False, load_from="TunnelCount"), + } + + +class IPSecDataSchema(schema.ResponseSchema): + """ IPSecData - IPSec参数 + """ + + fields = { + "IPSecAuthenticationAlgorithm": fields.Str( + required=False, load_from="IPSecAuthenticationAlgorithm" + ), + "IPSecEncryptionAlgorithm": fields.Str( + required=False, load_from="IPSecEncryptionAlgorithm" + ), + "IPSecLocalSubnetIds": fields.List(fields.Str()), + "IPSecPFSDhGroup": fields.Str( + required=False, load_from="IPSecPFSDhGroup" + ), + "IPSecProtocol": fields.Str(required=False, load_from="IPSecProtocol"), + "IPSecRemoteSubnets": fields.List(fields.Str()), + "IPSecSALifetime": fields.Str( + required=False, load_from="IPSecSALifetime" + ), + "IPSecSALifetimeBytes": fields.Str( + required=False, load_from="IPSecSALifetimeBytes" + ), + } + + +class IKEDataSchema(schema.ResponseSchema): + """ IKEData - IKE信息 + """ + + fields = { + "IKEAuthenticationAlgorithm": fields.Str( + required=False, load_from="IKEAuthenticationAlgorithm" + ), + "IKEDhGroup": fields.Str(required=False, load_from="IKEDhGroup"), + "IKEEncryptionAlgorithm": fields.Str( + required=False, load_from="IKEEncryptionAlgorithm" + ), + "IKEExchangeMode": fields.Str( + required=False, load_from="IKEExchangeMode" + ), + "IKELocalId": fields.Str(required=False, load_from="IKELocalId"), + "IKEPreSharedKey": fields.Str( + required=False, load_from="IKEPreSharedKey" + ), + "IKERemoteId": fields.Str(required=False, load_from="IKERemoteId"), + "IKESALifetime": fields.Str(required=False, load_from="IKESALifetime"), + "IKEVersion": fields.Str(required=False, load_from="IKEVersion"), + } + + +class VPNTunnelDataSetSchema(schema.ResponseSchema): + """ VPNTunnelDataSet - DescribeVPNTunnel信息 + """ + + fields = { + "CreateTime": fields.Int(required=False, load_from="CreateTime"), + "IKEData": IKEDataSchema(), + "IPSecData": IPSecDataSchema(), + "Remark": fields.Str(required=False, load_from="Remark"), + "RemoteVPNGatewayId": fields.Str( + required=False, load_from="RemoteVPNGatewayId" + ), + "RemoteVPNGatewayName": fields.Str( + required=False, load_from="RemoteVPNGatewayName" + ), + "Tag": fields.Str(required=False, load_from="Tag"), + "VPCId": fields.Str(required=False, load_from="VPCId"), + "VPCName": fields.Str(required=False, load_from="VPCName"), + "VPNGatewayId": fields.Str(required=False, load_from="VPNGatewayId"), + "VPNGatewayName": fields.Str( + required=False, load_from="VPNGatewayName" + ), + "VPNTunnelId": fields.Str(required=False, load_from="VPNTunnelId"), + "VPNTunnelName": fields.Str(required=False, load_from="VPNTunnelName"), + } diff --git a/ucloud/services/ufs/__init__.py b/ucloud/services/ufs/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/ucloud/services/ufs/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/ucloud/services/ufs/client.py b/ucloud/services/ufs/client.py new file mode 100644 index 0000000..03529de --- /dev/null +++ b/ucloud/services/ufs/client.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- + +""" Code is generated by ucloud-model, DO NOT EDIT IT. """ +from ucloud.core.client import Client +from ucloud.services.ufs.schemas import apis + + +class UFSClient(Client): + def __init__(self, config, transport=None, middleware=None, logger=None): + super(UFSClient, self).__init__(config, transport, middleware, logger) + + def create_ufs_volume(self, req=None, **kwargs): + """ CreateUFSVolume - 创建文件系统 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProtocolType** (str) - (Required) 文件系统协议,枚举值,NFSv3表示NFS V3协议,NFSv4表示NFS V4协议 + - **Size** (int) - (Required) 文件系统大小,单位为GB,最大不超过20T,香港容量型必须为100的整数倍,Size最小为500GB,北京,上海,广州的容量型必须为1024的整数倍,Size最小为1024GB。性能型文件系统Size最小为100GB + - **StorageType** (str) - (Required) 文件系统存储类型,枚举值,Basic表示容量型,Advanced表示性能型 + - **ChargeType** (str) - 计费模式,枚举值为: Year,按年付费; Month,按月付费; Dynamic,按需付费(需开启权限); Trial,试用(需开启权限) 默认为Dynamic + - **CouponId** (str) - 使用的代金券id + - **Quantity** (int) - 购买时长 默认: 1 + - **Remark** (str) - 备注 + - **Tag** (str) - 文件系统所属业务组 + - **VolumeName** (str) - 文件系统名称 + + **Response** + + - **VolumeId** (str) - 文件系统ID + - **VolumeName** (str) - 文件系统名称 + - **VolumeStatus** (str) - 文件系统挂载点状态 + + """ + d = {"ProjectId": self.config.project_id, "Region": self.config.region} + req and d.update(req) + d = apis.CreateUFSVolumeRequestSchema().dumps(d) + kwargs["max_retries"] = 0 + resp = self.invoke("CreateUFSVolume", d, **kwargs) + return apis.CreateUFSVolumeResponseSchema().loads(resp) + + def describe_ufs_volume_2(self, req=None, **kwargs): + """ DescribeUFSVolume2 - 获取文件系统列表 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **Limit** (int) - 文件列表长度 + - **Offset** (int) - 文件列表起始 + - **VolumeId** (str) - 文件系统ID + + **Response** + + - **DataSet** (list) - 见 **UFSVolumeInfo2** 模型定义 + - **TotalCount** (int) - 文件系统总数 + + **Response Model** + + **UFSVolumeInfo2** + + - **CreateTime** (int) - 文件系统创建时间(unix时间戳) + - **ExpiredTime** (int) - 文件系统过期时间(unix时间戳) + - **IsExpired** (str) - 是否过期 + - **MaxMountPointNum** (int) - 文件系统允许创建的最大挂载点数目 + - **ProtocolType** (str) - 文件系统协议,枚举值,NFSv3表示NFS V3协议,NFSv4表示NFS V4协议 + - **Remark** (str) - 文件系统备注信息 + - **Size** (int) - 文件系统大小,单位GB + - **StorageType** (str) - 文件系统存储类型,枚举值,Basic表示容量型,Advanced表示性能型 + - **Tag** (str) - 文件系统所属业务组 + - **TotalMountPointNum** (int) - 当前文件系统已创建的挂载点数目 + - **UsedSize** (int) - 文件系统当前使用容量,单位GB + - **VolumeId** (str) - 文件系统ID + - **VolumeName** (str) - 文件系统名称 + + """ + d = {"ProjectId": self.config.project_id, "Region": self.config.region} + req and d.update(req) + d = apis.DescribeUFSVolume2RequestSchema().dumps(d) + resp = self.invoke("DescribeUFSVolume2", d, **kwargs) + return apis.DescribeUFSVolume2ResponseSchema().loads(resp) + + def extend_ufs_volume(self, req=None, **kwargs): + """ ExtendUFSVolume - 文件系统扩容 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **Size** (int) - (Required) 文件系统大小,单位为GB,最大不超过20T,香港容量型必须为100的整数倍,Size最小为500GB,北京,上海,广州的容量型必须为1024的整数倍,Size最小为1024GB。性能型文件系统Size最小为100GB + - **VolumeId** (str) - (Required) 文件系统ID + + **Response** + + + """ + d = {"ProjectId": self.config.project_id, "Region": self.config.region} + req and d.update(req) + d = apis.ExtendUFSVolumeRequestSchema().dumps(d) + resp = self.invoke("ExtendUFSVolume", d, **kwargs) + return apis.ExtendUFSVolumeResponseSchema().loads(resp) + + def remove_ufs_volume(self, req=None, **kwargs): + """ RemoveUFSVolume - 删除UFS文件系统 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **VolumeId** (str) - (Required) 文件系统ID + + **Response** + + + """ + d = {"ProjectId": self.config.project_id, "Region": self.config.region} + req and d.update(req) + d = apis.RemoveUFSVolumeRequestSchema().dumps(d) + resp = self.invoke("RemoveUFSVolume", d, **kwargs) + return apis.RemoveUFSVolumeResponseSchema().loads(resp) diff --git a/ucloud/services/ufs/schemas/__init__.py b/ucloud/services/ufs/schemas/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/ucloud/services/ufs/schemas/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/ucloud/services/ufs/schemas/apis.py b/ucloud/services/ufs/schemas/apis.py new file mode 100644 index 0000000..99be471 --- /dev/null +++ b/ucloud/services/ufs/schemas/apis.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- + +""" Code is generated by ucloud-model, DO NOT EDIT IT. """ +from ucloud.core.typesystem import schema, fields +from ucloud.services.ufs.schemas import models + +""" UFS API Schema +""" +""" +API: CreateUFSVolume + +创建文件系统 +""" + + +class CreateUFSVolumeRequestSchema(schema.RequestSchema): + """ CreateUFSVolume - 创建文件系统 + """ + + fields = { + "ChargeType": fields.Str(required=False, dump_to="ChargeType"), + "CouponId": fields.Str(required=False, dump_to="CouponId"), + "ProjectId": fields.Str(required=False, dump_to="ProjectId"), + "ProtocolType": fields.Str(required=True, dump_to="ProtocolType"), + "Quantity": fields.Int(required=False, dump_to="Quantity"), + "Region": fields.Str(required=True, dump_to="Region"), + "Remark": fields.Str(required=False, dump_to="Remark"), + "Size": fields.Int(required=True, dump_to="Size"), + "StorageType": fields.Str(required=True, dump_to="StorageType"), + "Tag": fields.Str(required=False, dump_to="Tag"), + "VolumeName": fields.Str(required=False, dump_to="VolumeName"), + } + + +class CreateUFSVolumeResponseSchema(schema.ResponseSchema): + """ CreateUFSVolume - 创建文件系统 + """ + + fields = { + "VolumeId": fields.Str(required=True, load_from="VolumeId"), + "VolumeName": fields.Str(required=True, load_from="VolumeName"), + "VolumeStatus": fields.Str(required=True, load_from="VolumeStatus"), + } + + +""" +API: DescribeUFSVolume2 + +获取文件系统列表 +""" + + +class DescribeUFSVolume2RequestSchema(schema.RequestSchema): + """ DescribeUFSVolume2 - 获取文件系统列表 + """ + + fields = { + "Limit": fields.Int(required=False, dump_to="Limit"), + "Offset": fields.Int(required=False, dump_to="Offset"), + "ProjectId": fields.Str(required=False, dump_to="ProjectId"), + "Region": fields.Str(required=True, dump_to="Region"), + "VolumeId": fields.Str(required=False, dump_to="VolumeId"), + } + + +class DescribeUFSVolume2ResponseSchema(schema.ResponseSchema): + """ DescribeUFSVolume2 - 获取文件系统列表 + """ + + fields = { + "DataSet": fields.List( + models.UFSVolumeInfo2Schema(), required=True, load_from="DataSet" + ), + "TotalCount": fields.Int(required=True, load_from="TotalCount"), + } + + +""" +API: ExtendUFSVolume + +文件系统扩容 +""" + + +class ExtendUFSVolumeRequestSchema(schema.RequestSchema): + """ ExtendUFSVolume - 文件系统扩容 + """ + + fields = { + "ProjectId": fields.Str(required=False, dump_to="ProjectId"), + "Region": fields.Str(required=True, dump_to="Region"), + "Size": fields.Int(required=True, dump_to="Size"), + "VolumeId": fields.Str(required=True, dump_to="VolumeId"), + } + + +class ExtendUFSVolumeResponseSchema(schema.ResponseSchema): + """ ExtendUFSVolume - 文件系统扩容 + """ + + fields = {} + + +""" +API: RemoveUFSVolume + +删除UFS文件系统 +""" + + +class RemoveUFSVolumeRequestSchema(schema.RequestSchema): + """ RemoveUFSVolume - 删除UFS文件系统 + """ + + fields = { + "ProjectId": fields.Str(required=False, dump_to="ProjectId"), + "Region": fields.Str(required=True, dump_to="Region"), + "VolumeId": fields.Str(required=True, dump_to="VolumeId"), + } + + +class RemoveUFSVolumeResponseSchema(schema.ResponseSchema): + """ RemoveUFSVolume - 删除UFS文件系统 + """ + + fields = {} diff --git a/ucloud/services/ufs/schemas/models.py b/ucloud/services/ufs/schemas/models.py new file mode 100644 index 0000000..39ea2ab --- /dev/null +++ b/ucloud/services/ufs/schemas/models.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +""" Code is generated by ucloud-model, DO NOT EDIT IT. """ +from ucloud.core.typesystem import schema, fields + + +class UFSVolumeInfo2Schema(schema.ResponseSchema): + """ UFSVolumeInfo2 - 文件系统信息 + """ + + fields = { + "CreateTime": fields.Int(required=False, load_from="CreateTime"), + "ExpiredTime": fields.Int(required=False, load_from="ExpiredTime"), + "IsExpired": fields.Str(required=False, load_from="IsExpired"), + "MaxMountPointNum": fields.Int( + required=True, load_from="MaxMountPointNum" + ), + "ProtocolType": fields.Str(required=True, load_from="ProtocolType"), + "Remark": fields.Str(required=False, load_from="Remark"), + "Size": fields.Int(required=False, load_from="Size"), + "StorageType": fields.Str(required=True, load_from="StorageType"), + "Tag": fields.Str(required=False, load_from="Tag"), + "TotalMountPointNum": fields.Int( + required=True, load_from="TotalMountPointNum" + ), + "UsedSize": fields.Int(required=False, load_from="UsedSize"), + "VolumeId": fields.Str(required=True, load_from="VolumeId"), + "VolumeName": fields.Str(required=True, load_from="VolumeName"), + } diff --git a/ucloud/services/uhub/__init__.py b/ucloud/services/uhub/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/ucloud/services/uhub/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/ucloud/services/uhub/client.py b/ucloud/services/uhub/client.py new file mode 100644 index 0000000..78aa3bd --- /dev/null +++ b/ucloud/services/uhub/client.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- + +""" Code is generated by ucloud-model, DO NOT EDIT IT. """ +from ucloud.core.client import Client +from ucloud.services.uhub.schemas import apis + + +class UHubClient(Client): + def __init__(self, config, transport=None, middleware=None, logger=None): + super(UHubClient, self).__init__(config, transport, middleware, logger) + + def create_repo(self, req=None, **kwargs): + """ CreateRepo - 创建镜像仓库 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **RepoName** (str) - (Required) 仓库名称,不可修改 + - **Description** (str) - 仓库备注 + - **IsShared** (bool) - 镜像仓库是否公开,公开为true、不公开为false;默认为false + + **Response** + + - **Message** (str) - 有错误时返回内容 + + """ + d = {"ProjectId": self.config.project_id} + req and d.update(req) + d = apis.CreateRepoRequestSchema().dumps(d) + kwargs["max_retries"] = 0 + resp = self.invoke("CreateRepo", d, **kwargs) + return apis.CreateRepoResponseSchema().loads(resp) + + def delete_repo(self, req=None, **kwargs): + """ DeleteRepo - 删除镜像仓库 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **RepoName** (str) - (Required) 镜像仓库名称 + + **Response** + + + """ + d = {"ProjectId": self.config.project_id} + req and d.update(req) + d = apis.DeleteRepoRequestSchema().dumps(d) + resp = self.invoke("DeleteRepo", d, **kwargs) + return apis.DeleteRepoResponseSchema().loads(resp) + + def delete_repo_image(self, req=None, **kwargs): + """ DeleteRepoImage - 删除镜像 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **ImageName** (str) - (Required) 镜像名称 + - **RepoName** (str) - (Required) 镜像仓库名称 + - **TagName** (str) - 不指定tag则删除全部tag + + **Response** + + + """ + d = {"ProjectId": self.config.project_id} + req and d.update(req) + d = apis.DeleteRepoImageRequestSchema().dumps(d) + resp = self.invoke("DeleteRepoImage", d, **kwargs) + return apis.DeleteRepoImageResponseSchema().loads(resp) + + def get_image_tag(self, req=None, **kwargs): + """ GetImageTag - 获取镜像tag + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **ImageName** (str) - (Required) 镜像名称 + - **RepoName** (str) - (Required) 镜像仓库名称 + - **Limit** (int) - 每次获取数量,默认为20 + - **Offset** (int) - 偏移量,默认0 + - **TagName** (str) - 默认不写,如果填写,代表查询该tag,否则查全部tag + + **Response** + + - **TagSet** (list) - 见 **TagSet** 模型定义 + - **TotalCount** (int) - tag总数 + + **Response Model** + + **TagSet** + + - **TagName** (str) - Tag名称 + - **UpdateTime** (str) - 镜像更新时间 + + """ + d = {"ProjectId": self.config.project_id} + req and d.update(req) + d = apis.GetImageTagRequestSchema().dumps(d) + resp = self.invoke("GetImageTag", d, **kwargs) + return apis.GetImageTagResponseSchema().loads(resp) + + def get_repo(self, req=None, **kwargs): + """ GetRepo - 获取镜像仓库 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Limit** (int) - 数量,默认20 + - **Offset** (int) - 偏移量,默认0 + - **Type** (str) - private私有仓库,public公共仓库,默认public + + **Response** + + - **RepoSet** (list) - 见 **RepoSet** 模型定义 + - **TotalCount** (int) - 总的仓库数量 + + **Response Model** + + **RepoSet** + + - **CreateTime** (str) - 仓库创建时间 + - **Description** (str) - 镜像仓库描述 + - **IsOutSide** (str) - 镜像仓库是否外网可以访问,可以为ture,不可以为false + - **IsShared** (str) - 镜像仓库类型,false为私有;true为公有 + - **RepoName** (str) - 镜像仓库名称 + - **UpdateTime** (str) - 仓库更新时间 + + """ + d = {"ProjectId": self.config.project_id} + req and d.update(req) + d = apis.GetRepoRequestSchema().dumps(d) + resp = self.invoke("GetRepo", d, **kwargs) + return apis.GetRepoResponseSchema().loads(resp) + + def get_repo_image(self, req=None, **kwargs): + """ GetRepoImage - 获取镜像仓库下的镜像 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **RepoName** (str) - (Required) 镜像仓库名称 + - **Limit** (int) - 显示数量,默认为20 + - **Offset** (int) - 偏移量,默认0 + + **Response** + + - **ImageSet** (list) - 见 **ImageSet** 模型定义 + - **TotalCount** (int) - + + **Response Model** + + **ImageSet** + + - **CreateTime** (str) - 创建时间 + - **ImageName** (str) - 镜像名称 + - **LatestTag** (str) - 最新push的Tag + - **PullCount** (int) - 镜像被下载次数 + - **RepoName** (str) - 镜像仓库名称 + - **UpdateTime** (str) - 修改时间 + + """ + d = {"ProjectId": self.config.project_id} + req and d.update(req) + d = apis.GetRepoImageRequestSchema().dumps(d) + resp = self.invoke("GetRepoImage", d, **kwargs) + return apis.GetRepoImageResponseSchema().loads(resp) + + def update_repo(self, req=None, **kwargs): + """ UpdateRepo - 更新镜像仓库 + + **Request** + + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **RepoName** (str) - (Required) 镜像仓库名称,不可修改 + - **Description** (str) - 备注 + - **IsShared** (str) - false设置为私有;true设置为公有。默认false + + **Response** + + - **Message** (str) - 错误的时候返回 + + """ + d = {"ProjectId": self.config.project_id} + req and d.update(req) + d = apis.UpdateRepoRequestSchema().dumps(d) + resp = self.invoke("UpdateRepo", d, **kwargs) + return apis.UpdateRepoResponseSchema().loads(resp) diff --git a/ucloud/services/uhub/schemas/__init__.py b/ucloud/services/uhub/schemas/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/ucloud/services/uhub/schemas/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/ucloud/services/uhub/schemas/apis.py b/ucloud/services/uhub/schemas/apis.py new file mode 100644 index 0000000..c77f1aa --- /dev/null +++ b/ucloud/services/uhub/schemas/apis.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- + +""" Code is generated by ucloud-model, DO NOT EDIT IT. """ +from ucloud.core.typesystem import schema, fields +from ucloud.services.uhub.schemas import models + +""" UHub API Schema +""" +""" +API: CreateRepo + +创建镜像仓库 +""" + + +class CreateRepoRequestSchema(schema.RequestSchema): + """ CreateRepo - 创建镜像仓库 + """ + + fields = { + "Description": fields.Str(required=False, dump_to="Description"), + "IsShared": fields.Bool(required=False, dump_to="IsShared"), + "ProjectId": fields.Str(required=False, dump_to="ProjectId"), + "RepoName": fields.Str(required=True, dump_to="RepoName"), + } + + +class CreateRepoResponseSchema(schema.ResponseSchema): + """ CreateRepo - 创建镜像仓库 + """ + + fields = {"Message": fields.Str(required=False, load_from="Message")} + + +""" +API: DeleteRepo + +删除镜像仓库 +""" + + +class DeleteRepoRequestSchema(schema.RequestSchema): + """ DeleteRepo - 删除镜像仓库 + """ + + fields = { + "ProjectId": fields.Str(required=False, dump_to="ProjectId"), + "RepoName": fields.Str(required=True, dump_to="RepoName"), + } + + +class DeleteRepoResponseSchema(schema.ResponseSchema): + """ DeleteRepo - 删除镜像仓库 + """ + + fields = {} + + +""" +API: DeleteRepoImage + +删除镜像 +""" + + +class DeleteRepoImageRequestSchema(schema.RequestSchema): + """ DeleteRepoImage - 删除镜像 + """ + + fields = { + "ImageName": fields.Str(required=True, dump_to="ImageName"), + "ProjectId": fields.Str(required=False, dump_to="ProjectId"), + "RepoName": fields.Str(required=True, dump_to="RepoName"), + "TagName": fields.Str(required=False, dump_to="TagName"), + } + + +class DeleteRepoImageResponseSchema(schema.ResponseSchema): + """ DeleteRepoImage - 删除镜像 + """ + + fields = {} + + +""" +API: GetImageTag + +获取镜像tag +""" + + +class GetImageTagRequestSchema(schema.RequestSchema): + """ GetImageTag - 获取镜像tag + """ + + fields = { + "ImageName": fields.Str(required=True, dump_to="ImageName"), + "Limit": fields.Int(required=False, dump_to="Limit"), + "Offset": fields.Int(required=False, dump_to="Offset"), + "ProjectId": fields.Str(required=False, dump_to="ProjectId"), + "RepoName": fields.Str(required=True, dump_to="RepoName"), + "TagName": fields.Str(required=False, dump_to="TagName"), + } + + +class GetImageTagResponseSchema(schema.ResponseSchema): + """ GetImageTag - 获取镜像tag + """ + + fields = { + "TagSet": fields.List( + models.TagSetSchema(), required=True, load_from="TagSet" + ), + "TotalCount": fields.Int(required=True, load_from="TotalCount"), + } + + +""" +API: GetRepo + +获取镜像仓库 +""" + + +class GetRepoRequestSchema(schema.RequestSchema): + """ GetRepo - 获取镜像仓库 + """ + + fields = { + "Limit": fields.Int(required=False, dump_to="Limit"), + "Offset": fields.Int(required=False, dump_to="Offset"), + "ProjectId": fields.Str(required=False, dump_to="ProjectId"), + "Type": fields.Str(required=False, dump_to="Type"), + } + + +class GetRepoResponseSchema(schema.ResponseSchema): + """ GetRepo - 获取镜像仓库 + """ + + fields = { + "RepoSet": fields.List( + models.RepoSetSchema(), required=True, load_from="RepoSet" + ), + "TotalCount": fields.Int(required=True, load_from="TotalCount"), + } + + +""" +API: GetRepoImage + +获取镜像仓库下的镜像 +""" + + +class GetRepoImageRequestSchema(schema.RequestSchema): + """ GetRepoImage - 获取镜像仓库下的镜像 + """ + + fields = { + "Limit": fields.Int(required=False, dump_to="Limit"), + "Offset": fields.Int(required=False, dump_to="Offset"), + "ProjectId": fields.Str(required=False, dump_to="ProjectId"), + "RepoName": fields.Str(required=True, dump_to="RepoName"), + } + + +class GetRepoImageResponseSchema(schema.ResponseSchema): + """ GetRepoImage - 获取镜像仓库下的镜像 + """ + + fields = { + "ImageSet": fields.List( + models.ImageSetSchema(), required=True, load_from="ImageSet" + ), + "TotalCount": fields.Int(required=True, load_from="TotalCount"), + } + + +""" +API: UpdateRepo + +更新镜像仓库 +""" + + +class UpdateRepoRequestSchema(schema.RequestSchema): + """ UpdateRepo - 更新镜像仓库 + """ + + fields = { + "Description": fields.Str(required=False, dump_to="Description"), + "IsShared": fields.Str(required=False, dump_to="IsShared"), + "ProjectId": fields.Str(required=False, dump_to="ProjectId"), + "RepoName": fields.Str(required=True, dump_to="RepoName"), + } + + +class UpdateRepoResponseSchema(schema.ResponseSchema): + """ UpdateRepo - 更新镜像仓库 + """ + + fields = {"Message": fields.Str(required=False, load_from="Message")} diff --git a/ucloud/services/uhub/schemas/models.py b/ucloud/services/uhub/schemas/models.py new file mode 100644 index 0000000..db49eea --- /dev/null +++ b/ucloud/services/uhub/schemas/models.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +""" Code is generated by ucloud-model, DO NOT EDIT IT. """ +from ucloud.core.typesystem import schema, fields + + +class TagSetSchema(schema.ResponseSchema): + """ TagSet - Tag详细信息 + """ + + fields = { + "TagName": fields.Str(required=True, load_from="TagName"), + "UpdateTime": fields.Str(required=True, load_from="UpdateTime"), + } + + +class RepoSetSchema(schema.ResponseSchema): + """ RepoSet - 镜像仓库 + """ + + fields = { + "CreateTime": fields.Str(required=True, load_from="CreateTime"), + "Description": fields.Str(required=True, load_from="Description"), + "IsOutSide": fields.Str(required=True, load_from="IsOutSide"), + "IsShared": fields.Str(required=True, load_from="IsShared"), + "RepoName": fields.Str(required=True, load_from="RepoName"), + "UpdateTime": fields.Str(required=True, load_from="UpdateTime"), + } + + +class ImageSetSchema(schema.ResponseSchema): + """ ImageSet - 镜像信息 + """ + + fields = { + "CreateTime": fields.Str(required=True, load_from="CreateTime"), + "ImageName": fields.Str(required=True, load_from="ImageName"), + "LatestTag": fields.Str(required=True, load_from="LatestTag"), + "PullCount": fields.Int(required=True, load_from="PullCount"), + "RepoName": fields.Str(required=True, load_from="RepoName"), + "UpdateTime": fields.Str(required=True, load_from="UpdateTime"), + } diff --git a/ucloud/services/vpc/client.py b/ucloud/services/vpc/client.py index bd91bdb..df820e5 100644 --- a/ucloud/services/vpc/client.py +++ b/ucloud/services/vpc/client.py @@ -14,14 +14,14 @@ def add_vpc_network(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **Network** (list) - (Required) 增加网段 - **VPCId** (str) - (Required) 源VPC短ID - + **Response** - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -35,14 +35,14 @@ def associate_route_table(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **RouteTableId** (str) - (Required) 路由表ID,仅限自定义路由表 - **SubnetId** (str) - (Required) 子网ID - + **Response** - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -55,13 +55,13 @@ def clone_route_table(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **RouteTableId** (str) - (Required) 被克隆的路由表ID - + **Response** - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -75,17 +75,17 @@ def create_route_table(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **VPCId** (str) - (Required) VPC ID - **Name** (str) - 路由表名称 Default RouteTable - **Remark** (str) - 备注 - **Tag** (str) - 业务组 - + **Response** - **RouteTableId** (str) - 路由表ID - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -99,19 +99,19 @@ def create_subnet(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **Subnet** (str) - (Required) 子网网络地址,例如192.168.0.0 - **VPCId** (str) - (Required) VPC资源ID - **Netmask** (int) - 子网网络号位数,默认为24 - **Remark** (str) - 备注 - **SubnetName** (str) - 子网名称,默认为Subnet - **Tag** (str) - 业务组名称,默认为Default - + **Response** - **SubnetId** (str) - 子网ID - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -125,18 +125,18 @@ def create_vpc(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **Name** (str) - (Required) VPC名称 - **Network** (list) - (Required) VPC网段 - **Remark** (str) - 备注 - **Tag** (str) - 业务组名称 - **Type** (int) - VPC类型 - + **Response** - **VPCId** (str) - VPC资源Id - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -150,16 +150,16 @@ def create_vpc_intercom(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 源VPC所在项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 源VPC所在地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 源VPC所在项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 源VPC所在地域。 参见 `地域和可用区列表 `_ - **DstVPCId** (str) - (Required) 目的VPC短ID - **VPCId** (str) - (Required) 源VPC短ID - **DstProjectId** (str) - 目的VPC项目ID。默认与源VPC同项目。 - **DstRegion** (str) - 目的VPC所在地域,默认与源VPC同地域。 - + **Response** - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -173,13 +173,13 @@ def delete_route_table(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **RouteTableId** (str) - (Required) 路由ID - + **Response** - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -192,13 +192,13 @@ def delete_subnet(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **SubnetId** (str) - (Required) 子网ID - + **Response** - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -211,13 +211,13 @@ def delete_vpc(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **VPCId** (str) - (Required) VPC资源Id - + **Response** - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -230,16 +230,16 @@ def delete_vpc_intercom(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 源VPC所在项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 源VPC所在地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 源VPC所在项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 源VPC所在地域。 参见 `地域和可用区列表 `_ - **DstVPCId** (str) - (Required) 目的VPC短ID - **VPCId** (str) - (Required) 源VPC短ID - **DstProjectId** (str) - 目的VPC所在项目ID,默认为源VPC所在项目ID - **DstRegion** (str) - 目的VPC所在地域,默认为源VPC所在地域 - + **Response** - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -252,23 +252,23 @@ def describe_route_table(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **BusinessId** (str) - 业务组ID - **Limit** (int) - Limit - **OffSet** (int) - OffSet - **RouteTableId** (str) - 路由表ID - **VPCId** (str) - VPC ID - + **Response** - **RouteTables** (list) - 见 **RouteTableInfo** 模型定义 - **TotalCount** (int) - RouteTables字段的数量 - + **Response Model** - - **RouteRuleInfo** - + + **RouteRuleInfo** + - **DstAddr** (str) - 目的地址,比如10.10.8/24 - **NexthopId** (str) - 路由下一跳ID,比如uvnet-3eljvj - **NexthopType** (str) - 下一跳类型,比如local、vnet @@ -276,8 +276,8 @@ def describe_route_table(self, req=None, **kwargs): - **RouteRuleId** (str) - 规则ID - **RuleType** (int) - 路由规则类型(0表示系统路由,1表示自定义路由) - **RouteTableInfo** - + **RouteTableInfo** + - **CreateTime** (int) - 创建时间戳 - **Remark** (str) - 路由表备注 - **RouteRules** (list) - 见 **RouteRuleInfo** 模型定义 @@ -300,32 +300,33 @@ def describe_subnet(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **BusinessId** (str) - 业务组 - **Limit** (int) - 列表长度,默认为20 - **Offset** (int) - 偏移量,默认为0 - **RouteTableId** (str) - 路由表Id + - **ShowAvailableIPs** (bool) - 是否返回子网的可用IP数,true为是,false为否,默认不返回 - **SubnetId** (str) - 子网id,适用于一次查询一个子网信息 - **SubnetIds** (list) - 子网id数组,适用于一次查询多个子网信息 - **Tag** (str) - 业务组名称,默认为Default - **VPCId** (str) - VPC资源id - + **Response** - **DataSet** (list) - 见 **SubnetInfo** 模型定义 - **TotalCount** (int) - 子网总数量 - + **Response Model** - - **SubnetInfo** - + + **SubnetInfo** + + - **AvailableIPs** (int) - 可用IP数量 - **CreateTime** (int) - 创建时间 - **Gateway** (str) - 子网网关 - **HasNATGW** (bool) - 是否有natgw - **IPv6Network** (str) - 子网关联的IPv6网段 - - **Netmask** (int) - 子网掩码 - - **OperatorName** (str) - 子网关联的IPv6网段所属运营商 + - **Netmask** (str) - 子网掩码 - **Remark** (str) - 备注 - **RouteTableId** (str) - 路由表Id - **Subnet** (str) - 子网网段 @@ -344,77 +345,40 @@ def describe_subnet(self, req=None, **kwargs): resp = self.invoke("DescribeSubnet", d, **kwargs) return apis.DescribeSubnetResponseSchema().loads(resp) - def describe_subnet_resource(self, req=None, **kwargs): - """ DescribeSubnetResource - 展示子网资源 - - **Request** - - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - - **SubnetId** (str) - (Required) 子网id - - **Limit** (int) - 单页返回数据长度,默认为20 - - **Offset** (int) - 列表起始位置偏移量,默认为0 - - **ResourceType** (str) - 资源类型,默认为全部资源类型。枚举值为:UHOST,云主机;PHOST,物理云主机;ULB,负载均衡;UHADOOP_HOST,hadoop节点;UFORTRESS_HOST,堡垒机;UNATGW,NAT网关;UKAFKA,Kafka消息队列;UMEM,内存存储;DOCKER,容器集群;UDB,数据库;UDW,数据仓库;VIP,内网VIP. - - **Response** - - - **DataSet** (list) - 见 **SubnetResource** 模型定义 - - **TotalCount** (int) - 总数 - - **Response Model** - - **SubnetResource** - - - **IP** (str) - 资源ip - - **IPv6Address** (str) - 资源的IPv6地址 - - **Name** (str) - 资源名称 - - **ResourceId** (str) - 资源Id - - **ResourceType** (str) - 资源类型。对应的资源类型:UHOST,云主机;PHOST,物理云主机;ULB,负载均衡;UHADOOP_HOST,hadoop节点;UFORTRESS_HOST,堡垒机;UNATGW,NAT网关;UKAFKA,Kafka消息队列;UMEM,内存存储;DOCKER,容器集群;UDB,数据库;UDW,数据仓库;VIP,内网VIP. - - **SubResourceId** (str) - 资源绑定的虚拟网卡的实例ID - - **SubResourceName** (str) - 资源绑定的虚拟网卡的实例名称 - - **SubResourceType** (str) - 资源绑定的虚拟网卡的类型 - - """ - d = {"ProjectId": self.config.project_id, "Region": self.config.region} - req and d.update(req) - d = apis.DescribeSubnetResourceRequestSchema().dumps(d) - resp = self.invoke("DescribeSubnetResource", d, **kwargs) - return apis.DescribeSubnetResourceResponseSchema().loads(resp) - def describe_vpc(self, req=None, **kwargs): """ DescribeVPC - 获取VPC信息 **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - - **Limit** (int) - - - **Offset** (int) - + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **Limit** (int) - + - **Offset** (int) - - **Tag** (str) - 业务组名称 - **VPCIds** (list) - VPCId - + **Response** - **DataSet** (list) - 见 **VPCInfo** 模型定义 - + **Response Model** - - **VPCNetworkInfo** - + + **VPCNetworkInfo** + - **Network** (str) - vpc地址空间 - **SubnetCount** (int) - 地址空间中子网数量 - **VPCInfo** - - - **CreateTime** (int) - + **VPCInfo** + + - **CreateTime** (int) - - **IPv6Network** (str) - VPC关联的IPv6网段 - - **Name** (str) - - - **Network** (list) - + - **Name** (str) - + - **Network** (list) - - **NetworkInfo** (list) - 见 **VPCNetworkInfo** 模型定义 - **OperatorName** (str) - VPC关联的IPv6网段所属运营商 - - **SubnetCount** (int) - - - **Tag** (str) - - - **UpdateTime** (int) - + - **SubnetCount** (int) - + - **Tag** (str) - + - **UpdateTime** (int) - - **VPCId** (str) - VPCId """ @@ -429,20 +393,20 @@ def describe_vpc_intercom(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 源VPC所在项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 源VPC所在地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 源VPC所在项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 源VPC所在地域。 参见 `地域和可用区列表 `_ - **VPCId** (str) - (Required) VPC短ID - **DstProjectId** (str) - 目的项目ID,默认为全部项目 - **DstRegion** (str) - 目的VPC所在地域,默认为全部地域 - + **Response** - **DataSet** (list) - 见 **VPCIntercomInfo** 模型定义 - + **Response Model** - - **VPCIntercomInfo** - + + **VPCIntercomInfo** + - **DstRegion** (str) - 所属地域 - **Name** (str) - VPC名字 - **Network** (list) - VPC的地址空间 @@ -462,14 +426,14 @@ def modify_route_rule(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **RouteRule** (list) - (Required) 格式: RouteRuleId | 目的网段 | 下一跳类型 | 下一跳 |优先级| 备注 | 增、删、改标志 (下一跳类型为instance或者vip,下一跳为云主机id或者vip的id,优先级使用0,动作标志为add/delete/update) 。"添加"示例: test_id | 10.8.0.0/16 | instance | uhost-xd8ja | 0 | Default Route Rule| add (添加的RouteRuleId填任意非空字符串) 。"删除"示例: routerule-xk3jxa | 10.8.0.0/16 | instance | uhost-xd8ja | 0 | Default Route Rule| delete (RouteRuleId来自DescribeRouteTable中) 。“修改”示例: routerule-xk3jxa | 10.8.0.0/16 | instance | uhost-cjksa2 | 0 | Default Route Rule| update (RouteRuleId来自DescribeRouteTable中) - **RouteTableId** (str) - (Required) 通过DescribeRouteTable拿到 - + **Response** - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -482,16 +446,16 @@ def update_route_table_attribute(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **RouteTableId** (str) - (Required) 路由表ID - **Name** (str) - 名称 - **Remark** (str) - 备注 - **Tag** (str) - 业务组名称 - + **Response** - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -504,15 +468,15 @@ def update_subnet_attribute(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - **SubnetId** (str) - (Required) 子网ID - **Name** (str) - 子网名称(如果Name不填写,Tag必须填写) - **Tag** (str) - 业务组名称(如果Tag不填写,Name必须填写) - + **Response** - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) @@ -525,15 +489,15 @@ def update_vpc_network(self, req=None, **kwargs): **Request** - - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ - - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ - - **Network** (list) - (Required) 需要保留的VPC网段。当前仅支持删除VPC网段,添加网段请参考 `AddVPCNetwork `_ + - **ProjectId** (str) - (Config) 项目ID。不填写为默认项目,子帐号必须填写。 请参考 `GetProjectList接口 `_ + - **Region** (str) - (Config) 地域。 参见 `地域和可用区列表 `_ + - **Network** (list) - (Required) 需要保留的VPC网段。当前仅支持删除VPC网段,添加网段请参考 `AddVPCNetwork `_ - **VPCId** (str) - (Required) VPC的ID - + **Response** - **Message** (str) - 错误信息 - + """ d = {"ProjectId": self.config.project_id, "Region": self.config.region} req and d.update(req) diff --git a/ucloud/services/vpc/schemas/apis.py b/ucloud/services/vpc/schemas/apis.py index cef0fc2..3986831 100644 --- a/ucloud/services/vpc/schemas/apis.py +++ b/ucloud/services/vpc/schemas/apis.py @@ -357,6 +357,9 @@ class DescribeSubnetRequestSchema(schema.RequestSchema): "ProjectId": fields.Str(required=False, dump_to="ProjectId"), "Region": fields.Str(required=True, dump_to="Region"), "RouteTableId": fields.Str(required=False, dump_to="RouteTableId"), + "ShowAvailableIPs": fields.Bool( + required=False, dump_to="ShowAvailableIPs" + ), "SubnetId": fields.Str(required=False, dump_to="SubnetId"), "SubnetIds": fields.List(fields.Str()), "Tag": fields.Str(required=False, dump_to="Tag"), diff --git a/ucloud/services/vpc/schemas/models.py b/ucloud/services/vpc/schemas/models.py index 97a8b97..51ec466 100644 --- a/ucloud/services/vpc/schemas/models.py +++ b/ucloud/services/vpc/schemas/models.py @@ -42,12 +42,12 @@ class SubnetInfoSchema(schema.ResponseSchema): """ fields = { + "AvailableIPs": fields.Int(required=False, load_from="AvailableIPs"), "CreateTime": fields.Int(required=False, load_from="CreateTime"), "Gateway": fields.Str(required=False, load_from="Gateway"), "HasNATGW": fields.Bool(required=False, load_from="HasNATGW"), "IPv6Network": fields.Str(required=False, load_from="IPv6Network"), - "Netmask": fields.Int(required=False, load_from="Netmask"), - "OperatorName": fields.Str(required=False, load_from="OperatorName"), + "Netmask": fields.Str(required=False, load_from="Netmask"), "Remark": fields.Str(required=False, load_from="Remark"), "RouteTableId": fields.Str(required=False, load_from="RouteTableId"), "Subnet": fields.Str(required=False, load_from="Subnet"), @@ -110,7 +110,7 @@ class VPCInfoSchema(schema.ResponseSchema): class VPCIntercomInfoSchema(schema.ResponseSchema): - """ VPCIntercomInfo - + """ VPCIntercomInfo - """ fields = { diff --git a/ucloud/testing/driver/__init__.py b/ucloud/testing/driver/__init__.py new file mode 100644 index 0000000..4a6d914 --- /dev/null +++ b/ucloud/testing/driver/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from ._specification import Specification +from ._scenario import Scenario +from ._step import Step + +spec = Specification() diff --git a/ucloud/testing/driver/_scenario.py b/ucloud/testing/driver/_scenario.py new file mode 100644 index 0000000..201b099 --- /dev/null +++ b/ucloud/testing/driver/_scenario.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +import functools +from ucloud.testing.driver import _step + + +class Scenario(object): + def __init__(self, spec, id_, title=None, owners=None): + self.id = id_ + self.title = title + self.store = {} + self.errors = [] + self.steps = [] + self.spec = spec + self.owners = owners + + def step(self, invoker, *args, **kwargs): + step = _step.Step(self, invoker, *args, **kwargs) + self.steps.append(step) + return step + + def api(self, **step_kwargs): + def deco(fn): + step = self.step(fn, **step_kwargs) + + @functools.wraps(fn) + def wrapped(*args, **kwargs): + return step.run_api(*args, **kwargs) + + return wrapped + + return deco + + def func(self, **step_kwargs): + def deco(fn): + step = self.step(fn, **step_kwargs) + + @functools.wraps(fn) + def wrapped(*args, **kwargs): + return step.run_func(*args, **kwargs) + + return wrapped + + return deco + + @property + def status(self): + if all([(item.status == "skipped") for item in self.steps]): + status = "skipped" + elif any([(item.status == "failed") for item in self.steps]): + status = "failed" + else: + status = "passed" + return status + + @property + def start_time(self): + times = [ + item.start_time for item in self.steps if item.status != "skipped" + ] + return min(times) if times else 0 + + @property + def end_time(self): + times = [ + item.end_time for item in self.steps if item.status != "skipped" + ] + return max(times) if times else 0 + + def json(self): + return { + "title": self.title, + "status": self.status, + "steps": [item.json() for item in self.steps], + "owners": self.owners, + "execution": { + "duration": self.end_time - self.start_time, + "start_time": self.start_time, + "end_time": self.end_time, + }, + "passed_count": len( + [(1) for item in self.steps if item.status == "passed"] + ), + "failed_count": len( + [(1) for item in self.steps if item.status == "failed"] + ), + "skipped_count": len( + [(1) for item in self.steps if item.status == "skipped"] + ), + } + + def __call__(self, *args, **kwargs): + for step in self.steps: + step(step, *args, **kwargs) diff --git a/ucloud/testing/driver/_specification.py b/ucloud/testing/driver/_specification.py new file mode 100644 index 0000000..ac71279 --- /dev/null +++ b/ucloud/testing/driver/_specification.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +from ucloud.testing.driver import _scenario + + +class Specification(object): + def __init__(self): + self.scenarios = [] + + def scenario(self, id_, title="", owners=None): + scenario = _scenario.Scenario(self, id_, title, owners) + self.scenarios.append(scenario) + return scenario + + @property + def status(self): + if all([(item.status == "skipped") for item in self.scenarios]): + status = "skipped" + elif any([(item.status == "failed") for item in self.scenarios]): + status = "failed" + else: + status = "passed" + return status + + @property + def start_time(self): + times = [ + item.start_time + for item in self.scenarios + if item.status != "skipped" + ] + return min(times) if times else 0 + + @property + def end_time(self): + times = [ + item.end_time for item in self.scenarios if item.status != "skipped" + ] + return max(times) if times else 0 + + def json(self): + return { + "status": self.status, + "execution": { + "duration": self.end_time - self.start_time, + "start_time": self.start_time, + "end_time": self.end_time, + }, + "scenarios": [item.json() for item in self.scenarios], + "passed_count": len( + [(1) for item in self.scenarios if item.status == "passed"] + ), + "failed_count": len( + [(1) for item in self.scenarios if item.status == "failed"] + ), + "skipped_count": len( + [(1) for item in self.scenarios if item.status == "skipped"] + ), + } diff --git a/ucloud/testing/driver/_step.py b/ucloud/testing/driver/_step.py new file mode 100644 index 0000000..0b9168d --- /dev/null +++ b/ucloud/testing/driver/_step.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +import logging +import time +from ucloud.core.transport import http +from ucloud.testing import exc, utest, op +from ucloud.testing.exc import ValueNotFoundError, CompareError + +logger = logging.getLogger(__name__) + + +class Step(object): + def __init__( + self, + scenario, + invoker, + max_retries=0, + retry_interval=0, + startup_delay=0, + retry_for=(CompareError, ValueNotFoundError), + fast_fail=False, + validators=None, + title="", + **kwargs + ): + """ Step is the test step in a test scenario + :param invoker: invoker is a callable function + :param max_retries: the maximum retry number by the `retry_for` exception, + it will resolve the flaky testing case + :param retry_interval: the interval between twice retrying + :param retry_for: the exceptions to retrying + :param startup_delay: the delay seconds before any action execution + :param fast_fail: if fast fail is true, the test will fail when got + unexpected exception + :return: + """ + self.invoker = invoker + self.validators = validators or (lambda _: []) + self.max_retries = max_retries + self.retry_interval = retry_interval + self.retry_for = retry_for + self.startup_delay = startup_delay + self.fast_fail = fast_fail + self.start_time = 0 + self.end_time = 0 + self.status = "skipped" + self.title = title + self.type = "api" + self.errors = [] + self.extras = kwargs + self.scenario = scenario + self.store = scenario.store + self.api_retries = [] + + def run_api(self, client, *args, **kwargs): + client.transport.middleware.response(self._handle_response, 0) + try: + result = self._run( + self._set_default_response, client, *args, **kwargs + ) + except Exception as e: + raise e + finally: + client.transport.middleware.response_handlers.pop(0) + return result + + def run_func(self, *args, **kwargs): + return self._run(None, *args, **kwargs) + + def json(self): + return { + "title": self.title, + "type": self.type, + "status": self.status, + "execution": { + "max_retries": self.max_retries, + "retry_interval": self.retry_interval, + "startup_delay": self.startup_delay, + "fast_fail": self.fast_fail, + "duration": self.end_time - self.start_time, + "start_time": self.start_time, + "end_time": self.end_time, + }, + "api_retries": self.api_retries, + "errors": list(set([str(e) for e in self.errors])), + } + + def _handle_response(self, resp): + req = resp.request.payload() + req.pop("Signature", None) + self.api_retries.append( + { + "request": req, + "response": resp.json(), + "request_uuid": resp.headers.get("X-UCLOUD-REQUEST-UUID"), + "request_time": resp.request.request_time, + } + ) + return resp + + def _set_default_response(self, result): + if result is None: + return result + result.setdefault("RetCode", 0) + if "action" in self.extras: + result["Action"] = "{}Response".format(self.extras["action"]) + return result + + def _run(self, invoke_callback=None, *args, **kwargs): + self.start_time = time.time() + try: + result = self._run_with_callback(invoke_callback, *args, **kwargs) + except Exception as e: + raise e + finally: + self.end_time = time.time() + return result + + def _run_with_callback(self, invoke_callback=None, *args, **kwargs): + if self.startup_delay: + time.sleep(self.startup_delay) + for i in range(self.max_retries + 1): + self.errors.clear() + try: + result = self.invoker(self, *args, **kwargs) + except Exception as e: + result = None + self.errors.append(e) + else: + invoke_callback and invoke_callback(result) + for validator in self.validators(self.store): + try: + op.check( + validator[0], + utest.value_at_path(result, validator[1]), + validator[2], + ) + except self.retry_for as e: + self.errors.append(e) + except Exception as e: + self.errors.append(e) + if self.errors: + if i == self.max_retries: + self.status = "failed" + raise exc.ValidateError(self.errors) + if self.retry_interval: + time.sleep(self.retry_interval) + continue + self.status = "passed" + return result or None diff --git a/ucloud/version.py b/ucloud/version.py index c8e1255..f57a59f 100644 --- a/ucloud/version.py +++ b/ucloud/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -version = "0.6.2" +version = "0.8.1"