diff --git a/coordinator/gscoordinator/coordinator.py b/coordinator/gscoordinator/coordinator.py index 9a3bef8658fb..f53ad433ba22 100644 --- a/coordinator/gscoordinator/coordinator.py +++ b/coordinator/gscoordinator/coordinator.py @@ -62,7 +62,9 @@ from gscoordinator.utils import distribute_lib_on_k8s from gscoordinator.utils import distribute_lib_via_hosts from gscoordinator.utils import dump_string -from gscoordinator.utils import generate_graph_type_sig +from gscoordinator.utils import get_app_sha256 +from gscoordinator.utils import get_graph_sha256 +from gscoordinator.utils import get_lib_path from gscoordinator.utils import str2bool from gscoordinator.utils import to_maxgraph_schema from gscoordinator.version import __version__ @@ -331,11 +333,15 @@ def RunStep(self, request, context): # noqa: C901 return response def _maybe_compile_app(self, op): - app_sig = self._generate_app_sig(op.attr) - if app_sig in self._object_manager: - app_lib_path = self._object_manager.get(app_sig).lib_path - else: - app_lib_path = self._compile_lib_and_distribute(compile_app, app_sig, op) + app_sig = get_app_sha256(op.attr) + space = self._builtin_workspace + if types_pb2.GAR in op.attr: + space = self._udf_app_workspace + app_lib_path = get_lib_path(os.path.join(space, app_sig), app_sig) + if not os.path.isfile(app_lib_path): + compiled_path = self._compile_lib_and_distribute(compile_app, app_sig, op) + if app_lib_path != compiled_path: + raise RuntimeError("Computed path not equal to compiled path.") op.attr[types_pb2.APP_LIBRARY_PATH].CopyFrom( attr_value_pb2.AttrValue(s=app_lib_path.encode("utf-8")) @@ -343,15 +349,16 @@ def _maybe_compile_app(self, op): return op, app_sig, app_lib_path def _maybe_register_graph(self, op, session_id): - graph_sig = self._generate_graph_sig(op.attr) - if graph_sig in self._object_manager: - lib_meta = self._object_manager.get(graph_sig) - graph_lib_path = lib_meta.lib_path - else: - graph_lib_path = self._compile_lib_and_distribute( + graph_sig = get_graph_sha256(op.attr) + space = self._builtin_workspace + graph_lib_path = get_lib_path(os.path.join(space, graph_sig), graph_sig) + if not os.path.isfile(graph_lib_path): + compiled_path = self._compile_lib_and_distribute( compile_graph_frame, graph_sig, op ) - + if graph_lib_path != compiled_path: + raise RuntimeError("Computed path not equal to compiled path.") + if graph_sig not in self._object_manager: # register graph op_def = op_def_pb2.OpDef(op=types_pb2.REGISTER_GRAPH_TYPE) op_def.attr[types_pb2.GRAPH_LIBRARY_PATH].CopyFrom( @@ -610,15 +617,6 @@ def _create_grpc_stub(self): ) return engine_service_pb2_grpc.EngineServiceStub(channel) - def _generate_app_sig(self, attr): - return hashlib.sha256( - attr[types_pb2.APP_SIGNATURE].s + attr[types_pb2.GRAPH_SIGNATURE].s - ).hexdigest() - - def _generate_graph_sig(self, attr: dict): - graph_signature = generate_graph_type_sig(attr) - return hashlib.sha256(graph_signature.encode("utf-8")).hexdigest() - def _get_engine_config(self): op_def = op_def_pb2.OpDef(op=types_pb2.GET_ENGINE_CONFIG) dag_def = op_def_pb2.DagDef() diff --git a/coordinator/gscoordinator/utils.py b/coordinator/gscoordinator/utils.py index a5f202016522..73f37d757444 100644 --- a/coordinator/gscoordinator/utils.py +++ b/coordinator/gscoordinator/utils.py @@ -19,6 +19,7 @@ import copy import glob +import hashlib import json import logging import numbers @@ -89,28 +90,53 @@ def get_lib_path(app_dir, app_name): lib_path = os.path.join(app_dir, "lib%s.dylib" % app_name) else: raise RuntimeError("Unsupported platform.") - assert os.path.isfile(lib_path), "Error occurs when building the frame library." return lib_path -def compile_app(workspace: str, app_name: str, attr, engine_config: dict): +def get_app_sha256(attr): + ( + app_type, + app_header, + app_class, + vd_type, + md_type, + pregel_combine, + ) = _codegen_app_info(attr, DEFAULT_GS_CONFIG_FILE) + graph_header, graph_type = _codegen_graph_info(attr) + logger.info("Codegened graph type: %s, Graph header: %s", graph_type, graph_header) + if app_type == "cpp_pie": + return hashlib.sha256( + f"{app_type}.{app_class}.{graph_type}".encode("utf-8") + ).hexdigest() + else: + s = hashlib.sha256() + s.update(f"{app_type}.{app_class}.{graph_type}".encode("utf-8")) + if types_pb2.GAR in attr: + s.update(attr[types_pb2.GAR].s) + return s.hexdigest() + + +def get_graph_sha256(attr): + _, graph_class = _codegen_graph_info(attr) + return hashlib.sha256(graph_class.encode("utf-8")).hexdigest() + + +def compile_app(workspace: str, library_name, attr, engine_config: dict): """Compile an application. Args: workspace (str): working dir. - app_name (str): target app_name. + library_name (str): name of library attr (`AttrValue`): All information needed to compile an app. + engine_config (dict): for options of experimental_on Returns: str: Path of the built library. """ - - app_dir = os.path.join(workspace, app_name) + app_dir = os.path.join(workspace, library_name) os.makedirs(app_dir, exist_ok=True) - # extract gar content _extract_gar(app_dir, attr) - # codegen app and graph info # vd_type and md_type is None in cpp_pie ( @@ -120,7 +146,7 @@ def compile_app(workspace: str, app_name: str, attr, engine_config: dict): vd_type, md_type, pregel_combine, - ) = _codegen_app_info(app_dir, DEFAULT_GS_CONFIG_FILE, attr) + ) = _codegen_app_info(attr, DEFAULT_GS_CONFIG_FILE) logger.info( "Codegened application type: %s, app header: %s, app_class: %s, vd_type: %s, md_type: %s, pregel_combine: %s", app_type, @@ -171,7 +197,7 @@ def compile_app(workspace: str, app_name: str, attr, engine_config: dict): content = template.read() content = Template(content).safe_substitute( _analytical_engine_home=ANALYTICAL_ENGINE_HOME, - _frame_name=app_name, + _frame_name=library_name, _vd_type=vd_type, _md_type=md_type, _graph_type=graph_type, @@ -208,45 +234,19 @@ def compile_app(workspace: str, app_name: str, attr, engine_config: dict): make_stderr_watcher = PipeWatcher(make_process.stderr, sys.stdout) setattr(make_process, "stderr_watcher", make_stderr_watcher) make_process.wait() - - return get_lib_path(app_dir, app_name) - - -def generate_graph_type_sig(attr: dict): - graph_type = attr[types_pb2.GRAPH_TYPE].graph_type - - if graph_type == types_pb2.ARROW_PROPERTY: - oid_type = attr[types_pb2.OID_TYPE].s.decode("utf-8") - vid_type = attr[types_pb2.VID_TYPE].s.decode("utf-8") - graph_signature = "vineyard::ArrowFragment<{},{}>".format(oid_type, vid_type) - elif graph_type == types_pb2.ARROW_PROJECTED: - oid_type = attr[types_pb2.OID_TYPE].s.decode("utf-8") - vid_type = attr[types_pb2.VID_TYPE].s.decode("utf-8") - vdata_type = attr[types_pb2.V_DATA_TYPE].s.decode("utf-8") - edata_type = attr[types_pb2.E_DATA_TYPE].s.decode("utf-8") - graph_signature = "gs::ArrowProjectedFragment<{},{},{},{}>".format( - oid_type, vid_type, vdata_type, edata_type - ) - elif graph_type == types_pb2.DYNAMIC_PROJECTED: - vdata_type = attr[types_pb2.V_DATA_TYPE].s.decode("utf-8") - edata_type = attr[types_pb2.E_DATA_TYPE].s.decode("utf-8") - graph_signature = "gs::DynamicProjectedFragment<{},{}>".format( - vdata_type, edata_type - ) - else: - raise ValueError("Unsupported graph type: {}".format(graph_type)) - return graph_signature + lib_path = get_lib_path(app_dir, library_name) + assert os.path.isfile(lib_path), "Error occurs when building the frame library." + return lib_path -def compile_graph_frame( - workspace: str, frame_name: str, attr: dict, engine_config: dict -): +def compile_graph_frame(workspace: str, library_name, attr: dict, engine_config: dict): """Compile an application. Args: workspace (str): Working dir. - frame_name (str): Target app_name. + library_name (str): name of library attr (`AttrValue`): All information needed to compile a graph library. + engine_config (dict): for options of experimental_on Raises: ValueError: When graph_type is not supported. @@ -255,14 +255,14 @@ def compile_graph_frame( str: Path of the built graph library. """ - frame_dir = os.path.join(workspace, frame_name) - os.makedirs(frame_dir, exist_ok=True) + _, graph_class = _codegen_graph_info(attr) - graph_signature = generate_graph_type_sig(attr) + logger.info("Codegened graph frame type: %s", graph_class) - logger.info("Codegened graph frame type: %s", graph_signature) + library_dir = os.path.join(workspace, library_name) + os.makedirs(library_dir, exist_ok=True) - os.chdir(frame_dir) + os.chdir(library_dir) graph_type = attr[types_pb2.GRAPH_TYPE].graph_type @@ -282,13 +282,13 @@ def compile_graph_frame( raise ValueError("Illegal graph type: {}".format(graph_type)) # replace and generate cmakelist cmakelists_file_tmp = os.path.join(TEMPLATE_DIR, "CMakeLists.template") - cmakelists_file = os.path.join(frame_dir, "CMakeLists.txt") + cmakelists_file = os.path.join(library_dir, "CMakeLists.txt") with open(cmakelists_file_tmp, mode="r") as template: content = template.read() content = Template(content).safe_substitute( _analytical_engine_home=ANALYTICAL_ENGINE_HOME, - _frame_name=frame_name, - _graph_type=graph_signature, + _frame_name=library_name, + _graph_type=graph_class, ) with open(cmakelists_file, mode="w") as f: f.write(content) @@ -318,13 +318,13 @@ def compile_graph_frame( make_stderr_watcher = PipeWatcher(make_process.stderr, sys.stdout) setattr(make_process, "stderr_watcher", make_stderr_watcher) make_process.wait() - - return get_lib_path(frame_dir, frame_name) + lib_path = get_lib_path(library_dir, library_name) + assert os.path.isfile(lib_path), "Error occurs when building the frame library." + return lib_path -def _extract_gar(workspace: str, attr): +def _extract_gar(app_dir: str, attr): """Extract gar to workspace - Args: workspace (str): Working directory attr (`AttrValue`): Optionally it can contains the bytes of gar. @@ -334,10 +334,10 @@ def _extract_gar(workspace: str, attr): # if gar sent via bytecode in attr, overwrite. fp = BytesIO(attr[types_pb2.GAR].s) with zipfile.ZipFile(fp, "r") as zip_ref: - zip_ref.extractall(workspace) + zip_ref.extractall(app_dir) -def _codegen_app_info(workspace: str, meta_file: str, attr): +def _codegen_app_info(attr, meta_file: str): """Codegen application by instanize the template specialization. Args: @@ -352,11 +352,15 @@ def _codegen_app_info(workspace: str, meta_file: str, attr): type: app_type app class: for fulfilling the CMakelists. """ - algo = attr[types_pb2.APP_ALGO].s.decode("utf-8") - - with open(os.path.join(workspace, meta_file), "r") as f: - config_yaml = yaml.safe_load(f) + fp = BUILTIN_APP_RESOURCE_PATH # default is builtin app resources. + if types_pb2.GAR in attr: + # if gar sent via bytecode in attr, overwrite. + fp = BytesIO(attr[types_pb2.GAR].s) + with zipfile.ZipFile(fp, "r") as zip_ref: + with zip_ref.open(meta_file, "r") as f: + config_yaml = yaml.safe_load(f) + algo = attr[types_pb2.APP_ALGO].s.decode("utf-8") for app in config_yaml["app"]: if app["algo"] == algo: app_type = app["type"] # cpp_pie or cython_pregel or cython_pie diff --git a/k8s/graphscope.Dockerfile b/k8s/graphscope.Dockerfile index 87cce4ecd43f..f678cebb5077 100644 --- a/k8s/graphscope.Dockerfile +++ b/k8s/graphscope.Dockerfile @@ -101,6 +101,8 @@ ARG profile=release COPY --from=builder /opt/graphscope /usr/local/ RUN cd /usr/local/dist/ && pip3 install ./*.whl +COPY --from=builder /root/gs/k8s/precompile.py /tmp/precompile.py +RUN python3 /tmp/precompile.py && rm /tmp/precompile.py RUN mkdir -p /home/maxgraph ENV VINEYARD_IPC_SOCKET /home/maxgraph/data/vineyard/vineyard.sock diff --git a/k8s/precompile.py b/k8s/precompile.py new file mode 100755 index 000000000000..28de0dd96e2c --- /dev/null +++ b/k8s/precompile.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 + +import glob +import hashlib +import multiprocessing +import os +import shutil +import subprocess +import zipfile +from pathlib import Path +from string import Template + +import yaml + + +def compute_sig(s): + return hashlib.sha256(s.encode("utf-8")).hexdigest() + + +EXPERIMENTAL_ON = os.environ.get("EXPERIMENTAL_ON", "ON") +try: + import gscoordinator + + COORDINATOR_HOME = Path(gscoordinator.__file__).parent.parent.absolute() +except ModuleNotFoundError: + print("Could not found coordinator") + exit(1) +TEMPLATE_DIR = COORDINATOR_HOME / "gscoordinator" / "template" +BUILTIN_APP_RESOURCE_PATH = ( + COORDINATOR_HOME / "gscoordinator" / "builtin" / "app" / "builtin_app.gar" +) +CMAKELISTS_TEMPLATE = TEMPLATE_DIR / "CMakeLists.template" +ANALYTICAL_ENGINE_HOME = "/usr/local/bin" +WORKSPACE = Path("/tmp/gs/builtin") + + +def cmake_and_make(cmake_commands): + try: + cmake_process = subprocess.run( + cmake_commands, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True + ) + make_process = subprocess.run( + ["make", "-j4"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True + ) + shutil.rmtree("CMakeFiles") + except subprocess.CalledProcessError as e: + print(e.stderr) + + +def cmake_graph(graph_class): + library_name = compute_sig(graph_class) + library_dir = WORKSPACE / library_name + library_dir.mkdir(exist_ok=True) + os.chdir(library_dir) + cmakelists_file = library_dir / "CMakeLists.txt" + with open(CMAKELISTS_TEMPLATE, mode="r") as template: + content = template.read() + content = Template(content).safe_substitute( + _analytical_engine_home=ANALYTICAL_ENGINE_HOME, + _frame_name=library_name, + _graph_type=graph_class, + ) + with open(cmakelists_file, mode="w") as f: + f.write(content) + cmake_commands = ["cmake", ".", "-DEXPERIMENTAL_ON=" + EXPERIMENTAL_ON] + if "ArrowFragment" in graph_class: + cmake_commands.append("-DPROPERTY_GRAPH_FRAME=True") + else: + cmake_commands.append("-DPROJECT_FRAME=True") + + cmake_and_make(cmake_commands) + print("Finished compiling", graph_class) + + +def cmake_app(app): + algo, graph_class = app + if "ArrowFragment" in graph_class: + graph_header = "vineyard/graph/fragment/arrow_fragment.h" + elif "ArrowProjectedFragment" in graph_class: + graph_header = "core/fragment/arrow_projected_fragment.h" + else: + raise ValueError("Not supported graph class %s" % graph_class) + + app_type, app_header, app_class = get_app_info(algo) + assert app_type == "cpp_pie", "Only support cpp_pie currently." + + library_name = compute_sig(f"{app_type}.{app_class}.{graph_class}") + library_dir = WORKSPACE / library_name + library_dir.mkdir(exist_ok=True) + os.chdir(library_dir) + cmakelists_file = library_dir / "CMakeLists.txt" + with open(CMAKELISTS_TEMPLATE, mode="r") as template: + content = template.read() + content = Template(content).safe_substitute( + _analytical_engine_home=ANALYTICAL_ENGINE_HOME, + _frame_name=library_name, + _graph_type=graph_class, + _graph_header=graph_header, + _app_type=app_class, + _app_header=app_header, + ) + with open(cmakelists_file, mode="w") as f: + f.write(content) + cmake_commands = ["cmake", ".", "-DEXPERIMENTAL_ON=" + EXPERIMENTAL_ON] + + cmake_and_make(cmake_commands) + print("Finished compiling", app_class, graph_class) + + +def get_app_info(algo: str): + fp = BUILTIN_APP_RESOURCE_PATH # default is builtin app resources. + with zipfile.ZipFile(fp, "r") as zip_ref: + with zip_ref.open(".gs_conf.yaml", "r") as f: + config_yaml = yaml.safe_load(f) + + for app in config_yaml["app"]: + if app["algo"] == algo: + app_type = app["type"] # cpp_pie or cython_pregel or cython_pie + if app_type == "cpp_pie": + return app_type, app["src"], f"{app['class_name']}<_GRAPH_TYPE>" + + raise KeyError("Algorithm %s does not exist in the gar resource." % algo) + + +def compile_graph(): + property_frame_template = "vineyard::ArrowFragment<{},{}>" + projected_frame_template = "gs::ArrowProjectedFragment<{},{},{},{}>" + + oid_types = ["int64_t", "std::string"] + vid_types = ["uint64_t"] + vdata_types = ["grape::EmptyType"] + edata_types = ["grape::EmptyType", "int64_t", "double"] + graph_classes = [] + + for oid in oid_types: + for vid in vid_types: + graph_class = property_frame_template.format(oid, vid) + graph_classes.append(graph_class) + + for oid in oid_types: + for vid in vid_types: + for vdata in vdata_types: + for edata in edata_types: + graph_class = projected_frame_template.format( + oid, vid, vdata, edata + ) + graph_classes.append(graph_class) + + with multiprocessing.Pool() as pool: + pool.map(cmake_graph, graph_classes) + + +def compile_cpp_pie_app(): + template = "gs::ArrowProjectedFragment<{},{},{},{}>" + luee = template.format( + "int64_t", "uint64_t", "grape::EmptyType", "grape::EmptyType" + ) + luel = template.format("int64_t", "uint64_t", "grape::EmptyType", "int64_t") + lued = template.format("int64_t", "uint64_t", "grape::EmptyType", "double") + targets = [ + ("pagerank", luee), + ("pagerank", luel), + ("wcc", luee), + ("wcc", luel), + ("cdlp", luee), + ("cdlp", luel), + ("bfs", luee), + ("bfs", luel), + ("sssp", luel), + ("sssp", lued), + ("kcore", luee), + ("triangles", luee), + ] + + with multiprocessing.Pool() as pool: + pool.map(cmake_app, targets) + + +if __name__ == "__main__": + os.makedirs(WORKSPACE, exist_ok=True) + compile_graph() + compile_cpp_pie_app() diff --git a/python/graphscope/experimental/nx/classes/graph.py b/python/graphscope/experimental/nx/classes/graph.py index c5980a65ad37..161bc3fbcda1 100644 --- a/python/graphscope/experimental/nx/classes/graph.py +++ b/python/graphscope/experimental/nx/classes/graph.py @@ -42,6 +42,7 @@ from graphscope.experimental.nx.utils.other import empty_graph_in_engine from graphscope.experimental.nx.utils.other import parse_ret_as_dict from graphscope.framework import dag_utils +from graphscope.framework import utils from graphscope.framework.errors import InvalidArgumentError from graphscope.framework.errors import check_argument from graphscope.framework.graph_schema import GraphSchema @@ -255,7 +256,6 @@ def __init__(self, incoming_graph_data=None, **attr): self._key = None self._op = None - self._graph_type = self._graph_type self._schema = GraphSchema() self._schema.init_nx_schema() create_empty_in_engine = attr.pop( @@ -330,18 +330,17 @@ def schema(self): return self._schema @property - def template_sigature(self): + def template_str(self): if self._key is None: raise RuntimeError("graph should be registered in remote.") - return hashlib.sha256( - "{}.{}.{}.{}.{}".format( - self._graph_type, - self._schema.oid_type, - self._schema.vid_type, - self._schema.vdata_type, - self._schema.edata_type, - ).encode("utf-8") - ).hexdigest() + if self._graph_type != types_pb2.DYNAMIC_PROPERTY: + return "gs::DynamicFragment" + elif self._graph_type == types_pb2.DYNAMIC_PROJECTED: + vdata_type = utils.data_type_to_cpp(self._schema.vdata_type) + edata_type = utils.data_type_to_cpp(self._schema.edata_type) + return f"gs::DynamicProjectedFragment<{vdata_type},{edata_type}>" + else: + raise ValueError(f"Unsupported graph type: {self._graph_type}") @property def graph_type(self): diff --git a/python/graphscope/framework/app.py b/python/graphscope/framework/app.py index 26275e81e1a2..9e0b14265477 100644 --- a/python/graphscope/framework/app.py +++ b/python/graphscope/framework/app.py @@ -126,8 +126,6 @@ def __init__(self, algo, gar=None, **kwargs): # built_in apps has no gar resource. self._gar = None - self._saved_signature = self.signature - @property def algo(self): """Algorithm name, e.g. sssp, pagerank. @@ -281,7 +279,7 @@ def key(self): def signature(self): """Signature is computed by all critical components of the App.""" return hashlib.sha256( - "{}.{}".format(self._app_assets.signature, self._graph.signature).encode( + "{}.{}".format(self._app_assets.signature, self._graph.template_str).encode( "utf-8" ) ).hexdigest() diff --git a/python/graphscope/framework/dag_utils.py b/python/graphscope/framework/dag_utils.py index ce09f133cf7c..2d2c8d3aa31e 100644 --- a/python/graphscope/framework/dag_utils.py +++ b/python/graphscope/framework/dag_utils.py @@ -51,8 +51,6 @@ def create_app(graph, app): types_pb2.E_DATA_TYPE: utils.s_to_attr( utils.data_type_to_cpp(graph.schema.edata_type) ), - types_pb2.APP_SIGNATURE: utils.s_to_attr(app.signature), - types_pb2.GRAPH_SIGNATURE: utils.s_to_attr(graph.template_sigature), } if app.gar is not None: config[types_pb2.GAR] = utils.bytes_to_attr(app.gar) diff --git a/python/graphscope/framework/graph.py b/python/graphscope/framework/graph.py index c321268b03d9..c7227d1d4074 100644 --- a/python/graphscope/framework/graph.py +++ b/python/graphscope/framework/graph.py @@ -34,9 +34,11 @@ from graphscope.framework.errors import check_argument from graphscope.framework.graph_schema import GraphSchema from graphscope.framework.utils import b_to_attr +from graphscope.framework.utils import data_type_to_cpp from graphscope.framework.utils import decode_dataframe from graphscope.framework.utils import decode_numpy from graphscope.framework.utils import i_to_attr +from graphscope.framework.utils import normalize_data_type_str from graphscope.framework.utils import s_to_attr from graphscope.framework.utils import transform_labeled_vertex_property_data_selector from graphscope.framework.utils import transform_vertex_range @@ -200,18 +202,24 @@ def signature(self): ).hexdigest() @property - def template_sigature(self): + def template_str(self): if self._key is None: raise RuntimeError("graph should be registered in remote.") - return hashlib.sha256( - "{}.{}.{}.{}.{}".format( - self._graph_type, - self._schema.oid_type, - self._schema.vid_type, - self._schema.vdata_type, - self._schema.edata_type, - ).encode("utf-8") - ).hexdigest() + graph_type = self._graph_type + # transform str/string to std::string + oid_type = normalize_data_type_str(self._schema.oid_type) + vid_type = self._schema.vid_type + vdata_type = data_type_to_cpp(self._schema.vdata_type) + edata_type = data_type_to_cpp(self._schema.edata_type) + if graph_type == types_pb2.ARROW_PROPERTY: + template = f"vineyard::ArrowFragment<{oid_type},{vid_type}>" + elif graph_type == types_pb2.ARROW_PROJECTED: + template = f"gs::ArrowProjectedFragment<{oid_type},{vid_type},{vdata_type},{edata_type}>" + elif graph_type == types_pb2.DYNAMIC_PROJECTED: + template = f"gs::DynamicProjectedFragment<{vdata_type},{edata_type}>" + else: + raise ValueError(f"Unsupported graph type: {graph_type}") + return template @property def vineyard_id(self):