diff --git a/addon/flamenco/__init__.py b/addon/flamenco/__init__.py index 3898b036..3a2f2296 100644 --- a/addon/flamenco/__init__.py +++ b/addon/flamenco/__init__.py @@ -27,6 +27,7 @@ if __is_first_load: preferences, projects, worker_tags, + manager_info, ) else: import importlib @@ -38,6 +39,7 @@ else: preferences = importlib.reload(preferences) projects = importlib.reload(projects) worker_tags = importlib.reload(worker_tags) + manager_info = importlib.reload(manager_info) import bpy @@ -160,6 +162,9 @@ def register() -> None: gui.register() job_types.register() + # Once everything is registered, load the cached manager info from JSON. + manager_info.load_into_cache() + def unregister() -> None: discard_global_flamenco_data(None) diff --git a/addon/flamenco/comms.py b/addon/flamenco/comms.py index 4c303391..15eabe05 100644 --- a/addon/flamenco/comms.py +++ b/addon/flamenco/comms.py @@ -3,13 +3,12 @@ # import logging -import dataclasses -import platform -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING -from urllib3.exceptions import HTTPError, MaxRetryError import bpy +from flamenco import manager_info, job_types + _flamenco_client = None _log = logging.getLogger(__name__) @@ -27,23 +26,6 @@ else: _SharedStorageLocation = object -@dataclasses.dataclass(frozen=True) -class ManagerInfo: - version: Optional[_FlamencoVersion] = None - storage: Optional[_SharedStorageLocation] = None - error: str = "" - - @classmethod - def with_error(cls, error: str) -> "ManagerInfo": - return cls(error=error) - - @classmethod - def with_info( - cls, version: _FlamencoVersion, storage: _SharedStorageLocation - ) -> "ManagerInfo": - return cls(version=version, storage=storage) - - def flamenco_api_client(manager_url: str) -> _ApiClient: """Returns an API client for communicating with a Manager.""" global _flamenco_client @@ -87,12 +69,12 @@ def discard_flamenco_data(): _flamenco_client = None -def ping_manager_with_report( +def ping_manager( window_manager: bpy.types.WindowManager, + scene: bpy.types.Scene, api_client: _ApiClient, - prefs: _FlamencoPreferences, ) -> tuple[str, str]: - """Ping the Manager, update preferences, and return a report as string. + """Fetch Manager info, and update the scene for it. :returns: tuple (report, level). The report will be something like " version found", or an error message. The level will be @@ -100,55 +82,49 @@ def ping_manager_with_report( `Operator.report()`. """ - info = ping_manager(window_manager, api_client, prefs) - if info.error: - return info.error, "ERROR" - - assert info.version is not None - report = "%s version %s found" % (info.version.name, info.version.version) - return report, "INFO" - - -def ping_manager( - window_manager: bpy.types.WindowManager, - api_client: _ApiClient, - prefs: _FlamencoPreferences, -) -> ManagerInfo: - """Fetch Manager config & version, and update cached preferences.""" - window_manager.flamenco_status_ping = "..." - # Do a late import, so that the API is only imported when actually used. - from flamenco.manager import ApiException - from flamenco.manager.apis import MetaApi - from flamenco.manager.models import FlamencoVersion, SharedStorageLocation + # Remember the old values, as they may have disappeared from the Manager. + old_job_type_name = getattr(scene, "flamenco_job_type", "") + old_tag_name = getattr(scene, "flamenco_worker_tag", "") - meta_api = MetaApi(api_client) - error = "" try: - version: FlamencoVersion = meta_api.get_version() - storage: SharedStorageLocation = meta_api.get_shared_storage( - "users", platform.system().lower() - ) - except ApiException as ex: - error = "Manager cannot be reached: %s" % ex - except MaxRetryError as ex: - # This is the common error, when for example the port number is - # incorrect and nothing is listening. The exception text is not included - # because it's very long and confusing. - error = "Manager cannot be reached" - except HTTPError as ex: - error = "Manager cannot be reached: %s" % ex + info = manager_info.fetch(api_client) + except manager_info.FetchError as ex: + report = str(ex) + window_manager.flamenco_status_ping = report + return report, "ERROR" - if error: - window_manager.flamenco_status_ping = error - return ManagerInfo.with_error(error) + manager_info.save(info) - # Store whether this Manager supports the Shaman API. - prefs.is_shaman_enabled = storage.shaman_enabled - prefs.job_storage = storage.location + report = "%s version %s found" % ( + info.flamenco_version.name, + info.flamenco_version.version, + ) + report_level = "INFO" + + job_types.refresh_scene_properties(scene, info.job_types) + + # Try to restore the old values. + # + # Since you cannot un-set an enum property, and 'empty string' is not a + # valid value either, when the old choice is no longer available we remove + # the underlying ID property. + if old_job_type_name: + try: + scene.flamenco_job_type = old_job_type_name + except TypeError: # Thrown when the old enum value no longer exists. + del scene["flamenco_job_type"] + report = f"Job type {old_job_type_name!r} no longer available, choose another one" + report_level = "WARNING" + + if old_tag_name: + try: + scene.flamenco_worker_tag = old_tag_name + except TypeError: # Thrown when the old enum value no longer exists. + del scene["flamenco_worker_tag"] + report = f"Tag {old_tag_name!r} no longer available, choose another one" + report_level = "WARNING" - report = "%s version %s found" % (version.name, version.version) window_manager.flamenco_status_ping = report - - return ManagerInfo.with_info(version, storage) + return report, report_level diff --git a/addon/flamenco/gui.py b/addon/flamenco/gui.py index b0c2a7ff..f1850021 100644 --- a/addon/flamenco/gui.py +++ b/addon/flamenco/gui.py @@ -43,23 +43,19 @@ class FLAMENCO_PT_job_submission(bpy.types.Panel): col.prop(context.scene, "flamenco_job_name", text="Job Name") col.prop(context.scene, "flamenco_job_priority", text="Priority") - # Worker tag: - row = col.row(align=True) - row.prop(context.scene, "flamenco_worker_tag", text="Tag") - row.operator("flamenco.fetch_worker_tags", text="", icon="FILE_REFRESH") - - layout.separator() - - col = layout.column() + # Refreshables: + col = layout.column(align=True) + col.operator( + "flamenco.ping_manager", text="Refresh from Manager", icon="FILE_REFRESH" + ) if not job_types.are_job_types_available(): - col.operator("flamenco.fetch_job_types", icon="FILE_REFRESH") return + col.prop(context.scene, "flamenco_worker_tag", text="Tag") - row = col.row(align=True) - row.prop(context.scene, "flamenco_job_type", text="") - row.operator("flamenco.fetch_job_types", text="", icon="FILE_REFRESH") - - self.draw_job_settings(context, layout.column(align=True)) + # Job properties: + job_col = layout.column(align=True) + job_col.prop(context.scene, "flamenco_job_type", text="Job Type") + self.draw_job_settings(context, job_col) layout.separator() diff --git a/addon/flamenco/job_submission.py b/addon/flamenco/job_submission.py index dad16a9f..21273399 100644 --- a/addon/flamenco/job_submission.py +++ b/addon/flamenco/job_submission.py @@ -8,7 +8,7 @@ import bpy from .job_types_propgroup import JobTypePropertyGroup from .bat.submodules import bpathlib -from . import preferences +from . import manager_info if TYPE_CHECKING: from .manager import ApiClient as _ApiClient @@ -133,8 +133,11 @@ def is_file_inside_job_storage(context: bpy.types.Context, blendfile: Path) -> b blendfile = bpathlib.make_absolute(blendfile) - prefs = preferences.get(context) - job_storage = bpathlib.make_absolute(Path(prefs.job_storage)) + info = manager_info.load_cached() + if not info: + raise RuntimeError("Flamenco Manager info unknown, please refresh.") + + job_storage = bpathlib.make_absolute(Path(info.shared_storage.location)) log.info("Checking whether the file is already inside the job storage") log.info(" file : %s", blendfile) diff --git a/addon/flamenco/job_types.py b/addon/flamenco/job_types.py index 8150a953..233c149c 100644 --- a/addon/flamenco/job_types.py +++ b/addon/flamenco/job_types.py @@ -1,14 +1,10 @@ # SPDX-License-Identifier: GPL-3.0-or-later -import json -import logging from typing import TYPE_CHECKING, Optional, Union import bpy -from . import job_types_propgroup - -_log = logging.getLogger(__name__) +from . import job_types_propgroup, manager_info if TYPE_CHECKING: from flamenco.manager import ApiClient as _ApiClient @@ -39,24 +35,12 @@ _selected_job_type_propgroup: Optional[ ] = None -def fetch_available_job_types(api_client: _ApiClient, scene: bpy.types.Scene) -> None: - from flamenco.manager import ApiClient - from flamenco.manager.api import jobs_api - from flamenco.manager.model.available_job_types import AvailableJobTypes - - assert isinstance(api_client, ApiClient) - - job_api_instance = jobs_api.JobsApi(api_client) - response: AvailableJobTypes = job_api_instance.get_job_types() - +def refresh_scene_properties( + scene: bpy.types.Scene, available_job_types: _AvailableJobTypes +) -> None: _clear_available_job_types(scene) - - # Store the response JSON on the scene. This is used when the blend file is - # loaded (and thus the _available_job_types global variable is still empty) - # to generate the PropertyGroup of the selected job type. - scene.flamenco_available_job_types_json = json.dumps(response.to_dict()) - - _store_available_job_types(response) + _store_available_job_types(available_job_types) + update_job_type_properties(scene) def setting_is_visible(setting: _AvailableJobSetting) -> bool: @@ -125,33 +109,6 @@ def _store_available_job_types(available_job_types: _AvailableJobTypes) -> None: _job_type_enum_items.insert(0, ("", "Select a Job Type", "", 0, 0)) -def _available_job_types_from_json(job_types_json: str) -> None: - """Convert JSON to AvailableJobTypes object, and update global variables for it.""" - from flamenco.manager.models import AvailableJobTypes - from flamenco.manager.configuration import Configuration - from flamenco.manager.model_utils import validate_and_convert_types - - json_dict = json.loads(job_types_json) - - dummy_cfg = Configuration() - - try: - job_types = validate_and_convert_types( - json_dict, (AvailableJobTypes,), ["job_types"], True, True, dummy_cfg - ) - except TypeError: - _log.warn( - "Flamenco: could not restore cached job types, refresh them from Flamenco Manager" - ) - _store_available_job_types(AvailableJobTypes(job_types=[])) - return - - assert isinstance( - job_types, AvailableJobTypes - ), "expected AvailableJobTypes, got %s" % type(job_types) - _store_available_job_types(job_types) - - def are_job_types_available() -> bool: """Returns whether job types have been fetched and are available.""" return bool(_job_type_enum_items) @@ -199,7 +156,7 @@ def _clear_available_job_types(scene: bpy.types.Scene) -> None: _clear_job_type_propgroup() _available_job_types = None - _job_type_enum_items.clear() + _job_type_enum_items = [] scene.flamenco_available_job_types_json = "" @@ -238,21 +195,21 @@ def _get_job_types_enum_items(dummy1, dummy2): @bpy.app.handlers.persistent -def restore_available_job_types(dummy1, dummy2): +def restore_available_job_types(_filepath, _none): scene = bpy.context.scene - job_types_json = getattr(scene, "flamenco_available_job_types_json", "") - if not job_types_json: + info = manager_info.load_cached() + if info is None: _clear_available_job_types(scene) return - _available_job_types_from_json(job_types_json) - update_job_type_properties(scene) + refresh_scene_properties(scene, info.job_types) def discard_flamenco_data(): - if _available_job_types: - _available_job_types.clear() - if _job_type_enum_items: - _job_type_enum_items.clear() + global _available_job_types + global _job_type_enum_items + + _available_job_types = None + _job_type_enum_items = [] def register() -> None: diff --git a/addon/flamenco/manager_info.py b/addon/flamenco/manager_info.py new file mode 100644 index 00000000..aa17d797 --- /dev/null +++ b/addon/flamenco/manager_info.py @@ -0,0 +1,210 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import dataclasses +import json +import platform +from pathlib import Path +from typing import TYPE_CHECKING, Optional + +from urllib3.exceptions import HTTPError, MaxRetryError + +import bpy + +if TYPE_CHECKING: + from flamenco.manager import ApiClient as _ApiClient + from flamenco.manager.models import ( + AvailableJobTypes as _AvailableJobTypes, + FlamencoVersion as _FlamencoVersion, + SharedStorageLocation as _SharedStorageLocation, + WorkerTagList as _WorkerTagList, + ) +else: + _ApiClient = object + _AvailableJobTypes = object + _FlamencoVersion = object + _SharedStorageLocation = object + _WorkerTagList = object + + +@dataclasses.dataclass +class ManagerInfo: + """Cached information obtained from a Flamenco Manager. + + This is the root object of what is stored on disk, every time someone + presses a 'refresh' button to update worker tags, job types, etc. + """ + + flamenco_version: _FlamencoVersion + shared_storage: _SharedStorageLocation + job_types: _AvailableJobTypes + worker_tags: _WorkerTagList + + @staticmethod + def type_info() -> dict[str, type]: + # Do a late import, so that the API is only imported when actually used. + from flamenco.manager.models import ( + AvailableJobTypes, + FlamencoVersion, + SharedStorageLocation, + WorkerTagList, + ) + + # These types cannot be obtained by introspecting the ManagerInfo class, as + # at runtime that doesn't use real type annotations. + return { + "flamenco_version": FlamencoVersion, + "shared_storage": SharedStorageLocation, + "job_types": AvailableJobTypes, + "worker_tags": WorkerTagList, + } + + +class FetchError(RuntimeError): + """Raised when the manager info could not be fetched from the Manager.""" + + +class LoadError(RuntimeError): + """Raised when the manager info could not be loaded from disk cache.""" + + +_cached_manager_info: Optional[ManagerInfo] = None + + +def fetch(api_client: _ApiClient) -> ManagerInfo: + global _cached_manager_info + + # Do a late import, so that the API is only imported when actually used. + from flamenco.manager import ApiException + from flamenco.manager.apis import MetaApi, JobsApi, WorkerMgtApi + from flamenco.manager.models import ( + AvailableJobTypes, + FlamencoVersion, + SharedStorageLocation, + WorkerTagList, + ) + + meta_api = MetaApi(api_client) + jobs_api = JobsApi(api_client) + worker_mgt_api = WorkerMgtApi(api_client) + + try: + flamenco_version: FlamencoVersion = meta_api.get_version() + shared_storage: SharedStorageLocation = meta_api.get_shared_storage( + "users", platform.system().lower() + ) + job_types: AvailableJobTypes = jobs_api.get_job_types() + worker_tags: WorkerTagList = worker_mgt_api.fetch_worker_tags() + except ApiException as ex: + raise FetchError("Manager cannot be reached: %s" % ex) from ex + except MaxRetryError as ex: + # This is the common error, when for example the port number is + # incorrect and nothing is listening. The exception text is not included + # because it's very long and confusing. + raise FetchError("Manager cannot be reached") from ex + except HTTPError as ex: + raise FetchError("Manager cannot be reached: %s" % ex) from ex + + _cached_manager_info = ManagerInfo( + flamenco_version=flamenco_version, + shared_storage=shared_storage, + job_types=job_types, + worker_tags=worker_tags, + ) + return _cached_manager_info + + +class Encoder(json.JSONEncoder): + def default(self, o): + from flamenco.manager.model_utils import OpenApiModel + + if isinstance(o, OpenApiModel): + return o.to_dict() + + if isinstance(o, ManagerInfo): + # dataclasses.asdict() creates a copy of the OpenAPI models, + # in a way that just doesn't work, hence this workaround. + return {f.name: getattr(o, f.name) for f in dataclasses.fields(o)} + + return super().default(o) + + +def _to_json(info: ManagerInfo) -> str: + return json.dumps(info, indent=" ", cls=Encoder) + + +def _from_json(contents: str | bytes) -> ManagerInfo: + # Do a late import, so that the API is only imported when actually used. + from flamenco.manager.configuration import Configuration + from flamenco.manager.model_utils import validate_and_convert_types + + json_dict = json.loads(contents) + dummy_cfg = Configuration() + api_models = {} + + for name, api_type in ManagerInfo.type_info().items(): + api_model = validate_and_convert_types( + json_dict[name], + (api_type,), + [name], + True, + True, + dummy_cfg, + ) + api_models[name] = api_model + + return ManagerInfo(**api_models) + + +def _json_filepath() -> Path: + # This is the '~/.config/blender/{version}' path. + user_path = Path(bpy.utils.resource_path(type="USER")) + return user_path / "config" / "flamenco-manager-info.json" + + +def save(info: ManagerInfo) -> None: + json_path = _json_filepath() + json_path.parent.mkdir(parents=True, exist_ok=True) + + as_json = _to_json(info) + json_path.write_text(as_json, encoding="utf8") + + +def load() -> ManagerInfo: + json_path = _json_filepath() + if not json_path.exists(): + raise FileNotFoundError(f"{json_path.name} not found in {json_path.parent}") + + try: + as_json = json_path.read_text(encoding="utf8") + except OSError as ex: + raise LoadError(f"Could not read {json_path}: {ex}") from ex + + try: + return _from_json(as_json) + except json.JSONDecodeError as ex: + raise LoadError(f"Could not decode JSON in {json_path}") from ex + + +def load_into_cache() -> Optional[ManagerInfo]: + global _cached_manager_info + + _cached_manager_info = None + try: + _cached_manager_info = load() + except FileNotFoundError: + return None + except LoadError as ex: + print(f"Could not load Flamenco Manager info from disk: {ex}") + return None + + return _cached_manager_info + + +def load_cached() -> Optional[ManagerInfo]: + global _cached_manager_info + + if _cached_manager_info is not None: + return _cached_manager_info + + return load_into_cache() diff --git a/addon/flamenco/operators.py b/addon/flamenco/operators.py index 0d94effc..c8f1eea1 100644 --- a/addon/flamenco/operators.py +++ b/addon/flamenco/operators.py @@ -10,7 +10,7 @@ from urllib3.exceptions import HTTPError, MaxRetryError import bpy -from . import job_types, job_submission, preferences, worker_tags +from . import job_types, job_submission, preferences, manager_info from .job_types_propgroup import JobTypePropertyGroup from .bat.submodules import bpathlib @@ -51,80 +51,6 @@ class FlamencoOpMixin: return api_client -class FLAMENCO_OT_fetch_job_types(FlamencoOpMixin, bpy.types.Operator): - bl_idname = "flamenco.fetch_job_types" - bl_label = "Fetch Job Types" - bl_description = "Query Flamenco Manager to obtain the available job types" - - def execute(self, context: bpy.types.Context) -> set[str]: - api_client = self.get_api_client(context) - - from flamenco.manager import ApiException - - scene = context.scene - old_job_type_name = getattr(scene, "flamenco_job_type", "") - - try: - job_types.fetch_available_job_types(api_client, scene) - except ApiException as ex: - self.report({"ERROR"}, "Error getting job types: %s" % ex) - return {"CANCELLED"} - except MaxRetryError as ex: - # This is the common error, when for example the port number is - # incorrect and nothing is listening. - self.report({"ERROR"}, "Unable to reach Manager") - return {"CANCELLED"} - - if old_job_type_name: - try: - scene.flamenco_job_type = old_job_type_name - except TypeError: # Thrown when the old job type no longer exists. - # You cannot un-set an enum property, and 'empty string' is not - # a valid value either, so better to just remove the underlying - # ID property. - del scene["flamenco_job_type"] - - self.report( - {"WARNING"}, - "Job type %r no longer available, choose another one" - % old_job_type_name, - ) - - job_types.update_job_type_properties(scene) - return {"FINISHED"} - - -class FLAMENCO_OT_fetch_worker_tags(FlamencoOpMixin, bpy.types.Operator): - bl_idname = "flamenco.fetch_worker_tags" - bl_label = "Fetch Worker Tags" - bl_description = "Query Flamenco Manager to obtain the available worker tags" - - def execute(self, context: bpy.types.Context) -> set[str]: - api_client = self.get_api_client(context) - - from flamenco.manager import ApiException - - scene = context.scene - old_tag = getattr(scene, "flamenco_worker_tag", "") - - try: - worker_tags.refresh(context, api_client) - except ApiException as ex: - self.report({"ERROR"}, "Error getting job types: %s" % ex) - return {"CANCELLED"} - except MaxRetryError as ex: - # This is the common error, when for example the port number is - # incorrect and nothing is listening. - self.report({"ERROR"}, "Unable to reach Manager") - return {"CANCELLED"} - - if old_tag: - # TODO: handle cases where the old tag no longer exists. - scene.flamenco_worker_tag = old_tag - - return {"FINISHED"} - - class FLAMENCO_OT_ping_manager(FlamencoOpMixin, bpy.types.Operator): bl_idname = "flamenco.ping_manager" bl_label = "Flamenco: Ping Manager" @@ -132,13 +58,13 @@ class FLAMENCO_OT_ping_manager(FlamencoOpMixin, bpy.types.Operator): bl_options = {"REGISTER"} # No UNDO. def execute(self, context: bpy.types.Context) -> set[str]: - from . import comms, preferences + from . import comms api_client = self.get_api_client(context) - prefs = preferences.get(context) - - report, level = comms.ping_manager_with_report( - context.window_manager, api_client, prefs + report, level = comms.ping_manager( + context.window_manager, + context.scene, + api_client, ) self.report({level}, report) @@ -259,29 +185,31 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): :return: an error string when something went wrong. """ - from . import comms, preferences + from . import comms - # Get the manager's info. This is cached in the preferences, so - # regardless of whether this function actually responds to version - # mismatches, it has to be called to also refresh the shared storage - # location. + # Get the manager's info. This is cached to disk, so regardless of + # whether this function actually responds to version mismatches, it has + # to be called to also refresh the shared storage location. api_client = self.get_api_client(context) - prefs = preferences.get(context) - mgrinfo = comms.ping_manager(context.window_manager, api_client, prefs) - if mgrinfo.error: - return mgrinfo.error + + report, report_level = comms.ping_manager( + context.window_manager, + context.scene, + api_client, + ) + if report_level != "INFO": + return report # Check the Manager's version. if not self.ignore_version_mismatch: - my_version = comms.flamenco_client_version() - assert mgrinfo.version is not None + mgrinfo = manager_info.load_cached() + + # Safe to assume, as otherwise the ping_manager() call would not have succeeded. + assert mgrinfo is not None + + my_version = comms.flamenco_client_version() + mgrversion = mgrinfo.flamenco_version.shortversion - try: - mgrversion = mgrinfo.version.shortversion - except AttributeError: - # shortversion was introduced in Manager version 3.0-beta2, which - # may not be running here yet. - mgrversion = mgrinfo.version.version if mgrversion != my_version: context.window_manager.flamenco_version_mismatch = True return ( @@ -299,6 +227,23 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): # Empty error message indicates 'ok'. return "" + def _manager_info( + self, context: bpy.types.Context + ) -> Optional[manager_info.ManagerInfo]: + """Load the manager info. + + If it cannot be loaded, returns None after emitting an error message and + calling self._quit(context). + """ + manager = manager_info.load_cached() + if not manager: + self.report( + {"ERROR"}, "No information known about Flamenco Manager, refresh first." + ) + self._quit(context) + return None + return manager + def _save_blendfile(self, context): """Save to a different file, specifically for Flamenco. @@ -368,8 +313,11 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): self._quit(context) return {"CANCELLED"} - prefs = preferences.get(context) - if prefs.is_shaman_enabled: + manager = self._manager_info(context) + if not manager: + return {"CANCELLED"} + + if manager.shared_storage.shaman_enabled: # self.blendfile_on_farm will be set when BAT created the checkout, # see _on_bat_pack_msg() below. self.blendfile_on_farm = None @@ -414,11 +362,14 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator): raise FileNotFoundError() # Determine where the blend file will be stored. + manager = self._manager_info(context) + if not manager: + raise FileNotFoundError("Manager info not known") unique_dir = "%s-%s" % ( datetime.datetime.now().isoformat("-").replace(":", ""), self.job_name, ) - pack_target_dir = Path(prefs.job_storage) / unique_dir + pack_target_dir = Path(manager.shared_storage.location) / unique_dir # TODO: this should take the blendfile location relative to the project path into account. pack_target_file = pack_target_dir / blendfile.name @@ -690,8 +641,6 @@ class FLAMENCO3_OT_explore_file_path(bpy.types.Operator): classes = ( - FLAMENCO_OT_fetch_job_types, - FLAMENCO_OT_fetch_worker_tags, FLAMENCO_OT_ping_manager, FLAMENCO_OT_eval_setting, FLAMENCO_OT_submit_job, diff --git a/addon/flamenco/preferences.py b/addon/flamenco/preferences.py index 2b5daa27..46e0cc0e 100644 --- a/addon/flamenco/preferences.py +++ b/addon/flamenco/preferences.py @@ -5,7 +5,7 @@ from pathlib import Path import bpy -from . import projects +from . import projects, manager_info def discard_flamenco_client(context): @@ -16,9 +16,7 @@ def discard_flamenco_client(context): context.window_manager.flamenco_status_ping = "" -def _refresh_the_planet( - prefs: "FlamencoPreferences", context: bpy.types.Context -) -> None: +def _refresh_the_planet(context: bpy.types.Context) -> None: """Refresh all GUI areas.""" for win in context.window_manager.windows: for area in win.screen.areas: @@ -35,7 +33,8 @@ def _manager_url_updated(prefs, context): # Warning, be careful what of the context to access here. Accessing / # changing too much can cause crashes, infinite loops, etc. - comms.ping_manager_with_report(context.window_manager, api_client, prefs) + comms.ping_manager(context.window_manager, context.scene, api_client) + _refresh_the_planet(context) _project_finder_enum_items = [ @@ -66,22 +65,6 @@ class FlamencoPreferences(bpy.types.AddonPreferences): items=_project_finder_enum_items, ) - is_shaman_enabled: bpy.props.BoolProperty( # type: ignore - name="Shaman Enabled", - description="Whether this Manager has the Shaman protocol enabled", - default=False, - update=_refresh_the_planet, - ) - - # Property that should be editable from Python. It's not exposed to the GUI. - job_storage: bpy.props.StringProperty( # type: ignore - name="Job Storage Directory", - subtype="DIR_PATH", - default="", - options={"HIDDEN"}, - description="Directory where blend files are stored when submitting them to Flamenco. This value is determined by Flamenco Manager", - ) - # Property that gets its value from the above _job_storage, and cannot be # set. This makes it read-only in the GUI. job_storage_for_gui: bpy.props.StringProperty( # type: ignore @@ -90,14 +73,7 @@ class FlamencoPreferences(bpy.types.AddonPreferences): default="", options={"SKIP_SAVE"}, description="Directory where blend files are stored when submitting them to Flamenco. This value is determined by Flamenco Manager", - get=lambda prefs: prefs.job_storage, - ) - - worker_tags: bpy.props.CollectionProperty( # type: ignore - type=WorkerTag, - name="Worker Tags", - description="Cache for the worker tags available on the configured Manager", - options={"HIDDEN"}, + get=lambda prefs: prefs._job_storage(), ) def draw(self, context: bpy.types.Context) -> None: @@ -116,7 +92,9 @@ class FlamencoPreferences(bpy.types.AddonPreferences): split.label(text="") split.label(text=label) - if not self.job_storage: + manager = manager_info.load_cached() + + if not manager: text_row(col, "Press the refresh button before using Flamenco") if context.window_manager.flamenco_status_ping: @@ -126,7 +104,7 @@ class FlamencoPreferences(bpy.types.AddonPreferences): text_row(aligned, "Press the refresh button to check the connection") text_row(aligned, "and update the job storage location") - if self.is_shaman_enabled: + if manager and manager.shared_storage.shaman_enabled: text_row(col, "Shaman enabled") col.prop(self, "job_storage_for_gui", text="Job Storage") @@ -152,6 +130,12 @@ class FlamencoPreferences(bpy.types.AddonPreferences): blendfile = Path(bpy.data.filepath) return projects.for_blendfile(blendfile, self.project_finder) + def _job_storage(self) -> str: + info = manager_info.load_cached() + if not info: + return "Unknown, refresh first." + return str(info.shared_storage.location) + def get(context: bpy.types.Context) -> FlamencoPreferences: """Return the add-on preferences.""" diff --git a/addon/flamenco/worker_tags.py b/addon/flamenco/worker_tags.py index 2ed75cbb..c5d28e94 100644 --- a/addon/flamenco/worker_tags.py +++ b/addon/flamenco/worker_tags.py @@ -1,57 +1,35 @@ # SPDX-License-Identifier: GPL-3.0-or-later -from typing import TYPE_CHECKING, Union +from typing import Union import bpy -from . import preferences - -if TYPE_CHECKING: - from flamenco.manager import ApiClient as _ApiClient -else: - _ApiClient = object +from . import manager_info _enum_items: list[Union[tuple[str, str, str], tuple[str, str, str, int, int]]] = [] -def refresh(context: bpy.types.Context, api_client: _ApiClient) -> None: - """Fetch the available worker tags from the Manager.""" - from flamenco.manager import ApiClient - from flamenco.manager.api import worker_mgt_api - from flamenco.manager.model.worker_tag_list import WorkerTagList - - assert isinstance(api_client, ApiClient) - - api = worker_mgt_api.WorkerMgtApi(api_client) - response: WorkerTagList = api.fetch_worker_tags() - - # Store on the preferences, so a cached version persists until the next refresh. - prefs = preferences.get(context) - prefs.worker_tags.clear() - - for tag in response.tags: - rna_tag = prefs.worker_tags.add() - rna_tag.id = tag.id - rna_tag.name = tag.name - rna_tag.description = getattr(tag, "description", "") - - # Preferences have changed, so make sure that Blender saves them (assuming - # auto-save here). - context.preferences.is_dirty = True - - def _get_enum_items(self, context): global _enum_items - prefs = preferences.get(context) + + manager = manager_info.load_cached() + if manager is None: + _enum_items = [ + ( + "-", + "-tags unknown-", + "Refresh to load the available Worker tags from the Manager", + ), + ] + return _enum_items _enum_items = [ ("-", "All", "No specific tag assigned, any worker can handle this job"), ] - _enum_items.extend( - (tag.id, tag.name, tag.description) - for tag in prefs.worker_tags - ) + for tag in manager.worker_tags.tags: + _enum_items.append((tag.id, tag.name, getattr(tag, "description", ""))) + return _enum_items @@ -70,9 +48,3 @@ def unregister() -> None: delattr(ob, attr) except AttributeError: pass - - -if __name__ == "__main__": - import doctest - - print(doctest.testmod())