1.0.1
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
224
tests/conftest.py
Normal file
224
tests/conftest.py
Normal file
@@ -0,0 +1,224 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import inspect
|
||||
import os
|
||||
from types import GenericAlias, UnionType
|
||||
from typing import Any, get_args
|
||||
from urllib3 import disable_warnings
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import dynamix_sdk.types as sdk_types
|
||||
from dynamix_sdk.base import (
|
||||
gen_api_params_cls_name,
|
||||
create_api_params_cls,
|
||||
BaseAPIFunctionProtocol,
|
||||
base_proto_to_http_method,
|
||||
)
|
||||
from dynamix_sdk.utils import JSON, HTTPMethod
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class SDKFunction:
|
||||
api_cls: type[sdk_types.BaseAPI]
|
||||
call_attrs: tuple[str, ...]
|
||||
url_path: str
|
||||
proto_cls: type[BaseAPIFunctionProtocol]
|
||||
proto_method: Callable
|
||||
http_method: HTTPMethod
|
||||
params_model_cls: type[sdk_types.BaseAPIParamsModel]
|
||||
result_cls: type[sdk_types.BaseAPIResult]
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class APISubgroup:
|
||||
name: str
|
||||
cls: sdk_types.BaseAPI
|
||||
functions: tuple[SDKFunction, ...]
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class APIGroup:
|
||||
name: str
|
||||
cls: sdk_types.BaseAPI
|
||||
subgroups: tuple[APISubgroup, ...]
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def api_groups():
|
||||
result_list: list[APIGroup] = []
|
||||
for attr_name, attr_annot in sdk_types.API.__annotations__.items():
|
||||
api_group_name = attr_name
|
||||
api_group_cls = attr_annot
|
||||
|
||||
api_subgroups: list[APISubgroup] = []
|
||||
for attr_name, attr_annot in api_group_cls.__annotations__.items():
|
||||
api_subgroup_name = attr_name
|
||||
api_subgroup_cls = attr_annot
|
||||
|
||||
sdk_functions: list[SDKFunction] = []
|
||||
for attr_name in dir(api_subgroup_cls):
|
||||
if attr_name.startswith('_'):
|
||||
continue
|
||||
|
||||
attr = getattr(api_subgroup_cls, attr_name)
|
||||
if not callable(attr):
|
||||
continue
|
||||
method_name = attr_name
|
||||
method = attr
|
||||
|
||||
for mixin_cls in api_subgroup_cls.__bases__[1:]:
|
||||
if not hasattr(mixin_cls, method_name):
|
||||
continue
|
||||
|
||||
assert issubclass(mixin_cls, BaseAPIFunctionProtocol), (
|
||||
f'Class {mixin_cls.__qualname__}'
|
||||
f' must be inherited from'
|
||||
f' {BaseAPIFunctionProtocol.__qualname__}.'
|
||||
)
|
||||
valid_bases = base_proto_to_http_method.keys()
|
||||
mixin_cls_base = mixin_cls.__base__
|
||||
assert (
|
||||
mixin_cls_base
|
||||
and issubclass(mixin_cls_base, BaseAPIFunctionProtocol)
|
||||
and mixin_cls_base in valid_bases
|
||||
), (
|
||||
f'Class {mixin_cls.__qualname__}'
|
||||
f' must be inherited from one of these classes:'
|
||||
f" {', '.join(p.__qualname__ for p in valid_bases)}."
|
||||
)
|
||||
proto_cls = mixin_cls
|
||||
proto_cls_base = mixin_cls_base
|
||||
break
|
||||
else:
|
||||
raise LookupError(
|
||||
f'{api_subgroup_cls.__qualname__}:'
|
||||
f'mixin class for method "{method_name}" not found.'
|
||||
)
|
||||
|
||||
attr_names = (
|
||||
api_group_name,
|
||||
api_subgroup_name,
|
||||
attr_name,
|
||||
)
|
||||
api_func_url_path = ''
|
||||
for sdk_func_path_part in attr_names:
|
||||
url_path_part = api_subgroup_cls._path_mapping_dict.get(
|
||||
sdk_func_path_part,
|
||||
sdk_func_path_part
|
||||
)
|
||||
api_func_url_path = f'{api_func_url_path}/{url_path_part}'
|
||||
|
||||
api_params_cls_name = gen_api_params_cls_name(
|
||||
api_path=api_func_url_path,
|
||||
)
|
||||
result_cls = inspect.signature(method).return_annotation
|
||||
sdk_functions.append(
|
||||
SDKFunction(
|
||||
api_cls=api_subgroup_cls,
|
||||
call_attrs=attr_names,
|
||||
url_path=api_func_url_path,
|
||||
proto_cls=proto_cls,
|
||||
proto_method=method,
|
||||
http_method=base_proto_to_http_method[proto_cls_base],
|
||||
params_model_cls=create_api_params_cls(
|
||||
cls_name=api_params_cls_name,
|
||||
module_name=result_cls.__module__,
|
||||
protocol_method=method,
|
||||
),
|
||||
result_cls=result_cls,
|
||||
)
|
||||
)
|
||||
|
||||
api_subgroups.append(
|
||||
APISubgroup(
|
||||
name=api_subgroup_name,
|
||||
cls=api_subgroup_cls,
|
||||
functions=tuple(sdk_functions),
|
||||
)
|
||||
)
|
||||
|
||||
result_list.append(
|
||||
APIGroup(
|
||||
name=api_group_name,
|
||||
cls=api_group_cls,
|
||||
subgroups=tuple(api_subgroups),
|
||||
)
|
||||
)
|
||||
|
||||
return tuple(result_list)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def sdk_dx_functions(api_groups):
|
||||
result_list: list[SDKFunction] = []
|
||||
for api_group in api_groups:
|
||||
for api_subgroup in api_group.subgroups:
|
||||
result_list += api_subgroup.functions
|
||||
return tuple(result_list)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def dx_models(sdk_dx_functions: tuple[SDKFunction, ...]):
|
||||
def get_models_from_annotation(annotation: Any):
|
||||
if not annotation:
|
||||
raise TypeError
|
||||
|
||||
models = []
|
||||
|
||||
if annotation is Any:
|
||||
return models
|
||||
|
||||
if isinstance(annotation, (UnionType, GenericAlias)):
|
||||
for annotation in get_args(annotation):
|
||||
models += get_models_from_annotation(annotation=annotation)
|
||||
elif issubclass(annotation, sdk_types.BaseModel):
|
||||
model_cls = annotation
|
||||
models.append(model_cls)
|
||||
|
||||
return set(models)
|
||||
|
||||
def get_nested_models(model_cls: type[sdk_types.BaseModel]):
|
||||
models = []
|
||||
|
||||
for field_info in model_cls.model_fields.values():
|
||||
models += get_models_from_annotation(
|
||||
annotation=field_info.annotation,
|
||||
)
|
||||
for model in models:
|
||||
models += get_nested_models(model_cls=model)
|
||||
|
||||
return set(models)
|
||||
|
||||
dx_models = []
|
||||
for sdk_func in sdk_dx_functions:
|
||||
dx_models.append(sdk_func.params_model_cls)
|
||||
dx_models += get_nested_models(model_cls=sdk_func.params_model_cls)
|
||||
|
||||
if issubclass(sdk_func.result_cls, sdk_types.BaseAPIResultModel):
|
||||
dx_models.append(sdk_func.result_cls)
|
||||
dx_models += get_nested_models(model_cls=sdk_func.result_cls)
|
||||
|
||||
return set(dx_models)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def dx_url():
|
||||
dx_url = os.getenv('DYNAMIX_URL')
|
||||
assert dx_url
|
||||
return dx_url
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def dx_api_definition(dx_url: str) -> JSON:
|
||||
API_DEFINITION_API_PATH = '/restmachine/system/docgenerator/prepareCatalog'
|
||||
|
||||
disable_warnings()
|
||||
|
||||
api_definition_resp = requests.post(
|
||||
url=f'{dx_url}{API_DEFINITION_API_PATH}',
|
||||
verify=False,
|
||||
)
|
||||
api_definition_resp.raise_for_status()
|
||||
|
||||
return api_definition_resp.json()
|
||||
0
tests/local/__init__.py
Normal file
0
tests/local/__init__.py
Normal file
99
tests/local/test_class_naming.py
Normal file
99
tests/local/test_class_naming.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import dynamix_sdk.types as sdk_types
|
||||
from dynamix_sdk.utils import gen_cls_name_from_url_path
|
||||
|
||||
|
||||
def test_api_class_naming(api_groups):
|
||||
for api_group in api_groups:
|
||||
correct_class_name = f'{api_group.name.capitalize()}API'
|
||||
assert api_group.cls.__qualname__ == correct_class_name, (
|
||||
f'\nAPI group: {api_group.name}'
|
||||
f'\nCorrect API class name: {correct_class_name}'
|
||||
)
|
||||
|
||||
for api_subgroup in api_group.subgroups:
|
||||
correct_class_name = (
|
||||
f'{api_group.name.capitalize()}'
|
||||
f'{api_subgroup.name.capitalize()}'
|
||||
f'API'
|
||||
)
|
||||
assert api_subgroup.cls.__qualname__ == correct_class_name, (
|
||||
f'\nAPI group: {api_group.name}'
|
||||
f'\nAPI subgroup: {api_subgroup.name}'
|
||||
f'\nCorrect API class name: {correct_class_name}'
|
||||
)
|
||||
|
||||
|
||||
def test_protocol_class_naming(sdk_dx_functions):
|
||||
for sdk_func in sdk_dx_functions:
|
||||
correct_class_name = gen_cls_name_from_url_path(
|
||||
url_path=sdk_func.url_path,
|
||||
postfix='Protocol',
|
||||
)
|
||||
|
||||
assert sdk_func.proto_cls.__qualname__ == correct_class_name, (
|
||||
f'\nFunction call attributes: {sdk_func.call_attrs}'
|
||||
f'\nURL path: {sdk_func.url_path}'
|
||||
f'\nCorrect Protocol class name: {correct_class_name}'
|
||||
)
|
||||
|
||||
|
||||
def get_subclasses(cls: type, result: list[type] | None = None):
|
||||
if result:
|
||||
_result = result
|
||||
else:
|
||||
_result: list[type] = []
|
||||
|
||||
for subclass in cls.__subclasses__():
|
||||
if issubclass(subclass.__bases__[0], cls):
|
||||
_result.append(subclass)
|
||||
_result += get_subclasses(subclass)
|
||||
|
||||
return _result
|
||||
|
||||
|
||||
def test_params_nested_model_class_naming():
|
||||
base_cls = sdk_types.BaseAPIParamsNestedModel
|
||||
for params_nm_cls in get_subclasses(base_cls):
|
||||
suffix = 'APIParamsNM'
|
||||
assert params_nm_cls.__qualname__.endswith(suffix), (
|
||||
f'Class {params_nm_cls.__qualname__}:'
|
||||
f' all subclasses of {base_cls.__qualname__}'
|
||||
f' must have a name with suffix "{suffix}".'
|
||||
)
|
||||
|
||||
|
||||
def test_result_class_naming(sdk_dx_functions):
|
||||
for sdk_func in sdk_dx_functions:
|
||||
if issubclass(sdk_func.result_cls, sdk_types.BaseAPIResultModel):
|
||||
result_cls_postfix = 'ResultModel'
|
||||
elif issubclass(sdk_func.result_cls, sdk_types.BaseAPIResultStr):
|
||||
result_cls_postfix = 'ResultStr'
|
||||
elif issubclass(sdk_func.result_cls, sdk_types.BaseAPIResultInt):
|
||||
result_cls_postfix = 'ResultInt'
|
||||
elif issubclass(sdk_func.result_cls, sdk_types.BaseAPIResultBool):
|
||||
result_cls_postfix = 'ResultBool'
|
||||
else:
|
||||
raise TypeError
|
||||
|
||||
result_cls_name = gen_cls_name_from_url_path(
|
||||
url_path=sdk_func.url_path,
|
||||
postfix=result_cls_postfix,
|
||||
)
|
||||
assert sdk_func.result_cls.__qualname__ == result_cls_name, (
|
||||
f'\nFunction call attributes: {sdk_func.call_attrs}'
|
||||
f'\nURL path: {sdk_func.url_path}'
|
||||
f'\nResult base class:'
|
||||
f' {sdk_func.result_cls.__bases__[0].__qualname__}'
|
||||
f'\nCorrect result class name: {result_cls_name}'
|
||||
)
|
||||
|
||||
|
||||
def test_result_nested_model_class_naming():
|
||||
base_cls = sdk_types.BaseAPIResultNestedModel
|
||||
for result_nm_cls in get_subclasses(base_cls):
|
||||
suffix = 'APIResultNM'
|
||||
assert result_nm_cls.__qualname__.endswith(suffix), (
|
||||
f'Class {result_nm_cls.__qualname__}:'
|
||||
f' all subclasses of {base_cls.__qualname__}'
|
||||
f' must have a name with suffix "{suffix}".'
|
||||
)
|
||||
68
tests/local/test_name_mapping_file.py
Normal file
68
tests/local/test_name_mapping_file.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from dynamix_sdk import types as sdk_types
|
||||
from dynamix_sdk.base import (
|
||||
get_alias,
|
||||
name_mapping_dict,
|
||||
)
|
||||
|
||||
|
||||
def test_missing_mappings_in_name_mapping_file(dx_models):
|
||||
attrs_without_mapping = []
|
||||
for model_cls in dx_models:
|
||||
for field_name in model_cls.model_fields.keys():
|
||||
try:
|
||||
get_alias(
|
||||
field_name=field_name,
|
||||
model_cls=model_cls,
|
||||
name_mapping_dict=name_mapping_dict
|
||||
)
|
||||
except KeyError:
|
||||
attrs_without_mapping.append(
|
||||
f'{model_cls.__qualname__}.{field_name}'
|
||||
)
|
||||
|
||||
attrs_without_mapping.sort()
|
||||
|
||||
assert not attrs_without_mapping, (
|
||||
f'{len(attrs_without_mapping)} attributes without mapping:'
|
||||
f' {attrs_without_mapping}'
|
||||
)
|
||||
|
||||
|
||||
def test_unused_mappings_in_name_mapping_file(dx_models):
|
||||
mapping_dict_keys = set(name_mapping_dict.keys())
|
||||
|
||||
def exclude_used_keys(
|
||||
model_cls: type[sdk_types.BaseModel],
|
||||
mapping_dict_keys: set[str],
|
||||
):
|
||||
for field_name in model_cls.__annotations__.keys():
|
||||
used_key = None
|
||||
|
||||
individual_alias_key = f'{field_name}__{model_cls.__qualname__}'
|
||||
if individual_alias_key in mapping_dict_keys:
|
||||
used_key = individual_alias_key
|
||||
elif field_name in mapping_dict_keys:
|
||||
used_key = field_name
|
||||
|
||||
if used_key and used_key in mapping_dict_keys:
|
||||
mapping_dict_keys.remove(used_key)
|
||||
|
||||
for base_cls in model_cls.__bases__:
|
||||
if issubclass(base_cls, sdk_types.BaseModel):
|
||||
exclude_used_keys(
|
||||
model_cls=base_cls,
|
||||
mapping_dict_keys=mapping_dict_keys,
|
||||
)
|
||||
|
||||
for model_cls in dx_models:
|
||||
exclude_used_keys(
|
||||
model_cls=model_cls,
|
||||
mapping_dict_keys=mapping_dict_keys,
|
||||
)
|
||||
|
||||
unused_mapping_dict_keys = sorted(mapping_dict_keys)
|
||||
|
||||
assert not unused_mapping_dict_keys, (
|
||||
f'{len(unused_mapping_dict_keys)} unused keys in mapping file:'
|
||||
f' {unused_mapping_dict_keys}.'
|
||||
)
|
||||
154
tests/local/test_types.py
Normal file
154
tests/local/test_types.py
Normal file
@@ -0,0 +1,154 @@
|
||||
from enum import Enum
|
||||
import inspect
|
||||
from types import GenericAlias, ModuleType, UnionType
|
||||
from typing import Any, get_args
|
||||
|
||||
import dynamix_sdk.types as sdk_types
|
||||
import dynamix_sdk.api._nested.params as nested_params
|
||||
import dynamix_sdk.api._nested.result as nested_result
|
||||
import dynamix_sdk.api._nested.enums as nested_enums
|
||||
|
||||
from tests.conftest import SDKFunction
|
||||
|
||||
|
||||
def check_model_field_annotation(
|
||||
annotation: Any,
|
||||
field_descr: str,
|
||||
valid_nested_model_cls: type[sdk_types.BaseModel],
|
||||
):
|
||||
if not annotation:
|
||||
raise TypeError
|
||||
|
||||
if annotation is Any:
|
||||
return
|
||||
|
||||
assert annotation is not list, (
|
||||
f'{field_descr}: missing list elements type annotation.'
|
||||
)
|
||||
|
||||
if isinstance(annotation, (UnionType, GenericAlias)):
|
||||
for annotation in get_args(annotation):
|
||||
check_model_field_annotation(
|
||||
annotation=annotation,
|
||||
field_descr=field_descr,
|
||||
valid_nested_model_cls=valid_nested_model_cls,
|
||||
)
|
||||
elif issubclass(annotation, sdk_types.BaseModel):
|
||||
model_cls = annotation
|
||||
assert issubclass(model_cls, valid_nested_model_cls), (
|
||||
f'{field_descr}: nested model class must be'
|
||||
f' a subclass of {valid_nested_model_cls.__qualname__}.'
|
||||
)
|
||||
|
||||
check_model_field_annotations(
|
||||
model_cls=model_cls,
|
||||
valid_nested_model_cls=valid_nested_model_cls,
|
||||
)
|
||||
|
||||
|
||||
def check_model_field_annotations(
|
||||
model_cls: type[sdk_types.BaseModel],
|
||||
valid_nested_model_cls: type[sdk_types.BaseModel],
|
||||
):
|
||||
for field_name, field_info in model_cls.model_fields.items():
|
||||
field_descr = f'{model_cls.__qualname__}.{field_name}'
|
||||
|
||||
check_model_field_annotation(
|
||||
annotation=field_info.annotation,
|
||||
field_descr=field_descr,
|
||||
valid_nested_model_cls=valid_nested_model_cls,
|
||||
)
|
||||
|
||||
|
||||
def test_params_model_fields_type(sdk_dx_functions: tuple[SDKFunction, ...]):
|
||||
for sdk_func in sdk_dx_functions:
|
||||
params = inspect.signature(sdk_func.proto_method).parameters
|
||||
for param_name, param in params.items():
|
||||
if param_name == 'self':
|
||||
continue
|
||||
|
||||
field_descr = (
|
||||
f'{sdk_func.proto_cls.__qualname__}.'
|
||||
f'{sdk_func.call_attrs[-1]}.'
|
||||
f'{param_name}'
|
||||
)
|
||||
|
||||
assert param.annotation is not inspect.Parameter.empty, (
|
||||
f'{field_descr}: missing type annotation.'
|
||||
)
|
||||
|
||||
assert param.kind == param.KEYWORD_ONLY, (
|
||||
f'Parameter {field_descr} must be keyword-only.'
|
||||
)
|
||||
|
||||
check_model_field_annotation(
|
||||
annotation=param.annotation,
|
||||
field_descr=field_descr,
|
||||
valid_nested_model_cls=sdk_types.BaseAPIParamsNestedModel,
|
||||
)
|
||||
|
||||
|
||||
def test_function_return_type(sdk_dx_functions):
|
||||
for sdk_func in sdk_dx_functions:
|
||||
assert issubclass(sdk_func.result_cls, sdk_types.BaseAPIResult), (
|
||||
f'Return type for method'
|
||||
f' {sdk_func.proto_cls.__qualname__}.{sdk_func.call_attrs[-1]}'
|
||||
f' must be a subclass of BaseAPIResult.'
|
||||
)
|
||||
|
||||
|
||||
def test_result_model_fields_type():
|
||||
valid_nested_model_cls = sdk_types.BaseAPIResultNestedModel
|
||||
|
||||
for subcls in sdk_types.BaseAPIResultModel.__subclasses__():
|
||||
check_model_field_annotations(
|
||||
model_cls=subcls,
|
||||
valid_nested_model_cls=valid_nested_model_cls,
|
||||
)
|
||||
for subcls in sdk_types.BaseAPIResultNestedModel.__subclasses__():
|
||||
check_model_field_annotations(
|
||||
model_cls=subcls,
|
||||
valid_nested_model_cls=valid_nested_model_cls,
|
||||
)
|
||||
|
||||
|
||||
def check_class_inheritance_in_module(
|
||||
module: ModuleType,
|
||||
valid_base_cls: type,
|
||||
):
|
||||
for attr_name in dir(module):
|
||||
if attr_name.startswith('_'):
|
||||
continue
|
||||
|
||||
attr = getattr(module, attr_name)
|
||||
|
||||
if not isinstance(attr, type):
|
||||
continue
|
||||
|
||||
nested_result_cls = attr
|
||||
|
||||
assert issubclass(nested_result_cls, valid_base_cls), (
|
||||
f'Class {nested_result_cls.__qualname__}:'
|
||||
f' must be a subclass of class {valid_base_cls.__qualname__}.'
|
||||
)
|
||||
|
||||
|
||||
def test_class_inheritance_in_nested_result_module():
|
||||
check_class_inheritance_in_module(
|
||||
module=nested_result,
|
||||
valid_base_cls=sdk_types.BaseAPIResultNestedModel,
|
||||
)
|
||||
|
||||
|
||||
def test_class_inheritance_in_nested_params_module():
|
||||
check_class_inheritance_in_module(
|
||||
module=nested_params,
|
||||
valid_base_cls=sdk_types.BaseAPIParamsNestedModel,
|
||||
)
|
||||
|
||||
|
||||
def test_class_inheritance_in_nested_enums_module():
|
||||
check_class_inheritance_in_module(
|
||||
module=nested_enums,
|
||||
valid_base_cls=Enum,
|
||||
)
|
||||
351
tests/test_with_api_definition.py
Normal file
351
tests/test_with_api_definition.py
Normal file
@@ -0,0 +1,351 @@
|
||||
from enum import Enum
|
||||
import inspect
|
||||
from types import GenericAlias, NoneType, UnionType
|
||||
from typing import Any, get_args
|
||||
from warnings import warn
|
||||
|
||||
from dynamix_sdk import types as sdk_types
|
||||
from dynamix_sdk.base import (
|
||||
get_alias,
|
||||
name_mapping_dict,
|
||||
)
|
||||
|
||||
from dynamix_sdk.utils import JSON, get_nested_value
|
||||
from tests.conftest import SDKFunction
|
||||
|
||||
|
||||
expected_inconsistencies: list[str] = [
|
||||
'cloudapi.disks.limit_io: API has parameter "iops" but this SDK function doesn\'t have corresponding parameter.', # noqa: E501
|
||||
'cloudapi.image.list: API has parameter "architecture" but this SDK function doesn\'t have corresponding parameter.', # noqa: E501
|
||||
'cloudapi.bservice.group_stop: default value of parameter "force" must be None.', # noqa: E501
|
||||
'cloudapi.bservice.group_stop: annotation of parameter "force" must be Union.', # noqa: E501
|
||||
'cloudapi.bservice.delete: default value of parameter "permanently" must be None.', # noqa: E501
|
||||
'cloudapi.bservice.delete: annotation of parameter "permanently" must be Union.', # noqa: E501
|
||||
'cloudapi.compute.net_attach: result type must be child of BaseAPIResultBool.', # noqa: E501
|
||||
'cloudapi.compute.net_attach: default value of parameter "mtu" must be 1500.', # noqa: E501
|
||||
'''cloudapi.compute.update: parameter "cpu_pin", target_annotation = None | bool, expected_annot = <class 'bool'>''', # noqa: E501
|
||||
'cloudapi.disks.create: default value of parameter "size_gb" must be 10.',
|
||||
'''cloudapi.disks.create: API has parameter "ssdSize" but this SDK function doesn't have corresponding parameter.''', # noqa: E501
|
||||
'cloudbroker.account.create: annotation of parameter "uniq_pools" must contain int.', # noqa: E501
|
||||
'cloudapi.kvmx86.create: annotation of parameter "ci_user_data" must contain BaseAPIParamsNestedModel.', # noqa: E501
|
||||
'''cloudapi.image.create: API has parameter "architecture" but this SDK function doesn't have corresponding parameter.''', # noqa: E501
|
||||
'cloudapi.lb.create: annotation of parameter "sysctl_params" must contain BaseAPIParamsNestedModel.', # noqa: E501
|
||||
]
|
||||
|
||||
|
||||
def test_with_api_definition(
|
||||
dx_api_definition: JSON,
|
||||
sdk_dx_functions: tuple[SDKFunction, ...],
|
||||
):
|
||||
inconsistencies: list[str] = []
|
||||
for sdk_func in sdk_dx_functions:
|
||||
func_full_api_path = f'/restmachine{sdk_func.url_path}'
|
||||
|
||||
if not isinstance(dx_api_definition, dict):
|
||||
raise TypeError
|
||||
|
||||
dx_api_definition_paths = dx_api_definition['paths']
|
||||
|
||||
if not isinstance(dx_api_definition_paths, dict):
|
||||
raise TypeError
|
||||
|
||||
if func_full_api_path not in dx_api_definition_paths:
|
||||
inconsistencies.append(
|
||||
f"{'.'.join(sdk_func.call_attrs)}:"
|
||||
f' url path {sdk_func.url_path} not found in'
|
||||
f' DX API definition.'
|
||||
)
|
||||
continue
|
||||
|
||||
func_api_definition = dx_api_definition_paths[func_full_api_path]
|
||||
|
||||
if not isinstance(func_api_definition, dict):
|
||||
raise TypeError
|
||||
|
||||
http_method_lower = sdk_func.http_method.lower()
|
||||
if http_method_lower not in func_api_definition:
|
||||
api_def_http_methods = [
|
||||
m.upper() for m in func_api_definition.keys()
|
||||
]
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' HTTP method {sdk_func.http_method} not found.'
|
||||
f' DX API definition for {sdk_func.url_path} has'
|
||||
f' only these HTTP methods:'
|
||||
f" {', '.join(api_def_http_methods)}."
|
||||
)
|
||||
continue
|
||||
func_api_def_method = func_api_definition[http_method_lower]
|
||||
|
||||
if not isinstance(func_api_def_method, dict):
|
||||
raise TypeError
|
||||
|
||||
type_mappings = {
|
||||
'integer': int,
|
||||
'string': str,
|
||||
'boolean': bool,
|
||||
'object': sdk_types.BaseAPIParamsNestedModel,
|
||||
'number': float,
|
||||
}
|
||||
|
||||
if api_def_result_type := get_nested_value(
|
||||
d=func_api_def_method,
|
||||
keys=('responses', '200', 'schema', 'type'),
|
||||
):
|
||||
expected_result_type = type_mappings[api_def_result_type]
|
||||
if expected_result_type is bool:
|
||||
expected_result_type = sdk_types.BaseAPIResultBool
|
||||
if not issubclass(sdk_func.result_cls, expected_result_type):
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' result type must be child of'
|
||||
f' {expected_result_type.__qualname__}.'
|
||||
)
|
||||
|
||||
sdk_func_params: dict[str, inspect.Parameter] = {}
|
||||
for p in inspect.signature(sdk_func.proto_method).parameters.values():
|
||||
if p.name == 'self':
|
||||
continue
|
||||
alias = get_alias(
|
||||
field_name=p.name,
|
||||
model_cls=sdk_func.params_model_cls,
|
||||
name_mapping_dict=name_mapping_dict,
|
||||
)
|
||||
sdk_func_params[alias] = p
|
||||
|
||||
api_definition_params = func_api_def_method.get('parameters')
|
||||
if not api_definition_params:
|
||||
if sdk_func_params:
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' this function must not have parameters.'
|
||||
)
|
||||
continue
|
||||
|
||||
if not isinstance(api_definition_params, list):
|
||||
raise TypeError
|
||||
|
||||
dx_param_names = set()
|
||||
for p in api_definition_params:
|
||||
if not isinstance(p, dict):
|
||||
raise TypeError
|
||||
dx_param_names.add(p['name'])
|
||||
|
||||
if len(sdk_func_params) > len(dx_param_names):
|
||||
unused_sdk_param_names = []
|
||||
for sdk_param_alias, sdk_param in sdk_func_params.items():
|
||||
if sdk_param_alias not in dx_param_names:
|
||||
unused_sdk_param_names.append(sdk_param.name)
|
||||
if unused_sdk_param_names:
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' unused parameters:'
|
||||
f' {", ".join(sorted(unused_sdk_param_names))}'
|
||||
)
|
||||
|
||||
for api_def_param in api_definition_params:
|
||||
if not isinstance(api_def_param, dict):
|
||||
raise TypeError
|
||||
|
||||
api_def_param_enum = api_def_param.get('enum')
|
||||
if (
|
||||
api_def_param_enum is not None
|
||||
and not isinstance(api_def_param_enum, list)
|
||||
):
|
||||
raise TypeError
|
||||
|
||||
api_def_param_default = api_def_param.get('default')
|
||||
|
||||
api_def_param_type = api_def_param.get('type')
|
||||
if (
|
||||
api_def_param_type is not None
|
||||
and not isinstance(api_def_param_type, str)
|
||||
):
|
||||
raise TypeError
|
||||
|
||||
api_def_param_items = api_def_param.get('items')
|
||||
if (
|
||||
api_def_param_items is not None
|
||||
and not isinstance(api_def_param_items, dict)
|
||||
):
|
||||
raise TypeError
|
||||
|
||||
necessary_dx_param = not (
|
||||
api_def_param_enum
|
||||
and len(api_def_param_enum) == 1
|
||||
and api_def_param_default is not None
|
||||
)
|
||||
|
||||
sdk_param_alias = api_def_param['name']
|
||||
if not isinstance(sdk_param_alias, str):
|
||||
raise TypeError
|
||||
|
||||
param_exists_in_sdk = sdk_param_alias in sdk_func_params.keys()
|
||||
|
||||
if not param_exists_in_sdk:
|
||||
if necessary_dx_param:
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' API has parameter "{sdk_param_alias}"'
|
||||
f" but this SDK function doesn't have"
|
||||
f' corresponding parameter.'
|
||||
)
|
||||
continue
|
||||
|
||||
sdk_param_name = sdk_func_params[sdk_param_alias].name
|
||||
|
||||
if not necessary_dx_param:
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' parameter "{sdk_param_name} is unnecessary in SDK.'
|
||||
)
|
||||
continue
|
||||
|
||||
sdk_func_param = sdk_func_params[sdk_param_alias]
|
||||
|
||||
if sdk_func_param.annotation is Any:
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' annotation of parameter "{sdk_param_name}"'
|
||||
f' must not be Any.'
|
||||
)
|
||||
continue
|
||||
|
||||
if api_def_param_default and api_def_param_default != -1:
|
||||
if sdk_func_param.default != api_def_param_default:
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' default value of parameter "{sdk_param_name}"'
|
||||
f' must be {api_def_param_default}.'
|
||||
)
|
||||
|
||||
target_annotation = sdk_func_param.annotation
|
||||
if not api_def_param.get('required') and (
|
||||
api_def_param_default is None
|
||||
or (
|
||||
api_def_param_default == 0
|
||||
and api_def_param_default is not False
|
||||
)
|
||||
or api_def_param_default == ''
|
||||
or api_def_param_default == -1
|
||||
):
|
||||
if sdk_func_param.default is not None:
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' default value of parameter "{sdk_param_name}"'
|
||||
f' must be None.'
|
||||
)
|
||||
|
||||
if not isinstance(sdk_func_param.annotation, UnionType):
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' annotation of parameter "{sdk_param_name}"'
|
||||
f' must be Union.'
|
||||
)
|
||||
continue
|
||||
|
||||
annot_union_args = get_args(sdk_func_param.annotation)
|
||||
if NoneType not in annot_union_args:
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' annotation of parameter "{sdk_param_name}"'
|
||||
f' must contain None.'
|
||||
)
|
||||
continue
|
||||
|
||||
if not len(annot_union_args) == 2:
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' annotation of parameter "{sdk_param_name}"'
|
||||
f' must contain two types.'
|
||||
)
|
||||
continue
|
||||
|
||||
if annot_union_args[0] is NoneType:
|
||||
target_annotation = annot_union_args[1]
|
||||
else:
|
||||
target_annotation = annot_union_args[0]
|
||||
|
||||
if api_def_param_type == 'array':
|
||||
if (
|
||||
not isinstance(target_annotation, GenericAlias)
|
||||
or target_annotation.__origin__ is not list
|
||||
):
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' annotation of parameter "{sdk_param_name}"'
|
||||
f' must contain list[...].'
|
||||
)
|
||||
continue
|
||||
|
||||
if api_def_param_items is not None:
|
||||
api_def_param_items_type = api_def_param_items.get('type')
|
||||
if api_def_param_items_type is None:
|
||||
continue
|
||||
if not isinstance(api_def_param_items_type, str):
|
||||
raise TypeError
|
||||
|
||||
target_annotation = get_args(target_annotation)[0]
|
||||
expected_annot = type_mappings.get(
|
||||
api_def_param_items_type
|
||||
)
|
||||
if expected_annot is None:
|
||||
continue
|
||||
elif api_def_param_type is not None:
|
||||
expected_annot = type_mappings.get(api_def_param_type)
|
||||
if expected_annot is None:
|
||||
continue
|
||||
|
||||
if not inspect.isclass(target_annotation):
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' parameter "{sdk_param_name}",'
|
||||
f' {target_annotation = },'
|
||||
f' {expected_annot = }'
|
||||
)
|
||||
continue
|
||||
|
||||
if api_def_param_enum:
|
||||
if not issubclass(target_annotation, Enum):
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' annotation of parameter "{sdk_param_name}"'
|
||||
f' must contain enum.'
|
||||
)
|
||||
continue
|
||||
enum_from_annot = target_annotation
|
||||
enum_values = enum_from_annot.__members__.values()
|
||||
if sorted(api_def_param_enum) != sorted(enum_values):
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' parameter "{sdk_param_name}":'
|
||||
f' enum {enum_from_annot.__qualname__}'
|
||||
f' must contain these values: {api_def_param_enum}.'
|
||||
)
|
||||
elif not issubclass(target_annotation, expected_annot):
|
||||
inconsistencies.append(
|
||||
f'{".".join(sdk_func.call_attrs)}:'
|
||||
f' annotation of parameter "{sdk_param_name}"'
|
||||
f' must contain {expected_annot.__qualname__}.'
|
||||
)
|
||||
continue
|
||||
|
||||
for expected_inconsistency in expected_inconsistencies:
|
||||
if expected_inconsistency in inconsistencies:
|
||||
inconsistencies.remove(expected_inconsistency)
|
||||
else:
|
||||
inconsistencies.append(
|
||||
'This expected inconsistency not found:'
|
||||
f' {expected_inconsistency}'
|
||||
)
|
||||
|
||||
if expected_inconsistencies:
|
||||
warn(
|
||||
'\nExpected inconsistencies:\n'
|
||||
+ '\n'.join(expected_inconsistencies)
|
||||
)
|
||||
|
||||
assert not inconsistencies, (
|
||||
f'found {len(inconsistencies)} inconsistencies:\n'
|
||||
+ '\n'.join(inconsistencies)
|
||||
)
|
||||
Reference in New Issue
Block a user