diff --git a/addon/flamenco/bat/interface.py b/addon/flamenco/bat/interface.py index 851fd2f1..29c67d9d 100644 --- a/addon/flamenco/bat/interface.py +++ b/addon/flamenco/bat/interface.py @@ -197,7 +197,7 @@ def copy( # type: ignore *, relative_only: bool, packer_class=pack.Packer, - **packer_args: dict[Any, Any], + packer_kwargs: Optional[dict[Any, Any]] = None, ) -> PackThread: """Use BAT to copy the given file and dependencies to the target location. @@ -210,13 +210,18 @@ def copy( # type: ignore if _running_packthread is not None: raise RuntimeError("other packing operation already in progress") + print(f"packer_class: {packer_class}") + if packer_kwargs is None: + packer_kwargs = {} + packer_kwargs.setdefault("compress", True) + packer_kwargs.setdefault("relative_only", relative_only) + print(f"packer_kwargs: {packer_kwargs}") + packer = packer_class( base_blendfile, project, target, - compress=True, - relative_only=relative_only, - **packer_args, + **packer_kwargs, ) if exclusion_filter: filter_parts = exclusion_filter.strip().split(" ") diff --git a/addon/flamenco/bat/shaman.py b/addon/flamenco/bat/shaman.py index 0a062da3..c043ed38 100644 --- a/addon/flamenco/bat/shaman.py +++ b/addon/flamenco/bat/shaman.py @@ -44,6 +44,7 @@ class Packer(bat_pack.Packer): # type: ignore blendfile: Path, project_root: Path, target: str, + *, api_client: _ApiClient, checkout_path: str, **kwargs: dict[Any, Any], @@ -201,7 +202,7 @@ class Transferrer(bat_transfer.FileTransferer): # type: ignore try: checksum = cache.compute_cached_checksum(src) filesize = src.stat().st_size - relpath: str = bat_bpathlib.strip_root(dst) + relpath = str(bat_bpathlib.strip_root(dst)) filespec = ShamanFileSpec( sha=checksum, @@ -253,6 +254,7 @@ class Transferrer(bat_transfer.FileTransferer): # type: ignore to_upload: deque[_ShamanFileSpec] = deque() for file_spec in resp.files: + print(file_spec) if file_spec.path not in self._rel_to_local_path: msg = ( "Shaman requested path we did not intend to upload: %r" % file_spec @@ -261,7 +263,7 @@ class Transferrer(bat_transfer.FileTransferer): # type: ignore self.error_set(msg) return None - self.log.debug(" %s: %s", file_spec.status, file_spec.path) + self.log.debug(" %s: %s", file_spec.status.value, file_spec.path) match file_spec.status.value: case "unknown": to_upload.appendleft(file_spec) diff --git a/addon/flamenco/gui.py b/addon/flamenco/gui.py index 6a5963d3..5e86822e 100644 --- a/addon/flamenco/gui.py +++ b/addon/flamenco/gui.py @@ -3,8 +3,8 @@ from typing import Optional, TYPE_CHECKING -from flamenco import job_submission -from flamenco.job_types_propgroup import JobTypePropertyGroup +from . import preferences +from .job_types_propgroup import JobTypePropertyGroup import bpy @@ -31,6 +31,8 @@ class FLAMENCO_PT_job_submission(bpy.types.Panel): def draw(self, context: bpy.types.Context) -> None: from . import job_types + prefs = preferences.get(context) + layout = self.layout layout.use_property_decorate = False layout.use_property_split = True @@ -39,7 +41,14 @@ class FLAMENCO_PT_job_submission(bpy.types.Panel): col = layout.column(align=True) col.prop(context.scene, "flamenco_job_name", text="Job Name") - row = col.row(align=True) + + job_storage_col = col.column(align=True) + job_storage_col.enabled = not prefs.is_shaman_enabled + if prefs.is_shaman_enabled: + job_storage_col.label( + text="Shaman API will be used for job submission, so job storage location is ignored:" + ) + row = job_storage_col.row(align=True) row.prop(context.scene, "flamenco_job_storage", text="Job Storage") prop = row.operator("flamenco3.explore_file_path", text="", icon="WINDOW") prop.path = context.scene.flamenco_job_storage diff --git a/addon/flamenco/job_submission.py b/addon/flamenco/job_submission.py index 806ed3cb..b7542fd3 100644 --- a/addon/flamenco/job_submission.py +++ b/addon/flamenco/job_submission.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: GPL-3.0-or-later -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import TYPE_CHECKING, Optional, Union import bpy @@ -47,7 +47,9 @@ def job_for_scene(scene: bpy.types.Scene) -> Optional[_SubmittedJob]: def set_blend_file( - job_type: _AvailableJobType, job: _SubmittedJob, blendfile: Union[str, Path] + job_type: _AvailableJobType, + job: _SubmittedJob, + blendfile: Union[str, Path, PurePosixPath], ) -> None: """Update the job's 'blendfile' setting, if available. diff --git a/addon/flamenco/manager/__init__.py b/addon/flamenco/manager/__init__.py index a005ec40..7ab5e9ad 100644 --- a/addon/flamenco/manager/__init__.py +++ b/addon/flamenco/manager/__init__.py @@ -10,7 +10,7 @@ """ -__version__ = "63793f42-dirty" +__version__ = "8a43c69f-dirty" # import ApiClient from flamenco.manager.api_client import ApiClient diff --git a/addon/flamenco/manager/api/meta_api.py b/addon/flamenco/manager/api/meta_api.py index a3ad502c..3109dcf8 100644 --- a/addon/flamenco/manager/api/meta_api.py +++ b/addon/flamenco/manager/api/meta_api.py @@ -22,6 +22,7 @@ from flamenco.manager.model_utils import ( # noqa: F401 validate_and_convert_types ) from flamenco.manager.model.flamenco_version import FlamencoVersion +from flamenco.manager.model.manager_configuration import ManagerConfiguration class MetaApi(object): @@ -35,6 +36,48 @@ class MetaApi(object): if api_client is None: api_client = ApiClient() self.api_client = api_client + self.get_configuration_endpoint = _Endpoint( + settings={ + 'response_type': (ManagerConfiguration,), + 'auth': [], + 'endpoint_path': '/api/configuration', + 'operation_id': 'get_configuration', + 'http_method': 'GET', + 'servers': None, + }, + params_map={ + 'all': [ + ], + 'required': [], + 'nullable': [ + ], + 'enum': [ + ], + 'validation': [ + ] + }, + root_map={ + 'validations': { + }, + 'allowed_values': { + }, + 'openapi_types': { + }, + 'attribute_map': { + }, + 'location_map': { + }, + 'collection_format_map': { + } + }, + headers_map={ + 'accept': [ + 'application/json' + ], + 'content_type': [], + }, + api_client=api_client + ) self.get_version_endpoint = _Endpoint( settings={ 'response_type': (FlamencoVersion,), @@ -78,6 +121,78 @@ class MetaApi(object): api_client=api_client ) + def get_configuration( + self, + **kwargs + ): + """Get the configuration of this Manager. # noqa: E501 + + This method makes a synchronous HTTP request by default. To make an + asynchronous HTTP request, please pass async_req=True + + >>> thread = api.get_configuration(async_req=True) + >>> result = thread.get() + + + Keyword Args: + _return_http_data_only (bool): response data without head status + code and headers. Default is True. + _preload_content (bool): if False, the urllib3.HTTPResponse object + will be returned without reading/decoding response data. + Default is True. + _request_timeout (int/float/tuple): timeout setting for this request. If + one number provided, it will be total request timeout. It can also + be a pair (tuple) of (connection, read) timeouts. + Default is None. + _check_input_type (bool): specifies if type checking + should be done one the data sent to the server. + Default is True. + _check_return_type (bool): specifies if type checking + should be done one the data received from the server. + Default is True. + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _content_type (str/None): force body content-type. + Default is None and content-type will be predicted by allowed + content-types and body. + _host_index (int/None): specifies the index of the server + that we want to use. + Default is read from the configuration. + async_req (bool): execute request asynchronously + + Returns: + ManagerConfiguration + If the method is called asynchronously, returns the request + thread. + """ + kwargs['async_req'] = kwargs.get( + 'async_req', False + ) + kwargs['_return_http_data_only'] = kwargs.get( + '_return_http_data_only', True + ) + kwargs['_preload_content'] = kwargs.get( + '_preload_content', True + ) + kwargs['_request_timeout'] = kwargs.get( + '_request_timeout', None + ) + kwargs['_check_input_type'] = kwargs.get( + '_check_input_type', True + ) + kwargs['_check_return_type'] = kwargs.get( + '_check_return_type', True + ) + kwargs['_spec_property_naming'] = kwargs.get( + '_spec_property_naming', False + ) + kwargs['_content_type'] = kwargs.get( + '_content_type') + kwargs['_host_index'] = kwargs.get('_host_index') + return self.get_configuration_endpoint.call_with_http_info(**kwargs) + def get_version( self, **kwargs diff --git a/addon/flamenco/manager/api_client.py b/addon/flamenco/manager/api_client.py index 3173a39e..987229f7 100644 --- a/addon/flamenco/manager/api_client.py +++ b/addon/flamenco/manager/api_client.py @@ -76,7 +76,7 @@ class ApiClient(object): self.default_headers[header_name] = header_value self.cookie = cookie # Set default User-Agent. - self.user_agent = 'Flamenco/63793f42-dirty (Blender add-on)' + self.user_agent = 'Flamenco/8a43c69f-dirty (Blender add-on)' def __enter__(self): return self diff --git a/addon/flamenco/manager/configuration.py b/addon/flamenco/manager/configuration.py index c72918a0..e3737a06 100644 --- a/addon/flamenco/manager/configuration.py +++ b/addon/flamenco/manager/configuration.py @@ -404,7 +404,7 @@ conf = flamenco.manager.Configuration( "OS: {env}\n"\ "Python Version: {pyversion}\n"\ "Version of the API: 1.0.0\n"\ - "SDK Package Version: 63793f42-dirty".\ + "SDK Package Version: 8a43c69f-dirty".\ format(env=sys.platform, pyversion=sys.version) def get_host_settings(self): diff --git a/addon/flamenco/manager/docs/ManagerConfiguration.md b/addon/flamenco/manager/docs/ManagerConfiguration.md new file mode 100644 index 00000000..547b2d7d --- /dev/null +++ b/addon/flamenco/manager/docs/ManagerConfiguration.md @@ -0,0 +1,12 @@ +# ManagerConfiguration + + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**shaman_enabled** | **bool** | Whether the Shaman file transfer API is available. | +**any string name** | **bool, date, datetime, dict, float, int, list, str, none_type** | any string name can be used but the value must be the correct type | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/addon/flamenco/manager/docs/MetaApi.md b/addon/flamenco/manager/docs/MetaApi.md index e07208a1..3fa46c5e 100644 --- a/addon/flamenco/manager/docs/MetaApi.md +++ b/addon/flamenco/manager/docs/MetaApi.md @@ -4,9 +4,71 @@ All URIs are relative to *http://localhost* Method | HTTP request | Description ------------- | ------------- | ------------- +[**get_configuration**](MetaApi.md#get_configuration) | **GET** /api/configuration | Get the configuration of this Manager. [**get_version**](MetaApi.md#get_version) | **GET** /api/version | Get the Flamenco version of this Manager +# **get_configuration** +> ManagerConfiguration get_configuration() + +Get the configuration of this Manager. + +### Example + + +```python +import time +import flamenco.manager +from flamenco.manager.api import meta_api +from flamenco.manager.model.manager_configuration import ManagerConfiguration +from pprint import pprint +# Defining the host is optional and defaults to http://localhost +# See configuration.py for a list of all supported configuration parameters. +configuration = flamenco.manager.Configuration( + host = "http://localhost" +) + + +# Enter a context with an instance of the API client +with flamenco.manager.ApiClient() as api_client: + # Create an instance of the API class + api_instance = meta_api.MetaApi(api_client) + + # example, this endpoint has no required or optional parameters + try: + # Get the configuration of this Manager. + api_response = api_instance.get_configuration() + pprint(api_response) + except flamenco.manager.ApiException as e: + print("Exception when calling MetaApi->get_configuration: %s\n" % e) +``` + + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**ManagerConfiguration**](ManagerConfiguration.md) + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details + +| Status code | Description | Response headers | +|-------------|-------------|------------------| +**200** | normal response | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **get_version** > FlamencoVersion get_version() diff --git a/addon/flamenco/manager/model/manager_configuration.py b/addon/flamenco/manager/model/manager_configuration.py new file mode 100644 index 00000000..c1ee4ebb --- /dev/null +++ b/addon/flamenco/manager/model/manager_configuration.py @@ -0,0 +1,261 @@ +""" + Flamenco manager + + Render Farm manager API # noqa: E501 + + The version of the OpenAPI document: 1.0.0 + Generated by: https://openapi-generator.tech +""" + + +import re # noqa: F401 +import sys # noqa: F401 + +from flamenco.manager.model_utils import ( # noqa: F401 + ApiTypeError, + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + none_type, + validate_get_composed_info, + OpenApiModel +) +from flamenco.manager.exceptions import ApiAttributeError + + + +class ManagerConfiguration(ModelNormal): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + discriminator_value_class_map (dict): A dict to go from the discriminator + variable value to the discriminator class name. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = { + } + + validations = { + } + + @cached_property + def additional_properties_type(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + """ + return (bool, date, datetime, dict, float, int, list, str, none_type,) # noqa: E501 + + _nullable = False + + @cached_property + def openapi_types(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + return { + 'shaman_enabled': (bool,), # noqa: E501 + } + + @cached_property + def discriminator(): + return None + + + attribute_map = { + 'shaman_enabled': 'shamanEnabled', # noqa: E501 + } + + read_only_vars = { + } + + _composed_schemas = {} + + @classmethod + @convert_js_args_to_python_args + def _from_openapi_data(cls, shaman_enabled, *args, **kwargs): # noqa: E501 + """ManagerConfiguration - a model defined in OpenAPI + + Args: + shaman_enabled (bool): Whether the Shaman file transfer API is available. + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + + _check_type = kwargs.pop('_check_type', True) + _spec_property_naming = kwargs.pop('_spec_property_naming', False) + _path_to_item = kwargs.pop('_path_to_item', ()) + _configuration = kwargs.pop('_configuration', None) + _visited_composed_classes = kwargs.pop('_visited_composed_classes', ()) + + self = super(OpenApiModel, cls).__new__(cls) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.shaman_enabled = shaman_enabled + for var_name, var_value in kwargs.items(): + if var_name not in self.attribute_map and \ + self._configuration is not None and \ + self._configuration.discard_unknown_keys and \ + self.additional_properties_type is None: + # discard variable. + continue + setattr(self, var_name, var_value) + return self + + required_properties = set([ + '_data_store', + '_check_type', + '_spec_property_naming', + '_path_to_item', + '_configuration', + '_visited_composed_classes', + ]) + + @convert_js_args_to_python_args + def __init__(self, shaman_enabled, *args, **kwargs): # noqa: E501 + """ManagerConfiguration - a model defined in OpenAPI + + Args: + shaman_enabled (bool): Whether the Shaman file transfer API is available. + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + + _check_type = kwargs.pop('_check_type', True) + _spec_property_naming = kwargs.pop('_spec_property_naming', False) + _path_to_item = kwargs.pop('_path_to_item', ()) + _configuration = kwargs.pop('_configuration', None) + _visited_composed_classes = kwargs.pop('_visited_composed_classes', ()) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + self.shaman_enabled = shaman_enabled + for var_name, var_value in kwargs.items(): + if var_name not in self.attribute_map and \ + self._configuration is not None and \ + self._configuration.discard_unknown_keys and \ + self.additional_properties_type is None: + # discard variable. + continue + setattr(self, var_name, var_value) + if var_name in self.read_only_vars: + raise ApiAttributeError(f"`{var_name}` is a read-only attribute. Use `from_openapi_data` to instantiate " + f"class with read only attributes.") diff --git a/addon/flamenco/manager/models/__init__.py b/addon/flamenco/manager/models/__init__.py index 3ae76934..983bc1d0 100644 --- a/addon/flamenco/manager/models/__init__.py +++ b/addon/flamenco/manager/models/__init__.py @@ -23,6 +23,7 @@ from flamenco.manager.model.job_all_of import JobAllOf from flamenco.manager.model.job_metadata import JobMetadata from flamenco.manager.model.job_settings import JobSettings from flamenco.manager.model.job_status import JobStatus +from flamenco.manager.model.manager_configuration import ManagerConfiguration from flamenco.manager.model.registered_worker import RegisteredWorker from flamenco.manager.model.security_error import SecurityError from flamenco.manager.model.shaman_checkout import ShamanCheckout diff --git a/addon/flamenco/manager_README.md b/addon/flamenco/manager_README.md index 052eb753..c3093e59 100644 --- a/addon/flamenco/manager_README.md +++ b/addon/flamenco/manager_README.md @@ -4,7 +4,7 @@ Render Farm manager API The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: - API version: 1.0.0 -- Package version: 63793f42-dirty +- Package version: 8a43c69f-dirty - Build package: org.openapitools.codegen.languages.PythonClientCodegen For more information, please visit [https://flamenco.io/](https://flamenco.io/) @@ -67,6 +67,7 @@ Class | Method | HTTP request | Description *JobsApi* | [**fetch_job**](flamenco/manager/docs/JobsApi.md#fetch_job) | **GET** /api/jobs/{job_id} | Fetch info about the job. *JobsApi* | [**get_job_types**](flamenco/manager/docs/JobsApi.md#get_job_types) | **GET** /api/jobs/types | Get list of job types and their parameters. *JobsApi* | [**submit_job**](flamenco/manager/docs/JobsApi.md#submit_job) | **POST** /api/jobs | Submit a new job for Flamenco Manager to execute. +*MetaApi* | [**get_configuration**](flamenco/manager/docs/MetaApi.md#get_configuration) | **GET** /api/configuration | Get the configuration of this Manager. *MetaApi* | [**get_version**](flamenco/manager/docs/MetaApi.md#get_version) | **GET** /api/version | Get the Flamenco version of this Manager *ShamanApi* | [**shaman_checkout**](flamenco/manager/docs/ShamanApi.md#shaman_checkout) | **POST** /shaman/checkout/create | Create a directory, and symlink the required files into it. The files must all have been uploaded to Shaman before calling this endpoint. *ShamanApi* | [**shaman_checkout_requirements**](flamenco/manager/docs/ShamanApi.md#shaman_checkout_requirements) | **POST** /shaman/checkout/requirements | Checks a Shaman Requirements file, and reports which files are unknown. @@ -97,6 +98,7 @@ Class | Method | HTTP request | Description - [JobMetadata](flamenco/manager/docs/JobMetadata.md) - [JobSettings](flamenco/manager/docs/JobSettings.md) - [JobStatus](flamenco/manager/docs/JobStatus.md) + - [ManagerConfiguration](flamenco/manager/docs/ManagerConfiguration.md) - [RegisteredWorker](flamenco/manager/docs/RegisteredWorker.md) - [SecurityError](flamenco/manager/docs/SecurityError.md) - [ShamanCheckout](flamenco/manager/docs/ShamanCheckout.md) diff --git a/addon/flamenco/operators.py b/addon/flamenco/operators.py index aef546f4..ffc641a3 100644 --- a/addon/flamenco/operators.py +++ b/addon/flamenco/operators.py @@ -2,18 +2,19 @@ # import datetime +import functools import logging -from pathlib import Path +from pathlib import Path, PurePosixPath from typing import Optional, TYPE_CHECKING from urllib3.exceptions import HTTPError, MaxRetryError import bpy -from . import job_types, job_submission +from . import job_types, job_submission, preferences from .job_types_propgroup import JobTypePropertyGroup if TYPE_CHECKING: - from .bat_interface import ( + from .bat.interface import ( PackThread as _PackThread, Message as _Message, ) @@ -50,7 +51,6 @@ class FLAMENCO_OT_fetch_job_types(FlamencoOpMixin, bpy.types.Operator): api_client = self.get_api_client(context) from flamenco.manager import ApiException - from . import job_types scene = context.scene old_job_type_name = getattr(scene, "flamenco_job_type", "") @@ -85,13 +85,14 @@ class FLAMENCO_OT_ping_manager(FlamencoOpMixin, bpy.types.Operator): from flamenco.manager import ApiException from flamenco.manager.apis import MetaApi - from flamenco.manager.models import FlamencoVersion + from flamenco.manager.models import FlamencoVersion, ManagerConfiguration context.window_manager.flamenco_status_ping = "..." meta_api = MetaApi(api_client) try: - response: FlamencoVersion = meta_api.get_version() + version: FlamencoVersion = meta_api.get_version() + config: ManagerConfiguration = meta_api.get_configuration() except ApiException as ex: report = "Manager cannot be reached: %s" % ex level = "ERROR" @@ -104,9 +105,13 @@ class FLAMENCO_OT_ping_manager(FlamencoOpMixin, bpy.types.Operator): report = "Manager cannot be reached: %s" % ex level = "ERROR" else: - report = "%s version %s found" % (response.name, response.version) + report = "%s version %s found" % (version.name, version.version) level = "INFO" + # Store whether this Manager supports the Shaman API. + prefs = preferences.get(context) + prefs.is_shaman_enabled = config.shaman_enabled + self.report({level}, report) context.window_manager.flamenco_status_ping = report return {"FINISHED"} @@ -138,7 +143,7 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): bl_description = "Pack the current blend file and send it to Flamenco" bl_options = {"REGISTER"} # No UNDO. - blendfile_on_farm: Optional[Path] = None + blendfile_on_farm: Optional[PurePosixPath] = None job_name: bpy.props.StringProperty(name="Job Name") # type: ignore job: Optional[_SubmittedJob] = None @@ -211,13 +216,36 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): return filepath def _bat_pack(self, context: bpy.types.Context, blendfile: Path) -> set[str]: - from . import bat_interface + from .bat import interface as bat_interface if bat_interface.is_packing(): self.report({"ERROR"}, "Another packing operation is running") self._quit(context) return {"CANCELLED"} + prefs = preferences.get(context) + if prefs.is_shaman_enabled: + blendfile_on_farm = self._bat_pack_shaman(context, blendfile) + else: + blendfile_on_farm = self._bat_pack_filesystem(context, blendfile) + + self.blendfile_on_farm = blendfile_on_farm + + context.window_manager.modal_handler_add(self) + wm = context.window_manager + self.timer = wm.event_timer_add(0.25, window=context.window) + + return {"RUNNING_MODAL"} + + def _bat_pack_filesystem( + self, context: bpy.types.Context, blendfile: Path + ) -> PurePosixPath: + """Use BAT to store the pack on the filesystem. + + :return: the path of the blend file, for use in the job definition. + """ + from .bat import interface as bat_interface + # TODO: get project path from addon preferences / project definition on Manager. project_path = blendfile.parent try: @@ -225,7 +253,7 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): except FileNotFoundError: # Path.resolve() will raise a FileNotFoundError if the project path doesn't exist. self.report({"ERROR"}, "Project path %s does not exist" % project_path) - return {"CANCELLED"} + raise # TODO: handle this properly. # Determine where the blend file will be stored. unique_dir = "%s-%s" % ( @@ -237,7 +265,6 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): # TODO: this should take the blendfile location relative to the project path into account. pack_target_file = pack_target_dir / blendfile.name self.log.info("Will store blend file at %s", pack_target_file) - self.blendfile_on_farm = pack_target_file self.packthread = bat_interface.copy( base_blendfile=blendfile, @@ -247,14 +274,44 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): relative_only=True, # TODO: get from GUI. ) - context.window_manager.modal_handler_add(self) - wm = context.window_manager - self.timer = wm.event_timer_add(0.25, window=context.window) + return PurePosixPath(pack_target_file.as_posix()) - return {"RUNNING_MODAL"} + def _bat_pack_shaman( + self, context: bpy.types.Context, blendfile: Path + ) -> PurePosixPath: + """Use the Manager's Shaman API to submit the BAT pack. + + :return: the filesystem path of the blend file, for in the render job definition. + """ + from .bat import ( + interface as bat_interface, + shaman as bat_shaman, + ) + + assert self.job is not None + self.log.info("Sending BAT pack to Shaman") + + # TODO: get project name from preferences/GUI. + # TODO: update Shaman API to ensure this is a unique location. + checkout_root = PurePosixPath(f"project/{self.job.name}") + + self.packthread = bat_interface.copy( + base_blendfile=blendfile, + project=blendfile.parent, # TODO: get from preferences/GUI. + target="/", # Target directory irrelevant for Shaman transfers. + exclusion_filter="", # TODO: get from GUI. + relative_only=True, # TODO: get from GUI. + packer_class=functools.partial( + bat_shaman.Packer, + api_client=self.get_api_client(context), + checkout_path=checkout_root, + ), + ) + + return checkout_root / blendfile.name # TODO: get relative to the checkout dir. def _on_bat_pack_msg(self, context: bpy.types.Context, msg: _Message) -> set[str]: - from . import bat_interface + from .bat import interface as bat_interface if isinstance(msg, bat_interface.MsgDone): self._submit_job(context) diff --git a/addon/flamenco/preferences.py b/addon/flamenco/preferences.py index 28d710c8..f016eacd 100644 --- a/addon/flamenco/preferences.py +++ b/addon/flamenco/preferences.py @@ -9,6 +9,7 @@ def discard_flamenco_client(prefs, context): from . import comms comms.discard_flamenco_data() + context.window_manager.flamenco_status_ping = "" def _update_default_job_storage( @@ -28,6 +29,12 @@ class FlamencoPreferences(bpy.types.AddonPreferences): update=discard_flamenco_client, ) + is_shaman_enabled: bpy.props.BoolProperty( # type: ignore + name="Shaman Enabled", + description="Whether this Manager has the Shaman protocol enabled", + default=False, + ) + job_storage: bpy.props.StringProperty( # type: ignore name="Job Storage Directory", subtype="DIR_PATH", @@ -48,6 +55,12 @@ class FlamencoPreferences(bpy.types.AddonPreferences): if context.window_manager.flamenco_status_ping: col.label(text=context.window_manager.flamenco_status_ping) + col = layout.column(align=True) + col.enabled = not self.is_shaman_enabled + if self.is_shaman_enabled: + col.label( + text="This Manager supports the Shaman API, so this setting will be ignored:" + ) col.prop(self, "job_storage") @@ -88,6 +101,7 @@ _register, _unregister = bpy.utils.register_classes_factory(classes) def register(): _register() _register_rna_props(get(bpy.context)) + bpy.context.window_manager.flamenco_status_ping = "" def unregister(): diff --git a/internal/manager/api_impl/api_impl.go b/internal/manager/api_impl/api_impl.go index 82f7a306..a3e030c0 100644 --- a/internal/manager/api_impl/api_impl.go +++ b/internal/manager/api_impl/api_impl.go @@ -7,9 +7,7 @@ import ( "context" "fmt" "io" - "net/http" - "git.blender.org/flamenco/internal/appinfo" "git.blender.org/flamenco/internal/manager/job_compilers" "git.blender.org/flamenco/internal/manager/persistence" "git.blender.org/flamenco/internal/manager/task_state_machine" @@ -136,10 +134,3 @@ func sendAPIError(e echo.Context, code int, message string, args ...interface{}) } return e.JSON(code, petErr) } - -func (f *Flamenco) GetVersion(e echo.Context) error { - return e.JSON(http.StatusOK, api.FlamencoVersion{ - Version: appinfo.ApplicationVersion, - Name: appinfo.ApplicationName, - }) -} diff --git a/internal/manager/api_impl/meta.go b/internal/manager/api_impl/meta.go new file mode 100644 index 00000000..38552bb7 --- /dev/null +++ b/internal/manager/api_impl/meta.go @@ -0,0 +1,25 @@ +// Package api_impl implements the OpenAPI API from pkg/api/flamenco-manager.yaml. +package api_impl + +// SPDX-License-Identifier: GPL-3.0-or-later + +import ( + "net/http" + + "git.blender.org/flamenco/internal/appinfo" + "git.blender.org/flamenco/pkg/api" + "github.com/labstack/echo/v4" +) + +func (f *Flamenco) GetVersion(e echo.Context) error { + return e.JSON(http.StatusOK, api.FlamencoVersion{ + Version: appinfo.ApplicationVersion, + Name: appinfo.ApplicationName, + }) +} + +func (f *Flamenco) GetConfiguration(e echo.Context) error { + return e.JSON(http.StatusOK, api.ManagerConfiguration{ + ShamanEnabled: f.isShamanEnabled(), + }) +} diff --git a/internal/worker/mocks/client.gen.go b/internal/worker/mocks/client.gen.go index 960e6f3c..466876ee 100644 --- a/internal/worker/mocks/client.gen.go +++ b/internal/worker/mocks/client.gen.go @@ -56,6 +56,26 @@ func (mr *MockFlamencoClientMockRecorder) FetchJobWithResponse(arg0, arg1 interf return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchJobWithResponse", reflect.TypeOf((*MockFlamencoClient)(nil).FetchJobWithResponse), varargs...) } +// GetConfigurationWithResponse mocks base method. +func (m *MockFlamencoClient) GetConfigurationWithResponse(arg0 context.Context, arg1 ...api.RequestEditorFn) (*api.GetConfigurationResponse, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetConfigurationWithResponse", varargs...) + ret0, _ := ret[0].(*api.GetConfigurationResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetConfigurationWithResponse indicates an expected call of GetConfigurationWithResponse. +func (mr *MockFlamencoClientMockRecorder) GetConfigurationWithResponse(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfigurationWithResponse", reflect.TypeOf((*MockFlamencoClient)(nil).GetConfigurationWithResponse), varargs...) +} + // GetJobTypesWithResponse mocks base method. func (m *MockFlamencoClient) GetJobTypesWithResponse(arg0 context.Context, arg1 ...api.RequestEditorFn) (*api.GetJobTypesResponse, error) { m.ctrl.T.Helper() diff --git a/pkg/api/flamenco-manager.yaml b/pkg/api/flamenco-manager.yaml index 894face2..e9e82fad 100644 --- a/pkg/api/flamenco-manager.yaml +++ b/pkg/api/flamenco-manager.yaml @@ -13,7 +13,7 @@ servers: - url: / paths: /api/version: - summary: Workers can use this to check this is actually a Flamenco server. + summary: Clients can use this to check this is actually a Flamenco server. get: summary: Get the Flamenco version of this Manager operationId: getVersion @@ -26,6 +26,19 @@ paths: schema: $ref: "#/components/schemas/FlamencoVersion" + /api/configuration: + summary: Endpoint for getting configuration of Flamenco Manager. + get: + summary: Get the configuration of this Manager. + operationId: getConfiguration + tags: [meta] + responses: + "200": + description: normal response + content: + application/json: + schema: {$ref: "#/components/schemas/ManagerConfiguration"} + /api/worker/register-worker: summary: Registration of new workers post: @@ -437,6 +450,14 @@ components: version: { type: string } name: { type: string } + ManagerConfiguration: + type: object + properties: + "shamanEnabled": + description: Whether the Shaman file transfer API is available. + type: boolean + required: [shamanEnabled] + WorkerRegistration: type: object required: [secret, platform, supported_task_types, nickname] diff --git a/pkg/api/openapi_client.gen.go b/pkg/api/openapi_client.gen.go index 990c7533..1adbc70c 100644 --- a/pkg/api/openapi_client.gen.go +++ b/pkg/api/openapi_client.gen.go @@ -90,6 +90,9 @@ func WithRequestEditorFn(fn RequestEditorFn) ClientOption { // The interface specification for the client above. type ClientInterface interface { + // GetConfiguration request + GetConfiguration(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // SubmitJob request with any body SubmitJobWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -150,6 +153,18 @@ type ClientInterface interface { ShamanFileStoreWithBody(ctx context.Context, checksum string, filesize int, params *ShamanFileStoreParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) } +func (c *Client) GetConfiguration(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetConfigurationRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) SubmitJobWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewSubmitJobRequestWithBody(c.Server, contentType, body) if err != nil { @@ -414,6 +429,33 @@ func (c *Client) ShamanFileStoreWithBody(ctx context.Context, checksum string, f return c.Client.Do(req) } +// NewGetConfigurationRequest generates requests for GetConfiguration +func NewGetConfigurationRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/configuration") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewSubmitJobRequest calls the generic SubmitJob builder with application/json body func NewSubmitJobRequest(server string, body SubmitJobJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -1019,6 +1061,9 @@ func WithBaseURL(baseURL string) ClientOption { // ClientWithResponsesInterface is the interface specification for the client with responses above. type ClientWithResponsesInterface interface { + // GetConfiguration request + GetConfigurationWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetConfigurationResponse, error) + // SubmitJob request with any body SubmitJobWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SubmitJobResponse, error) @@ -1079,6 +1124,28 @@ type ClientWithResponsesInterface interface { ShamanFileStoreWithBodyWithResponse(ctx context.Context, checksum string, filesize int, params *ShamanFileStoreParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ShamanFileStoreResponse, error) } +type GetConfigurationResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ManagerConfiguration +} + +// Status returns HTTPResponse.Status +func (r GetConfigurationResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetConfigurationResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type SubmitJobResponse struct { Body []byte HTTPResponse *http.Response @@ -1419,6 +1486,15 @@ func (r ShamanFileStoreResponse) StatusCode() int { return 0 } +// GetConfigurationWithResponse request returning *GetConfigurationResponse +func (c *ClientWithResponses) GetConfigurationWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetConfigurationResponse, error) { + rsp, err := c.GetConfiguration(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetConfigurationResponse(rsp) +} + // SubmitJobWithBodyWithResponse request with arbitrary body returning *SubmitJobResponse func (c *ClientWithResponses) SubmitJobWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SubmitJobResponse, error) { rsp, err := c.SubmitJobWithBody(ctx, contentType, body, reqEditors...) @@ -1610,6 +1686,32 @@ func (c *ClientWithResponses) ShamanFileStoreWithBodyWithResponse(ctx context.Co return ParseShamanFileStoreResponse(rsp) } +// ParseGetConfigurationResponse parses an HTTP response from a GetConfigurationWithResponse call +func ParseGetConfigurationResponse(rsp *http.Response) (*GetConfigurationResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetConfigurationResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ManagerConfiguration + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + // ParseSubmitJobResponse parses an HTTP response from a SubmitJobWithResponse call func ParseSubmitJobResponse(rsp *http.Response) (*SubmitJobResponse, error) { bodyBytes, err := ioutil.ReadAll(rsp.Body) diff --git a/pkg/api/openapi_server.gen.go b/pkg/api/openapi_server.gen.go index a8cba647..6d9cc096 100644 --- a/pkg/api/openapi_server.gen.go +++ b/pkg/api/openapi_server.gen.go @@ -13,6 +13,9 @@ import ( // ServerInterface represents all server handlers. type ServerInterface interface { + // Get the configuration of this Manager. + // (GET /api/configuration) + GetConfiguration(ctx echo.Context) error // Submit a new job for Flamenco Manager to execute. // (POST /api/jobs) SubmitJob(ctx echo.Context) error @@ -66,6 +69,15 @@ type ServerInterfaceWrapper struct { Handler ServerInterface } +// GetConfiguration converts echo context to params. +func (w *ServerInterfaceWrapper) GetConfiguration(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.GetConfiguration(ctx) + return err +} + // SubmitJob converts echo context to params. func (w *ServerInterfaceWrapper) SubmitJob(ctx echo.Context) error { var err error @@ -320,6 +332,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL Handler: si, } + router.GET(baseURL+"/api/configuration", wrapper.GetConfiguration) router.POST(baseURL+"/api/jobs", wrapper.SubmitJob) router.GET(baseURL+"/api/jobs/types", wrapper.GetJobTypes) router.GET(baseURL+"/api/jobs/:job_id", wrapper.FetchJob) diff --git a/pkg/api/openapi_spec.gen.go b/pkg/api/openapi_spec.gen.go index 3ec06773..93d67260 100644 --- a/pkg/api/openapi_spec.gen.go +++ b/pkg/api/openapi_spec.gen.go @@ -18,92 +18,93 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "3by5+Z8AAAD//+uKmtshVAAA", + "H4sIAAAAAAAC/+Q8224cN5a/QtQskATbN11s2XpajR0nMpJYiOTJArEhsapOdVNikRWSpXbHEDAfsX+y", + "O8A+7DztD3j+aEEeVhWriy21E8nj2fGD0epiHR6e+439PslkWUkBwujk8H2iswWU1H080prNBeRnVF/Z", + "v3PQmWKVYVIkh72nhGlCibGfqCbM2L8VZMCuISfpipgFkJ+kugI1SUZJpWQFyjBwu2SyLKnI3WdmoHQf", + "/kVBkRwmf5h2yE09ZtNn+EJyM0rMqoLkMKFK0ZX9+1Km9m3/tTaKibn//rxSTCpmVsECJgzMQTUr8NvI", + "64KW8Qe3w9SGmvrO41j6neJKeyKqrzYjUtcstw8KqUpqkkP8YrS+8GaUKPilZgry5PDnZpEljj9Li1tw", + "hDUqBSQJsRp1/Hrb7ivTS8iMRfDomjJOUw4vZXoKxlh0BpJzysScA9H4nMiCUPJSpsRC0xEBWUiW4cc+", + "nJ8WIMicXYMYEc5KZpycXVPOcvt/DZoYab/TQDyQCXkl+IrU2uJIlswsCBLNbW73bkVwQPx1YcuhoDU3", + "Q7zOFkD8Q8SD6IVcCo8MqTUosrS452BAlUy4/RdMNySZIPgAZnyL9pupkZIbVvmNmOg2svKoCpqBAwo5", + "M/boCNHjX1CuYTQkrlmAskhTzuWS2FfXESW0MHbNAsilTMmCapICCKLrtGTGQD4hP8ma54SVFV+RHDjg", + "a5wTeMc0AqT6SpNCKgR9KdMRoSK3BkSWFeN2DTOTN6IT9FRKDlS4E11TPqTPycospCDwrlKgNZOO+CkQ", + "u7qmBnJLI6lyPGDDB3An6bOuxavlzWgoGlewGuJwnIMwrGCgPJBW5EekrLWx+NSC/VKjIHqmXXpFiO5j", + "FYOqeUQXjsSKwDujKKFqXpfWwjTyllariX1RT05lCSeoW6svvyKZZUOtIbcrMwXUAB7V698qwKFT8c6y", + "fIQIsbKEnFEDfEUUWFCEuqPmUDDB7Asjawjc9nbLkaOJrI3HiCrDsppT1fJhgzzoOm3M521WN2KoTv2b", + "rap/NIQz//o102xdyYyqbyOQVdy+anl5eH2MBtISq1ErRb7k7AoIJX/kIKwQ0zwfS/HVhJyCseAuHEMu", + "0MygP6YCbYGgvN3DLKixW9c8F184gWwtFYjcGRAdJ/Sai7EK4Bdt6RZOOz6teYc6HdsnKA6oEA3PybNa", + "KRCGr4i0dpw2cJ2GBZZcT8jFt0en3379/PzF8Xdfn58cnX17gVFKzhRkRqoVqahZkH8lF2+S6R/cvzfJ", + "BaFVZUma47FB1KU9X8E4nNv1ySjJmWo+uq+9R11QvYD8vFv5NqLAm4RmaOA9BYLTB1YD3RfV5Ph5o8/u", + "2FZovEhMyA+SCNDW1mmj6szUCjT50rkvPSI5y+xWVDHQXxGqgOi6qqQy60f3yI9sZLO3aw/NJTXJyMnC", + "nYeMn67x9t2eGCUyTb6ngs5BoQtgxqk+La2BjoQGnKbAPy5k88TcPtyMhTSDaGBNHbxIIHrBnnfphqVW", + "xLh/x7RphMFJ92a6DWnUhHG/7cRnPYu44bjdFrEDNvH64Fj+AVFgvbRzWZRoDA59lOks0TvIagN35RGb", + "g/RWgILHDXpxxgWvxE70tVJSWWDrmUwOvei80ZhhalCC1nQew3cNIQezWx/D5gWnJYhM/gmU9sHilpS5", + "7t64HYtmoderGBYvMfWinL8qksOfb5ew0yY+tG/djAaEdLFITGLsAxfNsRK0oWVl7VFD7pwaGNsnsdCJ", + "RcC9fn38vHEzL112dEditW1OZ01Fm9LVVX7Pp1njjsO0oVm3X4vs25u3yKDvwdCcGuoYlecu7KL8pEf7", + "wYnX4kyVMqOoWpHSA/NuV0/I91I5xa04vAt9TkaF9VqltPG/s1i11XJyQSfpJLsgQhqkQxMmX4ELPeEd", + "tbC8QDtBO0xOK8UMkBeKzRfWC9kYZQIlZdxivUoViH9LvQuUat6sQB1ITt0Ccmr+93+ugQeGrSfIp4GP", + "iNMJo7nou62ANA6UZoZdu8yZisxSAJPoioPxnwUSi0kxLijDFe2HitoQPRklv9RQuw9UZQt2HXxE/4zg", + "x1YynNv3QHpfuM8IpbYkGoebJ6NkSV2SNy6kGttIRkcdvPc1z6Qo2LxW1ETNjl7QkoqvhfUkeTR7x+B3", + "AeTULSV2R2IUFboARY5Ojl3E1nijyd1haH/LmJn6EeZMG1CQox8Zok3z3OaMUV3gVJtzx89+0SeIO1h2", + "tdkTcWqsfscDE1mYJVUbopatzA4eqbM8bZRw3hZw+lHAnTWO31VwamkxaokaFp4aYoySDKN6h2WyTuWA", + "MhtOFOPzKWS1Yma1wVVv7X9vc7wotc8WkF3JOlIHsrmYLJxUa6w1mQUwRU6/Pdp99Jhk9kVdlyOi2a8u", + "dE9XBjRGvjloiwLhMkPb6NPBzO/WpTFrlhK11jpgl4QcJl2GPZlLS8IFTQ6TvUfpbP/pTrZ7kM729vby", + "nSLdf1Rks4MnT+nObkZnj9Od/PH+LN999PjpwZNZ+mR2kMOj2X5+MNt9CjMLiP0KyeHO/u6+8+C4G5fz", + "uc3Ugq0e76UHu9njvfTp/u5+ke/spU/3DmZF+ng2e/x09mSW7dGdRwc7B1mxR/P9/d3He4/SnScH2WP6", + "5Omj2cHTbqvdA+fJ1suDSJETh8CgEGRzvOUCFNZ2vOXyOW+v6NHAGZFjX7/m1BrupozibVTLAJc9Uk0y", + "bwUhJ1KEm0zIsSCS2/Tcx0+6CTY8LLfvkmpyaRM7++BNexxy/PxNMiJpbZD1TDdQbD7vXSpFLFwx4ML7", + "yLHm9XyqMxAwtto3xRrT+Pj5RS+V75Tei8yW2QHi/oJxOK0guzM3QOCjPps2a1MLd6hNFWSsYF4dXE3E", + "+QuvGJ6k2khF5zBMD6qoeHwnO3gWSgixwTgaBi5oBMO+XocwozCcUK8DsQToG4RIAjF0e42SjJJqOwL/", + "xMyii1W2DNvXWT9w+Vu5qQCMd1Xr5wnj1sHqILSqxZWQS+GCXi5pjoGQlYGe++9ojsB+xL1clfRHjI5+", + "s/V21rrHr40G+YEs7yexsp/ARmyW2T6/dCWFhnhPBLlVKFkSSlTwGvFR8ChkJcpZY7YbCwLq2hrvFw6U", + "q5BSBcQJmvUFfpn9Dt5lvM4hxw0tDOWx+5Qy0EWHrT48jFiEG7Xqds+yEpil3ys12GbsG441Fff8/1hf", + "cs9mLnaCsEASLZ92YV3XbbPi2VSD1iSwDFL/h0um/YO9D/9B/vbnD3/58NcP//XhL3/784f//vDXD/8Z", + "NpsPH836pVi/y3lW5slh8t7/eeMCh1pcnaMQ7tkzGUUzc07rnMkmEbfM8wHoVLk3p7qYXspUYyC0s7s3", + "cSDDAsvJD9/YPyudHFolKhQtLXuTnfGOVTBW0jnoc6nOr1kO0rpf900ySmRtqtpgqR/eGRBYRUsmlfM/", + "iME5ruqjhJu0SAUyrpll1dgffIyvJAPdCvl4RwWqrfZsO8HQ9qoscyLjDAG77ip+NUuDXtrteZbPE/2M", + "QYtVTDeCgYmPqLK09ZS2AGLTyq7eEqme+MpLLI6wOLx2dbaIH2qfEdfOE4akK0J94drqKFbosCOMJuhN", + "PZvtPiZczr05crM0zHyhffnbd57XShVBJaKPwysBY86Eb76K3IbNQJYLaiFmbRNt4bpdTMxbr+g2npBX", + "16CW1jZoUim4ZrLWfIVnaTZt636x2JbLeSzYnhOLVNDst7tZl8y5S2d8780i7UjhNgSqOMOK/7Be0ZOF", + "bcdsYmU/5A6WhzZVtH5HcQcyBSb+6HcWadadCu7Uq69EtwjqM2830uOUzcWrj6VEU68539xfuPdjB7Wm", + "DacdYHXLqQ018GxBxRwiZU0nROedofioolw0BAiAbYVUvgmre8DlDgz6RlcbqgwmXXRJr1ylT3OAygYf", + "rvJmU+Xa5JikGdB+tSwKawkithWVxdXuTi3WeLylQ+Cc1rEk/rUGZXlvza01YbiYHD8fkYpqvZQqbx6h", + "duDQGKGmWaoCtbd2xtHLFZqpZllneBbGVMmNxZGJQmLPTxiama7N1rbjyBlQq3y14v5NfTidFk14xuR0", + "2F35Eac5XlBVktKXqo5OjpNRwlkGPuvx+3xz8t313gD+crmczEVto7Wpf0dP5xUf701mExCThSmx7cEM", + "72Hrt0uCrmCyM5lNZna1rEDQitnQzn2FVQbHmSmt2DRb7wTM0dhZCXXfHduo7hsw/ZaBlT/Mlxyo3dms", + "ISkI9z6tKu4rPtNLjaBRlu+S9GiLwnGuT3Fho0He5m0of3VZUrVCjLEUFIJp55SClruhNi762YVnydse", + "jK9FXkkmjHN6cz94MwDY8qEFejNC2too1um71BGaYqKAzUtvRf4o89W90bHfqR3Sz01ySJ+CJKFBsZH5", + "zQNy+BaEllQTXWcZ6KLmfEVwjtAN/flw6JrlNeU4ejhZG+a8F+yw7xDBzz0gTVuhL25IbEKJgKUb9rAi", + "sy4ZwUhEKHlOTPqS97IZGcMJSPCC2Betaet5N+lsO5fygMwcDsFESNcu6gZhIvrKB8Mybo7EFXz6s0S3", + "kK7bqiX/ZTeh3KPf+0uZnrP8ZiMJX4DJFqih4SjKz+8TZk/lR8m8VUdgA0UaBXS8qxf39u+jdM4j9tnh", + "Tu4eEJriLKfj3RZyiy+J3Pul0mLekD0IKzfJ7J/agZUHI8X62M1vdi6thDX9nTX/crt7ecaZq29mVJBa", + "+16WkVifxr+YtklbTa0ppN12vtDZkhVjoanyHfLxsmuQR11P00r3jfSH8T+RtCxC6C61brD/pK5oMFSw", + "jSx8Qp9TC3hXQWYgJ+DXhCLUoO8dz7LhZyN1/ou3kZe6AKZ7U69LlGZzMZZFcUsUY9PMohiq6/4w2v/8", + "COnTFWfSe4nKz2+tMe5o9j1VV2GGQjVpEqE7qP2Mcj86hRLmVJx7A9IEBlfCzZDD6gsFZC7xbo0DP4mz", + "RNzBEfGgSu232KzOba3zU+rysALwD6HMW8vgUW0WIAwWBH3Z0UpD0/tYtuO19yyQCmi+sqssPBxy6ZVC", + "WcfwobgaX2mN+vuAZcnfWzIcpiRzz0lX1rkZbTJmZPMbn7dIfbx4YEiy7Ob+FODFlNUGIsTlYJwFRbCo", + "8YoUzB7UkIUbRcj7Q+sa8Zxb2LP/X37P23PPNyTChJy5qSs3h5W6yyw0swaDQ47xPjZCvC3pGjM9WRkR", + "qazlaqjS2BdQYy4zyp1po1zftz27ht5paj0QVeMvTW9wr9kC8prDGU48PlxeHV7hjjDWXd4OCwqbDNUP", + "0t/T7F+5cvlFcyPjZpTsz/bur/TUG+GMIH8CqqltPAfB0Gjuz55GZo1RAJkmQprG02HHEMVpRLRsHrvr", + "rtC7eoJHd11yIuQSj7q792ldS6NFVFgsZWooEy7sdtjhqKC7ITaX7taukM7OorZ9pMa+Qui0hR9Q4y5V", + "cjKlvYCrSNkp0JDpe9ej8eWTuK4EvdZtKige4O8vody/uwhOskkXfTzEBKLY1DA+2lucLaCBtXSmNYOq", + "8ahRFTnzvV/nkb3VCMUImeb0xPRhO50J4f+juKXXXRse+9BmVbHMlUnCrnml5FyB1iN/t8VfVlakoIzX", + "Cu70LY1H0SDyXjXMkruBbq2YjYhQTfAiw7QZQZ3ioPIt/qQ/iv5AvYD+JhGG9GYm24hP1mby26S42cvJ", + "mb/hFFb2Q3F+WFFrMaEcExn38wXae4L9h0fgzIXLS/sfkte5PjGfkNcayIXu8SYcmLywjMAJdeJIuWDZ", + "gkgBevI5FaGe4Sh+cD8bc0S9KjkTV366EiXIUwC7OcZGki1RrP+jnJMFvQb8LQocVURj5gf7UijcVTXK", + "efuLFp2b6rQZibqmzaceIUp0KO0Omd4NDaqAxrU5HEzdVqdDlj6ofseGo7dV9U9aqbllNjiGb516flkm", + "WYpD3psQHjUWH0UCiB+mxSN+XrriZs8JbeQ5pIFDt/kBlUoqo73GI6eoag92p6Qf2UDYbpO5HkOYwvcB", + "djmBH6XG1gJi0dkb/JUBwzjvUAjUw8Gbvm8G62+m79037Fe4QeWwxNGb9ATnbKWCZ14Q1yLFra9quB8B", + "GoaVzdJb48rBLMvwp49+hfW7Ju2lgciuDQW22bW7HfL2wbVuMFsdK3v0CxafnwaFA5jdDHj0NgBenBoq", + "y22Wu5XIf25hHMUyDW9Rmhjb37PwF9tyKECR9ooB+mdHDefp3yS7sydvkq7m48ZHXU4s+IqkNk4wtbL5", + "i/v5m+54uo3ecC6kvdMxYDhm05RriTC0LEEKIMC1g9ON0MbQdNLiCLgAmrtemifhv49xm/EzKsbP7TnH", + "rx2AJELD4JZzjIZSsTkTlLs9LfwJOS78jC6X4Uxve/eFmXbWlgl/d4WFJtuN3Y68zba8oMytyCGt8WLn", + "Fmd75REbv/CIJbeJ5da5tswMmLE2CmjZtxBtOp8yYfV7mNAP43ncQ69d0vuNmbYTr0GevTt7ctdyL449", + "QQz68vs7B1EIyr9uk4CSmmxBUjBL8MLuyRlMuzQjMH4OABFwl9akGtidNmBuZNmlOI8i2Rkqsb9Le4fW", + "NhrYaY4XvErJDLRjRAr2xXb/dNXTOwwnLjaq0CGxPLvA6S+0LiE5/Ek+Fw/kPIMvsG32O+QH6SoU1Awf", + "Ov0spMpYylck41JjLePbs7MTkkkhwP2qAxqwpozjDW/BBNML0D1+AYF3NDNE0xJ8GGmkm/+3r+SythEe", + "vqAnb0TD1S/cHWjUJi8LKcQ4QFKZrza60rAuY7foUoshWXyhx35Gh4pDsNMkaEwNfo6vP4Y0GKtjRgMv", + "Jp09c8M2Q9P7UqZN39QVcH6pQTHQo2DUbrQ2uTTpzXfpCNCjk+P+sF/YNpNlWQt/g8Oa9OGsaAve158i", + "vh7pd3RyPHIbOZHrmO8P5Eos9u9LmbaJrA7ge37dvL35vwAAAP//hZ9HEDhWAAA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/api/openapi_types.gen.go b/pkg/api/openapi_types.gen.go index bbfe7e6f..227485c0 100644 --- a/pkg/api/openapi_types.gen.go +++ b/pkg/api/openapi_types.gen.go @@ -225,6 +225,12 @@ type JobSettings struct { // JobStatus defines model for JobStatus. type JobStatus string +// ManagerConfiguration defines model for ManagerConfiguration. +type ManagerConfiguration struct { + // Whether the Shaman file transfer API is available. + ShamanEnabled bool `json:"shamanEnabled"` +} + // RegisteredWorker defines model for RegisteredWorker. type RegisteredWorker struct { Address string `json:"address"`