Add-on: cache manager info in a JSON file

Instead of storing the cached manager info in the Blender preferences,
store the info in a JSON file. The file is located in the user prefs
folder (`~/.config/blender/{version}/config` on Linux).

This also reduces the number of 'refresh' operators to a single one, which
then fetches all necessary info from the Manager.

This fixes an issue (reported via chat) where worker tags were sometimes
not retained across file saves.
This commit is contained in:
Sybren A. Stüvel 2024-03-04 13:08:53 +01:00
parent a4e5eef83e
commit 3b4da656c9
9 changed files with 373 additions and 321 deletions

View File

@ -27,6 +27,7 @@ if __is_first_load:
preferences, preferences,
projects, projects,
worker_tags, worker_tags,
manager_info,
) )
else: else:
import importlib import importlib
@ -38,6 +39,7 @@ else:
preferences = importlib.reload(preferences) preferences = importlib.reload(preferences)
projects = importlib.reload(projects) projects = importlib.reload(projects)
worker_tags = importlib.reload(worker_tags) worker_tags = importlib.reload(worker_tags)
manager_info = importlib.reload(manager_info)
import bpy import bpy
@ -160,6 +162,9 @@ def register() -> None:
gui.register() gui.register()
job_types.register() job_types.register()
# Once everything is registered, load the cached manager info from JSON.
manager_info.load_into_cache()
def unregister() -> None: def unregister() -> None:
discard_global_flamenco_data(None) discard_global_flamenco_data(None)

View File

@ -3,13 +3,12 @@
# <pep8 compliant> # <pep8 compliant>
import logging import logging
import dataclasses from typing import TYPE_CHECKING
import platform
from typing import TYPE_CHECKING, Optional
from urllib3.exceptions import HTTPError, MaxRetryError
import bpy import bpy
from flamenco import manager_info, job_types
_flamenco_client = None _flamenco_client = None
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
@ -27,23 +26,6 @@ else:
_SharedStorageLocation = object _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: def flamenco_api_client(manager_url: str) -> _ApiClient:
"""Returns an API client for communicating with a Manager.""" """Returns an API client for communicating with a Manager."""
global _flamenco_client global _flamenco_client
@ -87,12 +69,12 @@ def discard_flamenco_data():
_flamenco_client = None _flamenco_client = None
def ping_manager_with_report( def ping_manager(
window_manager: bpy.types.WindowManager, window_manager: bpy.types.WindowManager,
scene: bpy.types.Scene,
api_client: _ApiClient, api_client: _ApiClient,
prefs: _FlamencoPreferences,
) -> tuple[str, str]: ) -> 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 "<name> :returns: tuple (report, level). The report will be something like "<name>
version <version> found", or an error message. The level will be version <version> found", or an error message. The level will be
@ -100,55 +82,49 @@ def ping_manager_with_report(
`Operator.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 = "..." window_manager.flamenco_status_ping = "..."
# Do a late import, so that the API is only imported when actually used. # Remember the old values, as they may have disappeared from the Manager.
from flamenco.manager import ApiException old_job_type_name = getattr(scene, "flamenco_job_type", "")
from flamenco.manager.apis import MetaApi old_tag_name = getattr(scene, "flamenco_worker_tag", "")
from flamenco.manager.models import FlamencoVersion, SharedStorageLocation
meta_api = MetaApi(api_client)
error = ""
try: try:
version: FlamencoVersion = meta_api.get_version() info = manager_info.fetch(api_client)
storage: SharedStorageLocation = meta_api.get_shared_storage( except manager_info.FetchError as ex:
"users", platform.system().lower() report = str(ex)
)
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
if error:
window_manager.flamenco_status_ping = error
return ManagerInfo.with_error(error)
# 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" % (version.name, version.version)
window_manager.flamenco_status_ping = report window_manager.flamenco_status_ping = report
return report, "ERROR"
return ManagerInfo.with_info(version, storage) manager_info.save(info)
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"
window_manager.flamenco_status_ping = report
return report, report_level

View File

@ -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_name", text="Job Name")
col.prop(context.scene, "flamenco_job_priority", text="Priority") col.prop(context.scene, "flamenco_job_priority", text="Priority")
# Worker tag: # Refreshables:
row = col.row(align=True) col = layout.column(align=True)
row.prop(context.scene, "flamenco_worker_tag", text="Tag") col.operator(
row.operator("flamenco.fetch_worker_tags", text="", icon="FILE_REFRESH") "flamenco.ping_manager", text="Refresh from Manager", icon="FILE_REFRESH"
)
layout.separator()
col = layout.column()
if not job_types.are_job_types_available(): if not job_types.are_job_types_available():
col.operator("flamenco.fetch_job_types", icon="FILE_REFRESH")
return return
col.prop(context.scene, "flamenco_worker_tag", text="Tag")
row = col.row(align=True) # Job properties:
row.prop(context.scene, "flamenco_job_type", text="") job_col = layout.column(align=True)
row.operator("flamenco.fetch_job_types", text="", icon="FILE_REFRESH") job_col.prop(context.scene, "flamenco_job_type", text="Job Type")
self.draw_job_settings(context, job_col)
self.draw_job_settings(context, layout.column(align=True))
layout.separator() layout.separator()

View File

@ -8,7 +8,7 @@ import bpy
from .job_types_propgroup import JobTypePropertyGroup from .job_types_propgroup import JobTypePropertyGroup
from .bat.submodules import bpathlib from .bat.submodules import bpathlib
from . import preferences from . import manager_info
if TYPE_CHECKING: if TYPE_CHECKING:
from .manager import ApiClient as _ApiClient 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) blendfile = bpathlib.make_absolute(blendfile)
prefs = preferences.get(context) info = manager_info.load_cached()
job_storage = bpathlib.make_absolute(Path(prefs.job_storage)) 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("Checking whether the file is already inside the job storage")
log.info(" file : %s", blendfile) log.info(" file : %s", blendfile)

View File

@ -1,14 +1,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import json
import logging
from typing import TYPE_CHECKING, Optional, Union from typing import TYPE_CHECKING, Optional, Union
import bpy import bpy
from . import job_types_propgroup from . import job_types_propgroup, manager_info
_log = logging.getLogger(__name__)
if TYPE_CHECKING: if TYPE_CHECKING:
from flamenco.manager import ApiClient as _ApiClient from flamenco.manager import ApiClient as _ApiClient
@ -39,24 +35,12 @@ _selected_job_type_propgroup: Optional[
] = None ] = None
def fetch_available_job_types(api_client: _ApiClient, scene: bpy.types.Scene) -> None: def refresh_scene_properties(
from flamenco.manager import ApiClient scene: bpy.types.Scene, available_job_types: _AvailableJobTypes
from flamenco.manager.api import jobs_api ) -> None:
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()
_clear_available_job_types(scene) _clear_available_job_types(scene)
_store_available_job_types(available_job_types)
# Store the response JSON on the scene. This is used when the blend file is update_job_type_properties(scene)
# 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)
def setting_is_visible(setting: _AvailableJobSetting) -> bool: 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)) _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: def are_job_types_available() -> bool:
"""Returns whether job types have been fetched and are available.""" """Returns whether job types have been fetched and are available."""
return bool(_job_type_enum_items) return bool(_job_type_enum_items)
@ -199,7 +156,7 @@ def _clear_available_job_types(scene: bpy.types.Scene) -> None:
_clear_job_type_propgroup() _clear_job_type_propgroup()
_available_job_types = None _available_job_types = None
_job_type_enum_items.clear() _job_type_enum_items = []
scene.flamenco_available_job_types_json = "" scene.flamenco_available_job_types_json = ""
@ -238,21 +195,21 @@ def _get_job_types_enum_items(dummy1, dummy2):
@bpy.app.handlers.persistent @bpy.app.handlers.persistent
def restore_available_job_types(dummy1, dummy2): def restore_available_job_types(_filepath, _none):
scene = bpy.context.scene scene = bpy.context.scene
job_types_json = getattr(scene, "flamenco_available_job_types_json", "") info = manager_info.load_cached()
if not job_types_json: if info is None:
_clear_available_job_types(scene) _clear_available_job_types(scene)
return return
_available_job_types_from_json(job_types_json) refresh_scene_properties(scene, info.job_types)
update_job_type_properties(scene)
def discard_flamenco_data(): def discard_flamenco_data():
if _available_job_types: global _available_job_types
_available_job_types.clear() global _job_type_enum_items
if _job_type_enum_items:
_job_type_enum_items.clear() _available_job_types = None
_job_type_enum_items = []
def register() -> None: def register() -> None:

View File

@ -0,0 +1,210 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# <pep8 compliant>
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()

View File

@ -10,7 +10,7 @@ from urllib3.exceptions import HTTPError, MaxRetryError
import bpy 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 .job_types_propgroup import JobTypePropertyGroup
from .bat.submodules import bpathlib from .bat.submodules import bpathlib
@ -51,80 +51,6 @@ class FlamencoOpMixin:
return api_client 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): class FLAMENCO_OT_ping_manager(FlamencoOpMixin, bpy.types.Operator):
bl_idname = "flamenco.ping_manager" bl_idname = "flamenco.ping_manager"
bl_label = "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. bl_options = {"REGISTER"} # No UNDO.
def execute(self, context: bpy.types.Context) -> set[str]: def execute(self, context: bpy.types.Context) -> set[str]:
from . import comms, preferences from . import comms
api_client = self.get_api_client(context) api_client = self.get_api_client(context)
prefs = preferences.get(context) report, level = comms.ping_manager(
context.window_manager,
report, level = comms.ping_manager_with_report( context.scene,
context.window_manager, api_client, prefs api_client,
) )
self.report({level}, report) 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. :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 # Get the manager's info. This is cached to disk, so regardless of
# regardless of whether this function actually responds to version # whether this function actually responds to version mismatches, it has
# mismatches, it has to be called to also refresh the shared storage # to be called to also refresh the shared storage location.
# location.
api_client = self.get_api_client(context) api_client = self.get_api_client(context)
prefs = preferences.get(context)
mgrinfo = comms.ping_manager(context.window_manager, api_client, prefs) report, report_level = comms.ping_manager(
if mgrinfo.error: context.window_manager,
return mgrinfo.error context.scene,
api_client,
)
if report_level != "INFO":
return report
# Check the Manager's version. # Check the Manager's version.
if not self.ignore_version_mismatch: if not self.ignore_version_mismatch:
my_version = comms.flamenco_client_version() mgrinfo = manager_info.load_cached()
assert mgrinfo.version is not None
# 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: if mgrversion != my_version:
context.window_manager.flamenco_version_mismatch = True context.window_manager.flamenco_version_mismatch = True
return ( return (
@ -299,6 +227,23 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
# Empty error message indicates 'ok'. # Empty error message indicates 'ok'.
return "" 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): def _save_blendfile(self, context):
"""Save to a different file, specifically for Flamenco. """Save to a different file, specifically for Flamenco.
@ -368,8 +313,11 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
self._quit(context) self._quit(context)
return {"CANCELLED"} return {"CANCELLED"}
prefs = preferences.get(context) manager = self._manager_info(context)
if prefs.is_shaman_enabled: if not manager:
return {"CANCELLED"}
if manager.shared_storage.shaman_enabled:
# self.blendfile_on_farm will be set when BAT created the checkout, # self.blendfile_on_farm will be set when BAT created the checkout,
# see _on_bat_pack_msg() below. # see _on_bat_pack_msg() below.
self.blendfile_on_farm = None self.blendfile_on_farm = None
@ -414,11 +362,14 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
raise FileNotFoundError() raise FileNotFoundError()
# Determine where the blend file will be stored. # 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" % ( unique_dir = "%s-%s" % (
datetime.datetime.now().isoformat("-").replace(":", ""), datetime.datetime.now().isoformat("-").replace(":", ""),
self.job_name, 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. # TODO: this should take the blendfile location relative to the project path into account.
pack_target_file = pack_target_dir / blendfile.name pack_target_file = pack_target_dir / blendfile.name
@ -690,8 +641,6 @@ class FLAMENCO3_OT_explore_file_path(bpy.types.Operator):
classes = ( classes = (
FLAMENCO_OT_fetch_job_types,
FLAMENCO_OT_fetch_worker_tags,
FLAMENCO_OT_ping_manager, FLAMENCO_OT_ping_manager,
FLAMENCO_OT_eval_setting, FLAMENCO_OT_eval_setting,
FLAMENCO_OT_submit_job, FLAMENCO_OT_submit_job,

View File

@ -5,7 +5,7 @@ from pathlib import Path
import bpy import bpy
from . import projects from . import projects, manager_info
def discard_flamenco_client(context): def discard_flamenco_client(context):
@ -16,9 +16,7 @@ def discard_flamenco_client(context):
context.window_manager.flamenco_status_ping = "" context.window_manager.flamenco_status_ping = ""
def _refresh_the_planet( def _refresh_the_planet(context: bpy.types.Context) -> None:
prefs: "FlamencoPreferences", context: bpy.types.Context
) -> None:
"""Refresh all GUI areas.""" """Refresh all GUI areas."""
for win in context.window_manager.windows: for win in context.window_manager.windows:
for area in win.screen.areas: 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 / # Warning, be careful what of the context to access here. Accessing /
# changing too much can cause crashes, infinite loops, etc. # 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 = [ _project_finder_enum_items = [
@ -66,22 +65,6 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
items=_project_finder_enum_items, 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 # Property that gets its value from the above _job_storage, and cannot be
# set. This makes it read-only in the GUI. # set. This makes it read-only in the GUI.
job_storage_for_gui: bpy.props.StringProperty( # type: ignore job_storage_for_gui: bpy.props.StringProperty( # type: ignore
@ -90,14 +73,7 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
default="", default="",
options={"SKIP_SAVE"}, options={"SKIP_SAVE"},
description="Directory where blend files are stored when submitting them to Flamenco. This value is determined by Flamenco Manager", 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, 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"},
) )
def draw(self, context: bpy.types.Context) -> None: def draw(self, context: bpy.types.Context) -> None:
@ -116,7 +92,9 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
split.label(text="") split.label(text="")
split.label(text=label) 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") text_row(col, "Press the refresh button before using Flamenco")
if context.window_manager.flamenco_status_ping: 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, "Press the refresh button to check the connection")
text_row(aligned, "and update the job storage location") 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") text_row(col, "Shaman enabled")
col.prop(self, "job_storage_for_gui", text="Job Storage") col.prop(self, "job_storage_for_gui", text="Job Storage")
@ -152,6 +130,12 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
blendfile = Path(bpy.data.filepath) blendfile = Path(bpy.data.filepath)
return projects.for_blendfile(blendfile, self.project_finder) 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: def get(context: bpy.types.Context) -> FlamencoPreferences:
"""Return the add-on preferences.""" """Return the add-on preferences."""

View File

@ -1,57 +1,35 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from typing import TYPE_CHECKING, Union from typing import Union
import bpy import bpy
from . import preferences from . import manager_info
if TYPE_CHECKING:
from flamenco.manager import ApiClient as _ApiClient
else:
_ApiClient = object
_enum_items: list[Union[tuple[str, str, str], tuple[str, str, str, int, int]]] = [] _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): def _get_enum_items(self, context):
global _enum_items 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 = [ _enum_items = [
("-", "All", "No specific tag assigned, any worker can handle this job"), ("-", "All", "No specific tag assigned, any worker can handle this job"),
] ]
_enum_items.extend( for tag in manager.worker_tags.tags:
(tag.id, tag.name, tag.description) _enum_items.append((tag.id, tag.name, getattr(tag, "description", "")))
for tag in prefs.worker_tags
)
return _enum_items return _enum_items
@ -70,9 +48,3 @@ def unregister() -> None:
delattr(ob, attr) delattr(ob, attr)
except AttributeError: except AttributeError:
pass pass
if __name__ == "__main__":
import doctest
print(doctest.testmod())