diff --git a/tools/async_client_generator/base_client_generator.py b/tools/async_client_generator/base_client_generator.py index 3f4b5c73..017fa8ba 100644 --- a/tools/async_client_generator/base_client_generator.py +++ b/tools/async_client_generator/base_client_generator.py @@ -1,74 +1,40 @@ -# type: ignore -import ast -from typing import Optional +from typing import Dict, List, Optional +from tools.async_client_generator.base_generator import BaseGenerator +from tools.async_client_generator.transformers import ( + ClassDefTransformer, + ConstantTransformer, + FunctionDefTransformer, +) -class AsyncAwaitTransformer(ast.NodeTransformer): + +class BaseClientGenerator(BaseGenerator): def __init__( self, - keep_sync: Optional[list[str]] = None, - class_replace_map: Optional[dict] = None, - import_replace_map: Optional[dict] = None, - constant_replace_map: Optional[dict] = None, + keep_sync: Optional[List[str]] = None, + class_replace_map: Optional[Dict[str, str]] = None, + constant_replace_map: Optional[Dict[str, str]] = None, ): - self._async_methods = None - self.keep_sync = keep_sync if keep_sync is not None else [] - self.class_replace_map = class_replace_map if class_replace_map is not None else {} - self.import_replace_map = import_replace_map if import_replace_map is not None else {} - self.constant_replace_map = ( - constant_replace_map if constant_replace_map is not None else {} - ) - - def _keep_sync(self, name: str) -> bool: - return name in self.keep_sync - - def visit_FunctionDef(self, sync_node: ast.FunctionDef): - if self._keep_sync(sync_node.name): - return self.generic_visit(sync_node) + super().__init__() - async_node = ast.AsyncFunctionDef( - name=sync_node.name, - args=sync_node.args, - body=sync_node.body, - decorator_list=sync_node.decorator_list, - returns=sync_node.returns, - type_comment=sync_node.type_comment, - ) - async_node.lineno = sync_node.lineno - async_node.col_offset = sync_node.col_offset - async_node.end_lineno = sync_node.end_lineno - async_node.end_col_offset = sync_node.end_col_offset - return self.generic_visit(async_node) - - def visit_ClassDef(self, node: ast.ClassDef): - # update class name - for old_value, new_value in self.class_replace_map.items(): - node.name = node.name.replace(old_value, new_value) - return self.generic_visit(node) - - def visit_Constant(self, node: ast.arg): - for old_value, new_value in self.constant_replace_map.items(): - if isinstance(node.value, str): - node.value = node.value.replace(old_value, new_value) - return self.generic_visit(node) + self.transformers.append(FunctionDefTransformer(keep_sync)) + self.transformers.append(ClassDefTransformer(class_replace_map)) + self.transformers.append(ConstantTransformer(constant_replace_map)) if __name__ == "__main__": - from .config import CODE_DIR + from tools.async_client_generator.config import CLIENT_DIR, CODE_DIR - with open(CODE_DIR / "client_base.py", "r") as source_file: + with open(CLIENT_DIR / "client_base.py", "r") as source_file: code = source_file.read() # Parse the code into an AST - parsed_code = ast.parse(code) - - await_transformer = AsyncAwaitTransformer( + base_client_generator = BaseClientGenerator( keep_sync=["__init__", "upload_records", "upload_collection", "migrate"], class_replace_map={"QdrantBase": "AsyncQdrantBase"}, constant_replace_map={"QdrantBase": "AsyncQdrantBase"}, ) - modified_code_ast = await_transformer.visit(parsed_code) - modified_code = ast.unparse(modified_code_ast) + modified_code = base_client_generator.generate(code) - with open("async_client_base.py", "w") as target_file: + with open(CODE_DIR / "async_client_base.py", "w") as target_file: target_file.write(modified_code) diff --git a/tools/async_client_generator/base_generator.py b/tools/async_client_generator/base_generator.py new file mode 100644 index 00000000..3a84ac91 --- /dev/null +++ b/tools/async_client_generator/base_generator.py @@ -0,0 +1,16 @@ +# type: ignore +import ast +from typing import List + + +class BaseGenerator: + def __init__(self): + self.transformers: List[ast.NodeTransformer] = [] + + def generate(self, code: str) -> str: + nodes = ast.parse(code) + + for transformer in self.transformers: + nodes = transformer.visit(nodes) + + return ast.unparse(nodes) diff --git a/tools/async_client_generator/client_generator.py b/tools/async_client_generator/client_generator.py index a919b6bc..c1d527a3 100644 --- a/tools/async_client_generator/client_generator.py +++ b/tools/async_client_generator/client_generator.py @@ -1,190 +1,73 @@ # type: ignore - -import ast import inspect -from typing import Optional - -from .async_client_base import AsyncQdrantBase - - -class AsyncAwaitTransformer(ast.NodeTransformer): +from typing import Dict, List, Optional + +from tools.async_client_generator.async_client_base import AsyncQdrantBase +from tools.async_client_generator.base_generator import BaseGenerator +from tools.async_client_generator.transformers import ( + AnnAssignTransformer, + CallTransformer, + ClassDefTransformer, + ImportFromTransformer, + ImportTransformer, + NameTransformer, +) +from tools.async_client_generator.transformers.client import ( + ClientFunctionDefTransformer, +) + + +class ClientGenerator(BaseGenerator): def __init__( self, - keep_sync: Optional[list[str]] = None, - class_replace_map: Optional[dict] = None, - import_replace_map: Optional[dict] = None, - exclude_methods: Optional[list[str]] = None, - rename_methods: Optional[dict[str, str]] = None, + keep_sync: Optional[List[str]] = None, + class_replace_map: Optional[Dict[str, str]] = None, + import_replace_map: Optional[Dict[str, str]] = None, + exclude_methods: Optional[List[str]] = None, + rename_methods: Optional[Dict[str, str]] = None, ): + super().__init__() self._async_methods = None - self.keep_sync = keep_sync if keep_sync is not None else [] - self.class_replace_map = class_replace_map if class_replace_map is not None else {} - self.import_replace_map = import_replace_map if import_replace_map is not None else {} - self.exclude_methods = exclude_methods if exclude_methods is not None else [] - self.rename_methods = rename_methods if rename_methods is not None else {} - def _keep_sync(self, name: str) -> bool: - return name in self.keep_sync + self.transformers.append(ImportTransformer(import_replace_map)) + self.transformers.append(ImportFromTransformer(import_replace_map)) + self.transformers.append( + ClientFunctionDefTransformer( + keep_sync, class_replace_map, exclude_methods, rename_methods, self.async_methods + ) + ) + self.transformers.append(ClassDefTransformer(class_replace_map)) + self.transformers.append(AnnAssignTransformer(class_replace_map)) + + # call_transformer should be after function_def_transformer + self.transformers.append(CallTransformer(class_replace_map, self.async_methods)) + # name_transformer should be after function_def, class_def and ann_assign transformers + self.transformers.append( + NameTransformer(class_replace_map, import_replace_map, rename_methods) + ) @property - def async_methods(self): + def async_methods(self) -> List[str]: if self._async_methods is None: self._async_methods = self.get_async_methods(AsyncQdrantBase) return self._async_methods @staticmethod - def get_async_methods(class_obj): + def get_async_methods(class_obj: type) -> List[str]: async_methods = [] for name, method in inspect.getmembers(class_obj): if inspect.iscoroutinefunction(method): async_methods.append(name) return async_methods - def visit_Name(self, node: ast.Name): - if node.id in self.class_replace_map: - node.id = self.class_replace_map[node.id] - elif node.id in self.import_replace_map: - node.id = self.import_replace_map[node.id] - elif node.id in self.rename_methods: - node.id = self.rename_methods[node.id] - return self.generic_visit(node) - - def visit_Call(self, node): - if isinstance(node.func, ast.Name): - if node.func.id in self.class_replace_map: - node.func.id = self.class_replace_map[node.func.id] - - if isinstance(node.func, ast.Attribute): - if node.func.attr in self.async_methods: - return ast.Await(value=node) - - return self.generic_visit(node) - - def visit_AnnAssign(self, node: ast.AnnAssign): - for old_value, new_value in self.class_replace_map.items(): - if isinstance(node.annotation, ast.Name): - node.annotation.id = node.annotation.id.replace(old_value, new_value) - return self.generic_visit(node) - - def visit_FunctionDef(self, sync_node: ast.FunctionDef): - if sync_node.name in self.exclude_methods: - return None - - if sync_node.name == "__init__": - return self.generate_init(sync_node) - - if self._keep_sync(sync_node.name): - return self.generic_visit(sync_node) - - async_node = ast.AsyncFunctionDef( - name=sync_node.name, - args=sync_node.args, - body=sync_node.body, - decorator_list=sync_node.decorator_list, - returns=sync_node.returns, - type_comment=sync_node.type_comment, - ) - async_node.lineno = sync_node.lineno - async_node.col_offset = sync_node.col_offset - async_node.end_lineno = sync_node.end_lineno - async_node.end_col_offset = sync_node.end_col_offset - return self.generic_visit(async_node) - - def visit_Import(self, node: ast.Import): - for old_value, new_value in self.import_replace_map.items(): - for alias in node.names: - alias.name = alias.name.replace(old_value, new_value) - return self.generic_visit(node) - - def visit_ImportFrom(self, node: ast.ImportFrom): - # update module name - for old_value, new_value in self.import_replace_map.items(): - node.module = node.module.replace(old_value, new_value) - - # update imported item name - for alias in node.names: - if hasattr(alias, "name"): - for old_value, new_value in self.import_replace_map.items(): - alias.name = alias.name.replace(old_value, new_value) - - return self.generic_visit(node) - - def visit_ClassDef(self, node: ast.ClassDef): - # update class name - for old_value, new_value in self.class_replace_map.items(): - node.name = node.name.replace(old_value, new_value) - - # update parent classes names - for base in node.bases: - for old_value, new_value in self.class_replace_map.items(): - base.id = base.id.replace(old_value, new_value) - return self.generic_visit(node) - - def generate_init(self, sync_node: ast.FunctionDef): - def traverse(node): - assignment_nodes = [] - - if isinstance(node, ast.Assign): - assignment_nodes.append(node) - for field_name, field_value in ast.iter_fields(node): - if isinstance(field_value, ast.AST): - assignment_nodes.extend(traverse(field_value)) - elif isinstance(field_value, list): - for item in field_value: - if isinstance(item, ast.AST): - assignment_nodes.extend(traverse(item)) - return assignment_nodes - - def unwrap_orelse_assignment(assign_node: ast.Assign): - for target in assign_node.targets: - if isinstance(target, ast.Attribute) and target.attr == "_client": - if isinstance(assign_node.value, ast.Call): - if assign_node.value.func.id in self.class_replace_map: - assign_node.value.func.id = self.class_replace_map[ - assign_node.value.func.id - ] - return assign_node - - args, defaults = [sync_node.args.args[0]], [] - for arg, default in zip(sync_node.args.args[1:], sync_node.args.defaults): - if arg.arg not in ("location", "path"): - args.append(arg) - defaults.append(default) - sync_node.args.args = args - sync_node.args.defaults = defaults - - for i, child_node in enumerate(sync_node.body): - if isinstance(child_node, ast.If): - orelse_assignment_nodes = traverse(child_node) - assignments = list( - filter( - lambda x: x, - [ - unwrap_orelse_assignment(assign_node) - for assign_node in orelse_assignment_nodes - ], - ) - ) - if len(assignments) == 1: - sync_node.body[i] = assignments[0] - break - return self.generic_visit(sync_node) - - -class ClientAsyncAwaitTransformer(AsyncAwaitTransformer): - def _keep_sync(self, name: str) -> bool: - return name in self.keep_sync or name not in self.async_methods - if __name__ == "__main__": - from .config import CODE_DIR + from tools.async_client_generator.config import CLIENT_DIR, CODE_DIR - with open(CODE_DIR / "qdrant_client.py", "r") as source_file: + with open(CLIENT_DIR / "qdrant_client.py", "r") as source_file: code = source_file.read() - parsed_code = ast.parse(code) - - await_transformer = ClientAsyncAwaitTransformer( + generator = ClientGenerator( class_replace_map={ "QdrantBase": "AsyncQdrantBase", "QdrantFastembedMixin": "AsyncQdrantFastembedMixin", @@ -208,8 +91,8 @@ def _keep_sync(self, name: str) -> bool: "async_grpc_points", ], ) - modified_code_ast = await_transformer.visit(parsed_code) - modified_code = ast.unparse(modified_code_ast) - with open("async_qdrant_client.py", "w") as target_file: + modified_code = generator.generate(code) + + with open(CODE_DIR / "async_qdrant_client.py", "w") as target_file: target_file.write(modified_code) diff --git a/tools/async_client_generator/config.py b/tools/async_client_generator/config.py index 5cc918fe..98fed10e 100644 --- a/tools/async_client_generator/config.py +++ b/tools/async_client_generator/config.py @@ -1,4 +1,5 @@ from pathlib import Path -ROOT_DIR = Path(__file__).parent.parent.parent -CODE_DIR = ROOT_DIR / "qdrant_client" +CODE_DIR = Path(__file__).parent +ROOT_DIR = CODE_DIR.parent.parent +CLIENT_DIR = ROOT_DIR / "qdrant_client" diff --git a/tools/async_client_generator/fastembed_generator.py b/tools/async_client_generator/fastembed_generator.py index cafc6417..58932299 100644 --- a/tools/async_client_generator/fastembed_generator.py +++ b/tools/async_client_generator/fastembed_generator.py @@ -1,112 +1,58 @@ # type: ignore -import ast import inspect -from typing import Optional - -from .async_client_base import AsyncQdrantBase - - -# Define a custom AST transformer to add 'await' before method calls -class AsyncAwaitTransformer(ast.NodeTransformer): +from typing import Dict, List, Optional + +from tools.async_client_generator.async_client_base import AsyncQdrantBase +from tools.async_client_generator.base_generator import BaseGenerator +from tools.async_client_generator.transformers import ( + ClassDefTransformer, + ImportFromTransformer, + ImportTransformer, +) +from tools.async_client_generator.transformers.fastembed import ( + FastembedCallTransformer, + FastembedFunctionDefTransformer, +) + + +class FastembedGenerator(BaseGenerator): def __init__( self, - keep_sync: Optional[list[str]] = None, - class_replace_map: Optional[dict] = None, - import_replace_map: Optional[dict] = None, + keep_sync: Optional[List[str]] = None, + class_replace_map: Optional[Dict[str, str]] = None, + import_replace_map: Optional[Dict[str, str]] = None, ): + super().__init__() self._async_methods = None - self.keep_sync = keep_sync if keep_sync is not None else [] - self.class_replace_map = class_replace_map if class_replace_map is not None else {} - self.import_replace_map = import_replace_map if import_replace_map is not None else {} - - def _keep_sync(self, name: str) -> bool: - return name in self.keep_sync + self.transformers.append(FastembedCallTransformer(self.async_methods)) + self.transformers.append(ClassDefTransformer(class_replace_map)) + self.transformers.append(ImportTransformer(import_replace_map)) + self.transformers.append(ImportFromTransformer(import_replace_map)) + self.transformers.append(FastembedFunctionDefTransformer(keep_sync)) @property - def async_methods(self): + def async_methods(self) -> List[str]: if self._async_methods is None: self._async_methods = self.get_async_methods(AsyncQdrantBase) return self._async_methods @staticmethod - def get_async_methods(class_obj): + def get_async_methods(class_obj: type) -> List[str]: async_methods = [] for name, method in inspect.getmembers(class_obj): if inspect.iscoroutinefunction(method): async_methods.append(name) return async_methods - def visit_Call(self, node): - if isinstance(node.func, ast.Attribute): - if isinstance(node.func.value, ast.Name) and node.func.value.id == "self": - if node.func.attr in self.async_methods: - return ast.Await(value=node) - return self.generic_visit(node) - - def visit_FunctionDef(self, sync_node: ast.FunctionDef): - if self._keep_sync(sync_node.name): - return self.generic_visit(sync_node) - async_node = ast.AsyncFunctionDef( - name=sync_node.name, - args=sync_node.args, - body=sync_node.body, - decorator_list=sync_node.decorator_list, - returns=sync_node.returns, - type_comment=sync_node.type_comment, - ) - async_node.lineno = sync_node.lineno - async_node.col_offset = sync_node.col_offset - async_node.end_lineno = sync_node.end_lineno - async_node.end_col_offset = sync_node.end_col_offset - return self.generic_visit(async_node) - - def visit_Import(self, node: ast.Import): - for old_value, new_value in self.import_replace_map.items(): - for alias in node.names: - alias.name = alias.name.replace(old_value, new_value) - return self.generic_visit(node) - - def visit_ImportFrom(self, node: ast.ImportFrom): - # update module name - for old_value, new_value in self.import_replace_map.items(): - node.module = node.module.replace(old_value, new_value) - - # update imported item name - for alias in node.names: - if hasattr(alias, "name"): - for old_value, new_value in self.import_replace_map.items(): - alias.name = alias.name.replace(old_value, new_value) - - return self.generic_visit(node) - - def visit_ClassDef(self, node: ast.ClassDef): - # update class name - for old_value, new_value in self.class_replace_map.items(): - node.name = node.name.replace(old_value, new_value) - - # update parent classes names - for base in node.bases: - for old_value, new_value in self.class_replace_map.items(): - base.id = base.id.replace(old_value, new_value) - return self.generic_visit(node) - - -class FastembedAsyncAwaitTransformer(AsyncAwaitTransformer): - def _keep_sync(self, name: str) -> bool: - return name in self.keep_sync or name.startswith("_") - if __name__ == "__main__": - from .config import CODE_DIR + from tools.async_client_generator.config import CLIENT_DIR, CODE_DIR - with open(CODE_DIR / "qdrant_fastembed.py", "r") as source_file: + with open(CLIENT_DIR / "qdrant_fastembed.py", "r") as source_file: code = source_file.read() - # Parse the code into an AST - parsed_code = ast.parse(code) - - await_transformer = FastembedAsyncAwaitTransformer( + generator = FastembedGenerator( keep_sync=[ "__init__", "set_model", @@ -120,8 +66,7 @@ def _keep_sync(self, name: str) -> bool: "QdrantBase": "AsyncQdrantBase", }, ) - modified_code_ast = await_transformer.visit(parsed_code) - modified_code = ast.unparse(modified_code_ast) + modified_code = generator.generate(code) - with open("async_qdrant_fastembed.py", "w") as target_file: + with open(CODE_DIR / "async_qdrant_fastembed.py", "w") as target_file: target_file.write(modified_code) diff --git a/tools/async_client_generator/qdrant_remote_generator.py b/tools/async_client_generator/qdrant_remote_generator.py deleted file mode 100644 index dcf0016d..00000000 --- a/tools/async_client_generator/qdrant_remote_generator.py +++ /dev/null @@ -1,260 +0,0 @@ -# type: ignore - -import ast -import inspect -from typing import Optional - -from qdrant_client.grpc import CollectionsStub, PointsStub, SnapshotsStub -from qdrant_client.http import AsyncApiClient -from qdrant_client.http.api.cluster_api import AsyncClusterApi -from qdrant_client.http.api.collections_api import AsyncCollectionsApi -from qdrant_client.http.api.points_api import AsyncPointsApi -from qdrant_client.http.api.service_api import AsyncServiceApi -from qdrant_client.http.api.snapshots_api import AsyncSnapshotsApi - -from .async_client_base import AsyncQdrantBase - - -class AsyncAwaitTransformer(ast.NodeTransformer): - def __init__( - self, - keep_sync: Optional[list[str]] = None, - class_replace_map: Optional[dict] = None, - import_replace_map: Optional[dict] = None, - exclude_methods: Optional[list[str]] = None, - rename_methods: Optional[dict[str, str]] = None, - ): - self._async_methods = None - self.keep_sync = keep_sync if keep_sync is not None else [] - self.class_replace_map = class_replace_map if class_replace_map is not None else {} - self.import_replace_map = import_replace_map if import_replace_map is not None else {} - self.exclude_methods = exclude_methods if exclude_methods is not None else [] - self.rename_methods = rename_methods if rename_methods is not None else {} - - def _keep_sync(self, name: str) -> bool: - return name in self.keep_sync - - @staticmethod - def _get_grpc_methods(grpc_stub_class): - init_source = inspect.getsource(grpc_stub_class) - - # Parse the source code using ast - parsed = ast.parse(init_source) - - # Extract attribute names - field_names = [] - for node in ast.walk(parsed): - if isinstance(node, ast.Assign): - for target in node.targets: - if ( - isinstance(target, ast.Attribute) - and isinstance(target.value, ast.Name) - and target.value.id == "self" - ): - field_name = target.attr - field_names.append(field_name) - return field_names - - @property - def async_methods(self): - if self._async_methods is None: - self._async_methods = [] - self._async_methods.extend(self.get_async_methods(AsyncQdrantBase)) - self._async_methods.extend(self.get_async_methods(AsyncClusterApi)) - self._async_methods.extend(self.get_async_methods(AsyncCollectionsApi)) - self._async_methods.extend(self.get_async_methods(AsyncPointsApi)) - self._async_methods.extend(self.get_async_methods(AsyncServiceApi)) - self._async_methods.extend(self.get_async_methods(AsyncSnapshotsApi)) - self._async_methods.extend(self.get_async_methods(AsyncApiClient)) - - self._async_methods.extend(self._get_grpc_methods(PointsStub)) - self._async_methods.extend(self._get_grpc_methods(SnapshotsStub)) - self._async_methods.extend(self._get_grpc_methods(CollectionsStub)) - - return self._async_methods - - @staticmethod - def get_async_methods(class_obj): - async_methods = [] - for name, method in inspect.getmembers(class_obj): - if inspect.iscoroutinefunction(method): - async_methods.append(name) - return async_methods - - def visit_Name(self, node: ast.Name): - if node.id in self.class_replace_map: - node.id = self.class_replace_map[node.id] - elif node.id in self.import_replace_map: - node.id = self.import_replace_map[node.id] - elif node.id in self.rename_methods: - node.id = self.rename_methods[node.id] - return self.generic_visit(node) - - def visit_Call(self, node): - if isinstance(node.func, ast.Name): - if node.func.id in self.class_replace_map: - node.func.id = self.class_replace_map[node.func.id] - - if isinstance(node.func, ast.Attribute): - method_name = node.func.attr - if method_name in self.async_methods: - return ast.Await(value=node) - - return self.generic_visit(node) - - def visit_AnnAssign(self, node: ast.AnnAssign): - for old_value, new_value in self.class_replace_map.items(): - if isinstance(node.annotation, ast.Name): - node.annotation.id = node.annotation.id.replace(old_value, new_value) - return self.generic_visit(node) - - @staticmethod - def override_init(sync_node): - kick_assignments = [] - for child_node in sync_node.body: - if isinstance(child_node, ast.Assign): - for target in child_node.targets: - if isinstance(target, ast.Attribute) and hasattr(target, "attr"): - if "aio" in target.attr: - kick_assignments.append(child_node) - if isinstance(child_node, ast.AnnAssign) and hasattr(child_node.target, "attr"): - if "aio" in child_node.target.attr: - kick_assignments.append(child_node) - sync_node.body = [node for node in sync_node.body if node not in kick_assignments] - return sync_node - - @staticmethod - def override_close(): - code = """ -async def close(self, grpc_grace: Optional[float] = None, **kwargs: Any) -> None: - if hasattr(self, "_grpc_channel") and self._grpc_channel is not None: - try: - await self._grpc_channel.close(grace=grpc_grace) - except AttributeError: - logging.warning( - "Unable to close grpc_channel. Connection was interrupted on the server side" - ) - except RuntimeError: - pass - - try: - await self.http.aclose() - except Exception: - logging.warning( - "Unable to close http connection. Connection was interrupted on the server side" - ) - - self._closed = True - """ - - parsed_code = ast.parse(code) - return parsed_code.body[0] - - def visit_FunctionDef(self, sync_node: ast.FunctionDef): - if sync_node.name in self.exclude_methods: - return None - - if sync_node.name == "__init__": - sync_node = self.override_init(sync_node) - return self.generic_visit(sync_node) - - if sync_node.name == "close": - return self.override_close() - - if self._keep_sync(sync_node.name): - return self.generic_visit(sync_node) - - async_node = ast.AsyncFunctionDef( - name=sync_node.name, - args=sync_node.args, - body=sync_node.body, - decorator_list=sync_node.decorator_list, - returns=sync_node.returns, - type_comment=sync_node.type_comment, - ) - async_node.lineno = sync_node.lineno - async_node.col_offset = sync_node.col_offset - async_node.end_lineno = sync_node.end_lineno - async_node.end_col_offset = sync_node.end_col_offset - return self.generic_visit(async_node) - - def visit_Import(self, node: ast.Import): - for old_value, new_value in self.import_replace_map.items(): - for alias in node.names: - alias.name = alias.name.replace(old_value, new_value) - return self.generic_visit(node) - - def visit_ImportFrom(self, node: ast.ImportFrom): - # update module name - for old_value, new_value in self.import_replace_map.items(): - node.module = node.module.replace(old_value, new_value) - - # update imported item name - - for i, alias in enumerate(node.names): - if hasattr(alias, "name"): - for old_value, new_value in self.import_replace_map.items(): - alias.name = alias.name.replace(old_value, new_value) - if alias.name == "get_async_channel": - alias.asname = "get_channel" - node.names = [alias for alias in node.names if alias.name != "get_channel"] - - return self.generic_visit(node) - - def visit_ClassDef(self, node: ast.ClassDef): - # update class name - for old_value, new_value in self.class_replace_map.items(): - node.name = node.name.replace(old_value, new_value) - - # update parent classes names - for base in node.bases: - for old_value, new_value in self.class_replace_map.items(): - base.id = base.id.replace(old_value, new_value) - return self.generic_visit(node) - - -class RemoteAsyncAwaitTransformer(AsyncAwaitTransformer): - def _keep_sync(self, name: str) -> bool: - return name in self.keep_sync or name not in self.async_methods - - -if __name__ == "__main__": - from .config import CODE_DIR - - with open(CODE_DIR / "qdrant_remote.py", "r") as source_file: - code = source_file.read() - - # Parse the code into an AST - parsed_code = ast.parse(code) - - await_transformer = RemoteAsyncAwaitTransformer( - class_replace_map={ - "QdrantBase": "AsyncQdrantBase", - "QdrantFastembedMixin": "AsyncQdrantFastembedMixin", - "QdrantClient": "AsyncQdrantClient", - "QdrantRemote": "AsyncQdrantRemote", - }, - import_replace_map={ - "qdrant_client.client_base": "qdrant_client.async_client_base", - "QdrantBase": "AsyncQdrantBase", - "QdrantRemote": "AsyncQdrantRemote", - "ApiClient": "AsyncApiClient", - "SyncApis": "AsyncApis", - }, - exclude_methods=[ - "__del__", - "migrate", - "async_grpc_collections", - "async_grpc_points", - "async_grpc_snapshots", - "_init_async_grpc_points_client", - "_init_async_grpc_collections_client", - "_init_async_grpc_snapshots_client", - "_init_async_grpc_channel", - ], - ) - modified_code_ast = await_transformer.visit(parsed_code) - modified_code = ast.unparse(modified_code_ast) - - with open("async_qdrant_remote.py", "w") as target_file: - target_file.write(modified_code) diff --git a/tools/async_client_generator/remote_generator.py b/tools/async_client_generator/remote_generator.py new file mode 100644 index 00000000..2a1875fd --- /dev/null +++ b/tools/async_client_generator/remote_generator.py @@ -0,0 +1,138 @@ +# type: ignore +import ast +import inspect +from typing import Dict, List, Optional + +from qdrant_client.grpc import CollectionsStub, PointsStub, SnapshotsStub +from qdrant_client.http import AsyncApiClient +from qdrant_client.http.api.cluster_api import AsyncClusterApi +from qdrant_client.http.api.collections_api import AsyncCollectionsApi +from qdrant_client.http.api.points_api import AsyncPointsApi +from qdrant_client.http.api.service_api import AsyncServiceApi +from qdrant_client.http.api.snapshots_api import AsyncSnapshotsApi +from tools.async_client_generator.async_client_base import AsyncQdrantBase +from tools.async_client_generator.base_generator import BaseGenerator +from tools.async_client_generator.transformers import ( + AnnAssignTransformer, + CallTransformer, + ClassDefTransformer, + ImportTransformer, + NameTransformer, +) +from tools.async_client_generator.transformers.remote import ( + RemoteFunctionDefTransformer, + RemoteImportFromTransformer, +) + + +class RemoteGenerator(BaseGenerator): + def __init__( + self, + keep_sync: Optional[List[str]] = None, + class_replace_map: Optional[dict] = None, + import_replace_map: Optional[dict] = None, + exclude_methods: Optional[List[str]] = None, + rename_methods: Optional[Dict[str, str]] = None, + ): + super().__init__() + self._async_methods = None + + self.transformers.append(RemoteImportFromTransformer(import_replace_map)) + self.transformers.append(ClassDefTransformer(class_replace_map)) + self.transformers.append(CallTransformer(class_replace_map, self.async_methods)) + self.transformers.append(AnnAssignTransformer(class_replace_map)) + self.transformers.append(ImportTransformer(import_replace_map)) + self.transformers.append( + RemoteFunctionDefTransformer(keep_sync, exclude_methods, self.async_methods) + ) + self.transformers.append( + NameTransformer(class_replace_map, import_replace_map, rename_methods) + ) + + @staticmethod + def _get_grpc_methods(grpc_stub_class) -> List[str]: + init_source = inspect.getsource(grpc_stub_class) + + # Parse the source code using ast + parsed = ast.parse(init_source) + + # Extract attribute names + field_names = [] + for node in ast.walk(parsed): + if isinstance(node, ast.Assign): + for target in node.targets: + if ( + isinstance(target, ast.Attribute) + and isinstance(target.value, ast.Name) + and target.value.id == "self" + ): + field_name = target.attr + field_names.append(field_name) + return field_names + + @property + def async_methods(self) -> List[str]: + if self._async_methods is None: + self._async_methods = [] + for cls_ in ( + AsyncQdrantBase, + AsyncClusterApi, + AsyncCollectionsApi, + AsyncPointsApi, + AsyncServiceApi, + AsyncSnapshotsApi, + AsyncApiClient, + ): + self._async_methods.extend(self.get_async_methods(cls_)) + + for cls_ in (PointsStub, SnapshotsStub, CollectionsStub): + self._async_methods.extend(self._get_grpc_methods(cls_)) + + return self._async_methods + + @staticmethod + def get_async_methods(class_obj) -> List[str]: + async_methods = [] + for name, method in inspect.getmembers(class_obj): + if inspect.iscoroutinefunction(method): + async_methods.append(name) + return async_methods + + +if __name__ == "__main__": + from tools.async_client_generator.config import CLIENT_DIR, CODE_DIR + + with open(CLIENT_DIR / "qdrant_remote.py", "r") as source_file: + code = source_file.read() + + generator = RemoteGenerator( + class_replace_map={ + "QdrantBase": "AsyncQdrantBase", + "QdrantFastembedMixin": "AsyncQdrantFastembedMixin", + "QdrantClient": "AsyncQdrantClient", + "QdrantRemote": "AsyncQdrantRemote", + }, + import_replace_map={ + "qdrant_client.client_base": "qdrant_client.async_client_base", + "QdrantBase": "AsyncQdrantBase", + "QdrantRemote": "AsyncQdrantRemote", + "ApiClient": "AsyncApiClient", + "SyncApis": "AsyncApis", + }, + exclude_methods=[ + "__del__", + "migrate", + "async_grpc_collections", + "async_grpc_points", + "async_grpc_snapshots", + "_init_async_grpc_points_client", + "_init_async_grpc_collections_client", + "_init_async_grpc_snapshots_client", + "_init_async_grpc_channel", + ], + ) + + modified_code = generator.generate(code) + + with open(CODE_DIR / "async_qdrant_remote.py", "w") as target_file: + target_file.write(modified_code) diff --git a/tools/async_client_generator/transformers/__init__.py b/tools/async_client_generator/transformers/__init__.py new file mode 100644 index 00000000..9f9a5c0a --- /dev/null +++ b/tools/async_client_generator/transformers/__init__.py @@ -0,0 +1,8 @@ +from .ann_assign_transformer import AnnAssignTransformer +from .call_transformer import CallTransformer +from .class_def_transformer import ClassDefTransformer +from .constant_transformer import ConstantTransformer +from .function_def_transformer import FunctionDefTransformer +from .import_from_transformer import ImportFromTransformer +from .import_transformer import ImportTransformer +from .name_transformer import NameTransformer diff --git a/tools/async_client_generator/transformers/ann_assign_transformer.py b/tools/async_client_generator/transformers/ann_assign_transformer.py new file mode 100644 index 00000000..89516710 --- /dev/null +++ b/tools/async_client_generator/transformers/ann_assign_transformer.py @@ -0,0 +1,13 @@ +import ast +from typing import Dict, Optional + + +class AnnAssignTransformer(ast.NodeTransformer): + def __init__(self, class_replace_map: Optional[Dict[str, str]] = None): + self.class_replace_map = class_replace_map if class_replace_map is not None else {} + + def visit_AnnAssign(self, node: ast.AnnAssign) -> ast.AST: + for old_value, new_value in self.class_replace_map.items(): + if isinstance(node.annotation, ast.Name): + node.annotation.id = node.annotation.id.replace(old_value, new_value) + return self.generic_visit(node) diff --git a/tools/async_client_generator/transformers/call_transformer.py b/tools/async_client_generator/transformers/call_transformer.py new file mode 100644 index 00000000..5dea0e1c --- /dev/null +++ b/tools/async_client_generator/transformers/call_transformer.py @@ -0,0 +1,23 @@ +import ast +from typing import Dict, List, Optional, Union + + +class CallTransformer(ast.NodeTransformer): + def __init__( + self, + class_replace_map: Optional[Dict[str, str]] = None, + async_methods: Optional[List[str]] = None, + ): + self.class_replace_map = class_replace_map if class_replace_map is not None else {} + self.async_methods = async_methods if async_methods is not None else [] + + def visit_Call(self, node: ast.Call) -> Union[ast.AST, ast.Await]: + if isinstance(node.func, ast.Name): + if node.func.id in self.class_replace_map: + node.func.id = self.class_replace_map[node.func.id] + + if isinstance(node.func, ast.Attribute): + if node.func.attr in self.async_methods: + return ast.Await(value=node) + + return self.generic_visit(node) diff --git a/tools/async_client_generator/transformers/class_def_transformer.py b/tools/async_client_generator/transformers/class_def_transformer.py new file mode 100644 index 00000000..f2f5f260 --- /dev/null +++ b/tools/async_client_generator/transformers/class_def_transformer.py @@ -0,0 +1,18 @@ +import ast +from typing import Dict, Optional + + +class ClassDefTransformer(ast.NodeTransformer): + def __init__(self, class_replace_map: Optional[Dict[str, str]]): + self.class_replace_map = class_replace_map if class_replace_map is not None else {} + + def visit_ClassDef(self, node: ast.ClassDef) -> ast.AST: + # update class name + for old_value, new_value in self.class_replace_map.items(): + node.name = node.name.replace(old_value, new_value) + + # update parent classes names + for base in node.bases: + for old_value, new_value in self.class_replace_map.items(): + base.id = base.id.replace(old_value, new_value) + return self.generic_visit(node) diff --git a/tools/async_client_generator/transformers/client/__init__.py b/tools/async_client_generator/transformers/client/__init__.py new file mode 100644 index 00000000..ad287762 --- /dev/null +++ b/tools/async_client_generator/transformers/client/__init__.py @@ -0,0 +1 @@ +from .function_def_transformer import ClientFunctionDefTransformer diff --git a/tools/async_client_generator/transformers/client/function_def_transformer.py b/tools/async_client_generator/transformers/client/function_def_transformer.py new file mode 100644 index 00000000..308836ab --- /dev/null +++ b/tools/async_client_generator/transformers/client/function_def_transformer.py @@ -0,0 +1,82 @@ +import ast +from typing import Dict, List, Optional + +from ...transformers import FunctionDefTransformer + + +class ClientFunctionDefTransformer(FunctionDefTransformer): + def __init__( + self, + keep_sync: Optional[List[str]] = None, + class_replace_map: Optional[Dict[str, str]] = None, + exclude_methods: Optional[List[str]] = None, + rename_methods: Optional[Dict[str, str]] = None, + async_methods: Optional[List[str]] = None, + ): + super().__init__(keep_sync) + self.class_replace_map = class_replace_map if class_replace_map is not None else {} + self.exclude_methods = exclude_methods if exclude_methods is not None else [] + self.rename_methods = rename_methods if rename_methods is not None else {} + self.async_methods = async_methods if async_methods is not None else [] + + def _keep_sync(self, name: str) -> bool: + return name in self.keep_sync or name not in self.async_methods + + def visit_FunctionDef(self, sync_node: ast.FunctionDef) -> Optional[ast.AST]: + if sync_node.name in self.exclude_methods: + return None + + if sync_node.name == "__init__": + return self.generate_init(sync_node) + + return super().visit_FunctionDef(sync_node) + + def generate_init(self, sync_node: ast.FunctionDef) -> ast.AST: + def traverse(node): + assignment_nodes = [] + + if isinstance(node, ast.Assign): + assignment_nodes.append(node) + for field_name, field_value in ast.iter_fields(node): + if isinstance(field_value, ast.AST): + assignment_nodes.extend(traverse(field_value)) + elif isinstance(field_value, list): + for item in field_value: + if isinstance(item, ast.AST): + assignment_nodes.extend(traverse(item)) + return assignment_nodes + + def unwrap_orelse_assignment(assign_node: ast.Assign) -> Optional[ast.Assign]: + for target in assign_node.targets: + if isinstance(target, ast.Attribute) and target.attr == "_client": + if isinstance(assign_node.value, ast.Call): + if assign_node.value.func.id in self.class_replace_map: + assign_node.value.func.id = self.class_replace_map[ + assign_node.value.func.id + ] + return assign_node + + args, defaults = [sync_node.args.args[0]], [] + for arg, default in zip(sync_node.args.args[1:], sync_node.args.defaults): + if arg.arg not in ("location", "path"): + args.append(arg) + defaults.append(default) + sync_node.args.args = args + sync_node.args.defaults = defaults + + for i, child_node in enumerate(sync_node.body): + if isinstance(child_node, ast.If): + orelse_assignment_nodes = traverse(child_node) + assignments = list( + filter( + lambda x: x, + [ + unwrap_orelse_assignment(assign_node) + for assign_node in orelse_assignment_nodes + ], + ) + ) + if len(assignments) == 1: + sync_node.body[i] = assignments[0] + break + return self.generic_visit(sync_node) diff --git a/tools/async_client_generator/transformers/constant_transformer.py b/tools/async_client_generator/transformers/constant_transformer.py new file mode 100644 index 00000000..194e88ae --- /dev/null +++ b/tools/async_client_generator/transformers/constant_transformer.py @@ -0,0 +1,15 @@ +import ast +from typing import Dict, Optional + + +class ConstantTransformer(ast.NodeTransformer): + def __init__(self, constant_replace_map: Optional[Dict[str, str]]): + self.constant_replace_map = ( + constant_replace_map if constant_replace_map is not None else {} + ) + + def visit_Constant(self, node: ast.arg) -> ast.AST: + for old_value, new_value in self.constant_replace_map.items(): + if isinstance(node.value, str): + node.value = node.value.replace(old_value, new_value) + return self.generic_visit(node) diff --git a/tools/async_client_generator/transformers/fastembed/__init__.py b/tools/async_client_generator/transformers/fastembed/__init__.py new file mode 100644 index 00000000..3ec29974 --- /dev/null +++ b/tools/async_client_generator/transformers/fastembed/__init__.py @@ -0,0 +1,2 @@ +from .call_transformer import FastembedCallTransformer +from .function_def_transformer import FastembedFunctionDefTransformer diff --git a/tools/async_client_generator/transformers/fastembed/call_transformer.py b/tools/async_client_generator/transformers/fastembed/call_transformer.py new file mode 100644 index 00000000..11a4f199 --- /dev/null +++ b/tools/async_client_generator/transformers/fastembed/call_transformer.py @@ -0,0 +1,14 @@ +import ast +from typing import List, Optional, Union + + +class FastembedCallTransformer(ast.NodeTransformer): + def __init__(self, async_methods: Optional[List[str]] = None): + self.async_methods = async_methods if async_methods is not None else [] + + def visit_Call(self, node: ast.Call) -> Union[ast.AST, ast.Await]: + if isinstance(node.func, ast.Attribute): + if isinstance(node.func.value, ast.Name) and node.func.value.id == "self": + if node.func.attr in self.async_methods: + return ast.Await(value=node) + return self.generic_visit(node) diff --git a/tools/async_client_generator/transformers/fastembed/function_def_transformer.py b/tools/async_client_generator/transformers/fastembed/function_def_transformer.py new file mode 100644 index 00000000..a2a74b31 --- /dev/null +++ b/tools/async_client_generator/transformers/fastembed/function_def_transformer.py @@ -0,0 +1,6 @@ +from ...transformers import FunctionDefTransformer + + +class FastembedFunctionDefTransformer(FunctionDefTransformer): + def _keep_sync(self, name: str) -> bool: + return name in self.keep_sync or name.startswith("_") diff --git a/tools/async_client_generator/transformers/function_def_transformer.py b/tools/async_client_generator/transformers/function_def_transformer.py new file mode 100644 index 00000000..dd2d9b9e --- /dev/null +++ b/tools/async_client_generator/transformers/function_def_transformer.py @@ -0,0 +1,28 @@ +import ast +from typing import List, Optional + + +class FunctionDefTransformer(ast.NodeTransformer): + def __init__(self, keep_sync: Optional[List[str]] = None): + self.keep_sync = keep_sync if keep_sync is not None else [] + + def _keep_sync(self, name: str) -> bool: + return name in self.keep_sync + + def visit_FunctionDef(self, sync_node: ast.FunctionDef) -> ast.AST: + if self._keep_sync(sync_node.name): + return self.generic_visit(sync_node) + + async_node = ast.AsyncFunctionDef( + name=sync_node.name, + args=sync_node.args, + body=sync_node.body, + decorator_list=sync_node.decorator_list, + returns=sync_node.returns, + type_comment=sync_node.type_comment, + ) + async_node.lineno = sync_node.lineno + async_node.col_offset = sync_node.col_offset + async_node.end_lineno = sync_node.end_lineno + async_node.end_col_offset = sync_node.end_col_offset + return self.generic_visit(async_node) diff --git a/tools/async_client_generator/transformers/import_from_transformer.py b/tools/async_client_generator/transformers/import_from_transformer.py new file mode 100644 index 00000000..0954f563 --- /dev/null +++ b/tools/async_client_generator/transformers/import_from_transformer.py @@ -0,0 +1,20 @@ +import ast +from typing import Dict, Optional + + +class ImportFromTransformer(ast.NodeTransformer): + def __init__(self, import_replace_map: Optional[Dict[str, str]] = None): + self.import_replace_map = import_replace_map if import_replace_map is not None else {} + + def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.AST: + # update module name + for old_value, new_value in self.import_replace_map.items(): + node.module = node.module.replace(old_value, new_value) + + # update imported item name + for alias in node.names: + if hasattr(alias, "name"): + for old_value, new_value in self.import_replace_map.items(): + alias.name = alias.name.replace(old_value, new_value) + + return self.generic_visit(node) diff --git a/tools/async_client_generator/transformers/import_transformer.py b/tools/async_client_generator/transformers/import_transformer.py new file mode 100644 index 00000000..6ef1526b --- /dev/null +++ b/tools/async_client_generator/transformers/import_transformer.py @@ -0,0 +1,13 @@ +import ast +from typing import Dict, Optional + + +class ImportTransformer(ast.NodeTransformer): + def __init__(self, import_replace_map: Optional[Dict[str, str]] = None): + self.import_replace_map = import_replace_map if import_replace_map is not None else {} + + def visit_Import(self, node: ast.Import) -> ast.AST: + for old_value, new_value in self.import_replace_map.items(): + for alias in node.names: + alias.name = alias.name.replace(old_value, new_value) + return self.generic_visit(node) diff --git a/tools/async_client_generator/transformers/name_transformer.py b/tools/async_client_generator/transformers/name_transformer.py new file mode 100644 index 00000000..2d1e4a8e --- /dev/null +++ b/tools/async_client_generator/transformers/name_transformer.py @@ -0,0 +1,23 @@ +import ast +from typing import Dict, Optional + + +class NameTransformer(ast.NodeTransformer): + def __init__( + self, + class_replace_map: Optional[Dict[str, str]] = None, + import_replace_map: Optional[Dict[str, str]] = None, + rename_methods: Optional[Dict[str, str]] = None, + ): + self.class_replace_map = class_replace_map if class_replace_map is not None else {} + self.import_replace_map = import_replace_map if import_replace_map is not None else {} + self.rename_methods = rename_methods if rename_methods is not None else {} + + def visit_Name(self, node: ast.Name) -> ast.AST: + if node.id in self.class_replace_map: + node.id = self.class_replace_map[node.id] + elif node.id in self.import_replace_map: + node.id = self.import_replace_map[node.id] + elif node.id in self.rename_methods: + node.id = self.rename_methods[node.id] + return self.generic_visit(node) diff --git a/tools/async_client_generator/transformers/remote/__init__.py b/tools/async_client_generator/transformers/remote/__init__.py new file mode 100644 index 00000000..dec7ea74 --- /dev/null +++ b/tools/async_client_generator/transformers/remote/__init__.py @@ -0,0 +1,2 @@ +from .function_def_transformer import RemoteFunctionDefTransformer +from .import_from_transformer import RemoteImportFromTransformer diff --git a/tools/async_client_generator/transformers/remote/function_def_transformer.py b/tools/async_client_generator/transformers/remote/function_def_transformer.py new file mode 100644 index 00000000..a6a2a966 --- /dev/null +++ b/tools/async_client_generator/transformers/remote/function_def_transformer.py @@ -0,0 +1,74 @@ +import ast +from typing import List, Optional + +from ...transformers import FunctionDefTransformer + + +class RemoteFunctionDefTransformer(FunctionDefTransformer): + def __init__( + self, + keep_sync: Optional[List[str]] = None, + exclude_methods: Optional[List[str]] = None, + async_methods: Optional[List[str]] = None, + ): + super().__init__(keep_sync=keep_sync) + self.exclude_methods = exclude_methods if exclude_methods is not None else [] + self.async_methods = async_methods + + def _keep_sync(self, name: str) -> bool: + return name in self.keep_sync or name not in self.async_methods + + @staticmethod + def override_init(sync_node) -> ast.AST: + kick_assignments = [] + for child_node in sync_node.body: + if isinstance(child_node, ast.Assign): + for target in child_node.targets: + if isinstance(target, ast.Attribute) and hasattr(target, "attr"): + if "aio" in target.attr: + kick_assignments.append(child_node) + if isinstance(child_node, ast.AnnAssign) and hasattr(child_node.target, "attr"): + if "aio" in child_node.target.attr: + kick_assignments.append(child_node) + sync_node.body = [node for node in sync_node.body if node not in kick_assignments] + return sync_node + + @staticmethod + def override_close() -> ast.stmt: + code = """ +async def close(self, grpc_grace: Optional[float] = None, **kwargs: Any) -> None: + if hasattr(self, "_grpc_channel") and self._grpc_channel is not None: + try: + await self._grpc_channel.close(grace=grpc_grace) + except AttributeError: + logging.warning( + "Unable to close grpc_channel. Connection was interrupted on the server side" + ) + except RuntimeError: + pass + + try: + await self.http.aclose() + except Exception: + logging.warning( + "Unable to close http connection. Connection was interrupted on the server side" + ) + + self._closed = True + """ + + parsed_code = ast.parse(code) + return parsed_code.body[0] + + def visit_FunctionDef(self, sync_node: ast.FunctionDef) -> Optional[ast.AST]: + if sync_node.name in self.exclude_methods: + return None + + if sync_node.name == "__init__": + sync_node = self.override_init(sync_node) + return self.generic_visit(sync_node) + + if sync_node.name == "close": + return self.override_close() + + return super().visit_FunctionDef(sync_node) diff --git a/tools/async_client_generator/transformers/remote/import_from_transformer.py b/tools/async_client_generator/transformers/remote/import_from_transformer.py new file mode 100644 index 00000000..930d401f --- /dev/null +++ b/tools/async_client_generator/transformers/remote/import_from_transformer.py @@ -0,0 +1,24 @@ +import ast +from typing import Dict, Optional + + +class RemoteImportFromTransformer(ast.NodeTransformer): + def __init__(self, import_replace_map: Optional[Dict[str, str]] = None): + self.import_replace_map = import_replace_map if import_replace_map is not None else {} + + def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.AST: + # update module name + for old_value, new_value in self.import_replace_map.items(): + node.module = node.module.replace(old_value, new_value) + + # update imported item name + + for i, alias in enumerate(node.names): + if hasattr(alias, "name"): + for old_value, new_value in self.import_replace_map.items(): + alias.name = alias.name.replace(old_value, new_value) + if alias.name == "get_async_channel": + alias.asname = "get_channel" + node.names = [alias for alias in node.names if alias.name != "get_channel"] + + return self.generic_visit(node) diff --git a/tools/generate_async_client.sh b/tools/generate_async_client.sh index 13ec8a68..af959a9f 100755 --- a/tools/generate_async_client.sh +++ b/tools/generate_async_client.sh @@ -11,7 +11,7 @@ cd $ABSOLUTE_PROJECT_ROOT/tools/async_client_generator python3 -m tools.async_client_generator.base_client_generator python3 -m tools.async_client_generator.fastembed_generator python3 -m tools.async_client_generator.client_generator -python3 -m tools.async_client_generator.qdrant_remote_generator +python3 -m tools.async_client_generator.remote_generator ls -1 | autoflake --recursive --imports qdrant_client --remove-unused-variables --in-place async*.py ls -1 | grep async | xargs -I {} isort -w 99 --multi-line 3 --trailing-comma --force-grid-wrap 0 --combine-as {}