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