You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

225 lines
7.2 KiB

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()