Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cuesubmit jobs from config file #1284

Open
wants to merge 89 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
7ea1562
doc!: update cuesubmit example config file with new jobs syntax + hel…
KernAttila Mar 28, 2023
14a0385
doc!: add a cuesubmit isolated example config file
KernAttila Mar 28, 2023
d761fac
feat: add `notEmptyString` validator
KernAttila Mar 28, 2023
72adc3d
feat: extract yaml loading to a function and add one to expand any su…
KernAttila Mar 28, 2023
959613d
feat: load RENDER_CMDS in Constants
KernAttila Mar 28, 2023
f26d5ca
feat: add regex to get opencue tokens
KernAttila Mar 28, 2023
195091f
feat: add regex to parse options from the cuesubmit config file
KernAttila Mar 28, 2023
0d375d2
feat: add `convertCommandOptions()` to parse command options from the…
KernAttila Mar 28, 2023
da98b04
feat: Append jobs from the config file to `JobTypes`
KernAttila Mar 28, 2023
47ac2ef
feat: return JobTypes.types dynamically
KernAttila Mar 28, 2023
6feddc3
feat: Associate `DynamicSettingsWidget` to all jobs from the config file
KernAttila Mar 28, 2023
03c08b6
feat: Convert custom commands options and pass them to the `DynamicSe…
KernAttila Mar 28, 2023
2c2bf7a
test: add validator test `testNotEmptyString`
KernAttila Mar 28, 2023
eef22bd
feat: Add function to build command line for jobs from the config fil…
KernAttila Mar 28, 2023
4ac65a7
feat: same optimisation as PR #1283 + recognize type from config files
KernAttila Mar 28, 2023
3ccea48
feat: add `signals`, `getter` and `setter` attributes on `CueCommandW…
KernAttila Mar 28, 2023
3e60389
feat: add `signals`, `getter` and `setter` attributes on `CueLabelLin…
KernAttila Mar 28, 2023
fd924ad
feat: add `signals`, `getter` and `setter` attributes on `CueSelectPu…
KernAttila Mar 28, 2023
a254c9a
feat: update `setter` attribute on `CueSelectPulldown` and add a func…
KernAttila Mar 28, 2023
e152336
feat: add `signals`, `getter` and `setter` attributes on `CueLabelTog…
KernAttila Mar 28, 2023
17bc8d2
feat!: add default value to `CueLabelToggle`
KernAttila Mar 28, 2023
3bc307c
fix: test string widget during init, showing missing fields in red fr…
KernAttila Mar 28, 2023
46ea2d8
fix: skip optional options if they have no value
KernAttila Mar 28, 2023
5d774b3
fix: setChecked function was not receiving the right data type
KernAttila Mar 28, 2023
dfb220d
fix: change `FileExistsError` to a proper `FileNotFoundError`
KernAttila Mar 28, 2023
7e6846c
feat: add `DynamicSettingsWidget`
KernAttila Mar 28, 2023
b9aa0e2
fix: return early, passing tests when no config file is there
KernAttila Mar 28, 2023
cb08c88
test: add 2 tests for sub config files when using RENDER_CMDS
KernAttila Mar 28, 2023
3e207cb
doc: add context to the test
KernAttila Mar 28, 2023
f873d55
fix: test if file exists
KernAttila Mar 28, 2023
108670a
fix: replace old variable name
KernAttila Mar 31, 2023
a84707a
pep: add space between functions
KernAttila Apr 1, 2023
fe1a6d6
doc: add example for hidden widget without flag adding a value
KernAttila Aug 8, 2024
161f17b
fix: let parent argument at the end
KernAttila Jun 2, 2023
1f86dc2
fix: the Raise here would not work in older versions of python not im…
KernAttila Aug 7, 2024
a8d563c
CueGui, fix service edit (#1293)
KernAttila Jul 31, 2024
ab69f7f
Abstract logging into seperate classes (RQDLogger and LogReader) (#1429)
lithorus Aug 1, 2024
8c5a6ed
fix: re-add RENDER_CMDS in Constants.py
KernAttila Aug 8, 2024
cfb8fe3
fix: add frame tokens in the config example file
KernAttila Aug 8, 2024
ff2bdda
fix: remove double assignation, upgrade %s to f-string; add *args to …
KernAttila Aug 8, 2024
61fcd8d
chore: get back changes from master
KernAttila Aug 8, 2024
726a374
fix: expand widget on 2 rows so they align on the left
KernAttila Aug 15, 2024
1d73834
fix: fix layout order
KernAttila Aug 15, 2024
60551e8
fix: lint, remove unused args/kwargs
KernAttila Aug 15, 2024
743e180
Merge branch 'master' into cuesubmit-jobs-from-config-file
KernAttila Aug 15, 2024
af4f5dc
Merge branch 'cuesubmit-jobs-from-config-file' of https://github.com/…
KernAttila Aug 15, 2024
5556f11
fix: use raw string for regex
KernAttila Aug 15, 2024
5ae3145
fix: lint, line too long
KernAttila Aug 15, 2024
ed56fc5
fix: lint, lines too long
KernAttila Aug 15, 2024
ecc71a4
chore: extract hide widget functionality from nested if
KernAttila Aug 15, 2024
69d118f
Merge branch 'master' into cuesubmit-jobs-from-config-file
KernAttila Aug 15, 2024
b942729
Merge branch 'master' into cuesubmit-jobs-from-config-file
KernAttila Aug 19, 2024
490bc8d
Merge branch 'master' into cuesubmit-jobs-from-config-file
KernAttila Aug 21, 2024
04d3a06
doc: fix nuke example to be browsable
KernAttila Sep 20, 2024
288adda
doc: add "services" and "limits" presets for each job type.
KernAttila Sep 20, 2024
8f82fa5
feat: add DEFAULT_SHOW to the cuesubmit config file and use it in the…
KernAttila Sep 20, 2024
c620039
feat: option to have services and limits preset for each job type dir…
KernAttila Sep 20, 2024
c4cd63e
feat: re-order buttons under the job layer list widget (put arrows to…
KernAttila Sep 20, 2024
0a9501c
feat: add validator to match any integer, even negative
KernAttila Sep 20, 2024
0076500
refactor: only override job minCores if it is explicitly overridden
KernAttila Sep 20, 2024
fb7967d
feat: rename greyOut() to disable(), add enable() and toggled() funct…
KernAttila Sep 20, 2024
4ef105d
feat: add option to make CueLineEdit toggleable (and optionally put t…
KernAttila Sep 20, 2024
5b6c204
feat: CueSelectPulldown: add tooltip and select proper option.
KernAttila Sep 20, 2024
9275e3a
feat: CueLabelToggle: add tooltip, fix layout and make label clickabl…
KernAttila Sep 20, 2024
fe4d549
chore: remove ghost spacer item in helpWidget
KernAttila Sep 20, 2024
23075d4
chore: DynamicSettingsWidget: manage sizePolicy to avoid it expanding.
KernAttila Sep 20, 2024
4b97913
feat: add option to override min cores.
KernAttila Sep 20, 2024
43ca5e9
chore: add tooltips and re-order UI to gather related widgets.
KernAttila Sep 20, 2024
f385cc0
feat: add overrideCores option during job submission.
KernAttila Sep 20, 2024
9629f6c
Merge branch 'AcademySoftwareFoundation:master' into cuesubmit-jobs-f…
KernAttila Sep 20, 2024
47daaa6
chores: fix pylint issues
KernAttila Sep 21, 2024
fb91f2f
test: update test with the override setting
KernAttila Sep 21, 2024
82e98f1
fix: forgot overrideCore argument in the buildFactory.
KernAttila Sep 21, 2024
06368cc
test: handle connection error to pass the tests (no cuebot available …
KernAttila Sep 21, 2024
3c0a053
fix: for backward compatibility, check if we override cores by testin…
KernAttila Sep 28, 2024
731bf80
fix: revert to default behavior, we do not override only if the "core…
KernAttila Sep 28, 2024
f7062af
fix: revert to default behavior, we do not override only if the "core…
KernAttila Sep 28, 2024
01d3fb2
fix: only send "cores" to pyoutline, "overrideCores" is only set in c…
KernAttila Sep 28, 2024
b3408cc
fix: better UI scaling
KernAttila Sep 28, 2024
34f2fea
Merge branch 'master' into cuesubmit-jobs-from-config-file
KernAttila Sep 28, 2024
1bb0b7a
fix: lint line too long
KernAttila Sep 29, 2024
02fac67
fix: Make sure we use threads for backward compatibility. We check if…
KernAttila Sep 29, 2024
3b028b9
Merge branch 'cuesubmit-jobs-from-config-file' of https://github.com/…
KernAttila Sep 29, 2024
936f368
fix: cast str to float
KernAttila Sep 29, 2024
73a66eb
feat: add opencue icon to cuesubmit window
KernAttila Sep 29, 2024
134fd5d
fix: add `overrideCores` attribute in `LayerData.toDict()`
KernAttila Sep 29, 2024
261db2f
test: add overrideCores param
KernAttila Sep 29, 2024
fda4ab8
Merge branch 'master' into cuesubmit-jobs-from-config-file
KernAttila Oct 18, 2024
7e3b181
Merge branch 'master' into cuesubmit-jobs-from-config-file
KernAttila Nov 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 53 additions & 11 deletions cuesubmit/cuesubmit/Config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,61 @@

def getConfigValues():
"""Reads the config file from disk and returns the values it defines."""
configData = {}
configFile = os.environ.get(CONFIG_FILE_ENV_VAR)
if not configFile:
configFile = os.path.join(opencue.config.config_base_directory(), 'cuesubmit.yaml')
if os.path.exists(configFile):
with open(configFile, 'r', encoding='utf-8') as data:
try:
configData = yaml.load(data, Loader=yaml.SafeLoader)
except yaml.YAMLError:
raise CuesubmitConfigError("Could not load yaml file: {}. Please check its "
"formatting".format(configFile))
configFile = os.environ.get(CONFIG_FILE_ENV_VAR)\
or os.path.join(opencue.config.config_base_directory(),
'cuesubmit.yaml')
if not os.path.exists(configFile):
return {}
configData = _loadYamlFile(yaml_file=configFile)
if 'RENDER_CMDS' in configData:
# look for any sub-config files and load them
configData['RENDER_CMDS'] = _expandRenderConfigValues(configData['RENDER_CMDS'])
return configData


def _loadYamlFile(yaml_file):
""" Load config yaml as dict
:param yaml_file: path to a config.yaml file (path can be an env var)
:type yaml_file: str
:returns: yaml content
:rtype: dict
"""
_yaml_file = os.path.expandvars(yaml_file)
if not os.path.exists(_yaml_file):
raise FileNotFoundError(f"yaml_file:{_yaml_file} not found")
config_data = {}
with open(_yaml_file, 'r', encoding='utf-8') as data:
try:
config_data = yaml.load(data, Loader=yaml.SafeLoader)
except yaml.YAMLError:
raise CuesubmitConfigError("Could not load yaml file: {}. Please check its "
"formatting".format(_yaml_file))
return config_data


def _expandRenderConfigValues(RENDER_CMDS):
""" Looks through each render command and loads their 'config_file' if any
If 'config_file' is set but does not exist, replace its content with error for proper feedback

:param RENDER_CMDS: all render commands from the cuesubmit_config.yaml file
:type RENDER_CMDS: dict
:returns: Updated RENDER_CMDS
:rtype: dict
"""
for job_type, _options in RENDER_CMDS.items():
_sub_config_file = _options.get('config_file')
if not _sub_config_file:
continue
try:
RENDER_CMDS[job_type] = _loadYamlFile(yaml_file=_sub_config_file)
except FileNotFoundError as error:
RENDER_CMDS[job_type] = {
'command': 'error',
'options': {
'{ERROR}': error}
}
return RENDER_CMDS


class CuesubmitConfigError(Exception):
"""Thrown when an error occurs reading the config file."""
14 changes: 14 additions & 0 deletions cuesubmit/cuesubmit/Constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
MAYA_RENDER_CMD = config.get('MAYA_RENDER_CMD', 'Render')
NUKE_RENDER_CMD = config.get('NUKE_RENDER_CMD', 'nuke')
BLENDER_RENDER_CMD = config.get('BLENDER_RENDER_CMD', 'blender')
RENDER_CMDS = config.get('RENDER_CMDS', {})

DEFAULT_SHOW = config.get('DEFAULT_SHOW') or os.environ.get('PROJECT', 'default')

FRAME_TOKEN = config.get('FRAME_TOKEN', '#IFRAME#')
FRAME_START_TOKEN = config.get('FRAME_START', '#FRAME_START#')
FRAME_END_TOKEN = config.get('FRAME_END', '#FRAME_END#')
Expand Down Expand Up @@ -68,6 +72,16 @@
BLENDER_OUTPUT_OPTIONS_URL = \
'https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html#render-options'

REGEX_CUETOKEN = r'^#.*#$' #FRAME_START#
REGEX_COMMAND_OPTIONS = (r'(?P<command_flag>-+\w*)?' # -optionFlag
r'(?P<hidden>\~)?' # -hiddenFlag~
r'\s?'
r'({'
r'(?P<mandatory>\!)?' # {!Mandatory argument}
r'(?P<label>[^{}\*\/\!]+)' # {Nice name}
r'(?P<browsable>\*?\/?)' # {browseFile*} or {browseFolder/}
r'})?')

DIR_PATH = os.path.dirname(__file__)

# Dropdown label to specify the default Facility, i.e. let Cuebot decide.
Expand Down
25 changes: 22 additions & 3 deletions cuesubmit/cuesubmit/JobTypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@

from builtins import object
from cuesubmit.ui import SettingsWidgets

from cuesubmit import Constants
from cuesubmit import Util

class JobTypes(object):
"""Base Job Types available in the UI.
Expand All @@ -36,23 +37,41 @@ class JobTypes(object):
MAYA = 'Maya'
NUKE = 'Nuke'
BLENDER = 'Blender'
FROM_CONFIG_FILE = Constants.RENDER_CMDS.keys()

SETTINGS_MAP = {
SHELL: SettingsWidgets.ShellSettings,
MAYA: SettingsWidgets.BaseMayaSettings,
NUKE: SettingsWidgets.BaseNukeSettings,
BLENDER: SettingsWidgets.BaseBlenderSettings,
}
for jobType in FROM_CONFIG_FILE:
SETTINGS_MAP[jobType] = SettingsWidgets.DynamicSettingsWidget

def __init__(self):
pass

@classmethod
def build(cls, jobType, *args, **kwargs):
"""Factory method for creating a settings widget."""
if jobType in cls.FROM_CONFIG_FILE:
jobOptions = Constants.RENDER_CMDS[jobType].get('options')
parameters = Util.convertCommandOptions(options=jobOptions)
kwargs.update({'tool_name': jobType,
'parameters': parameters})
return cls.SETTINGS_MAP[jobType](*args, **kwargs)

@classmethod
def types(cls):
"""return a list of types available."""
return [cls.SHELL, cls.MAYA, cls.NUKE, cls.BLENDER]
"""return a list of available types."""
return list(cls.SETTINGS_MAP.keys())

@classmethod
def services(cls, jobType):
"""return a list of services for a given jobType. (the "services" key in your yaml file)"""
return Constants.RENDER_CMDS[jobType].get('services', [])

@classmethod
def limits(cls, jobType):
"""return a list of limits for a given jobType. (the "limits" key in your yaml file)"""
return Constants.RENDER_CMDS[jobType].get('limits', [])
18 changes: 13 additions & 5 deletions cuesubmit/cuesubmit/Layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __init__(self):
self.cmd = {}
self.layerRange = ''
self.chunk = '1'
self.overrideCores = False
self.cores = '1'
self.env = {}
self.services = []
Expand All @@ -59,6 +60,7 @@ def toDict(self):
'cmd': self.cmd,
'layerRange': self.layerRange,
'chunk': self.chunk,
'overrideCores': self.overrideCores,
'cores': self.cores,
'env': self.env,
'services': self.services,
Expand All @@ -68,15 +70,19 @@ def toDict(self):
}

@staticmethod
def buildFactory(name=None, layerType=None, cmd=None, layerRange=None, chunk=None, cores=None,
env=None, services=None, limits=None, dependType=None, dependsOn=None):
def buildFactory(name=None, layerType=None, cmd=None, layerRange=None, chunk=None,
overrideCores=None, cores=None, env=None, services=None, limits=None,
dependType=None, dependsOn=None):
"""Build a new LayerData object with the given settings."""
layerData = LayerData()
layerData.update(name, layerType, cmd, layerRange, chunk, cores, env, services, limits,
dependType, dependsOn)
layerData.update(name=name, layerType=layerType, cmd=cmd, layerRange=layerRange,
chunk=chunk, overrideCores=overrideCores, cores=cores, env=env,
services=services, limits=limits,
dependType=dependType, dependsOn=dependsOn)
return layerData

def update(self, name=None, layerType=None, cmd=None, layerRange=None, chunk=None, cores=None,
def update(self, name=None, layerType=None, cmd=None, layerRange=None, chunk=None,
overrideCores=None, cores=None,
env=None, services=None, limits=None, dependType=None, dependsOn=None):
"""Update this Layer with the provided settings."""
if name is not None:
Expand All @@ -91,6 +97,8 @@ def update(self, name=None, layerType=None, cmd=None, layerRange=None, chunk=Non
self.chunk = chunk
if cores is not None:
self.cores = cores
if overrideCores is not None:
self.overrideCores = overrideCores
if env is not None:
self.env = env
if services is not None:
Expand Down
60 changes: 57 additions & 3 deletions cuesubmit/cuesubmit/Submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,49 @@

from cuesubmit import Constants
from cuesubmit import JobTypes
from cuesubmit import Util


def isSoloFlag(flag):
""" Check if the flag is solo, meaning it has no associated value
solo flags are marked with a ~ (ex: --background~)
"""
return re.match(r"^-+\w+~$", flag)


def isFlag(flag):
""" Check if the provided string is a flag (starts with a -)"""
return re.match(r"^-+\w+$", flag)


def formatValue(flag, value, isPath, isMandatory):
""" Adds quotes around file/folder path variables
and provide an error value to display for missing mandatory values.
"""
if isPath and value:
value = f'"{value}"'
if isMandatory and value in ('', None):
value = f'!!missing value for {flag}!!'
return value


def buildDynamicCmd(layerData):
"""From a layer, builds a customized render command."""
renderCommand = Constants.RENDER_CMDS[layerData.layerType].get('command')
for (flag, isPath, isMandatory), value in layerData.cmd.items():
if isSoloFlag(flag):
renderCommand += f' {flag[:-1]}'
continue
value = formatValue(flag, value, isPath, isMandatory)
if isFlag(flag) and value not in ('', None):
# flag and value
renderCommand += f' {flag} {value}'
continue
# solo argument without flag
if value not in ('', None):
renderCommand += f' {value}'

return renderCommand


def buildMayaCmd(layerData, silent=False):
Expand Down Expand Up @@ -97,10 +140,17 @@ def buildLayer(layerData, command, lastLayer=None):
@type lastLayer: outline.layer.Layer
@param lastLayer: layer that this new layer should be dependent on if dependType is set.
"""
threadable = float(layerData.cores) >= 2 or float(layerData.cores) <= 0
threadable = False
if layerData.overrideCores:
threadable = float(layerData.cores) >= 2 or float(layerData.cores) <= 0
elif layerData.services and layerData.services[0] in Util.getServices():
threadable = Util.getServiceOption(layerData.services[0], 'threadable')

cores = layerData.cores if layerData.overrideCores else None
layer = outline.modules.shell.Shell(
layerData.name, command=command.split(), chunk=layerData.chunk,
threads=float(layerData.cores), range=str(layerData.layerRange), threadable=threadable)
cores=cores,
range=str(layerData.layerRange), threadable=threadable)
if layerData.services:
layer.set_service(layerData.services[0])
if layerData.limits:
Expand All @@ -112,9 +162,12 @@ def buildLayer(layerData, command, lastLayer=None):
layer.depend_on(lastLayer)
return layer


def buildLayerCommand(layerData, silent=False):
"""Builds the command to be sent per jobType"""
if layerData.layerType == JobTypes.JobTypes.MAYA:
if layerData.layerType in JobTypes.JobTypes.FROM_CONFIG_FILE:
command = buildDynamicCmd(layerData)
elif layerData.layerType == JobTypes.JobTypes.MAYA:
command = buildMayaCmd(layerData, silent)
elif layerData.layerType == JobTypes.JobTypes.SHELL:
command = layerData.cmd.get('commandTextBox') if silent else layerData.cmd['commandTextBox']
Expand All @@ -129,6 +182,7 @@ def buildLayerCommand(layerData, silent=False):
raise ValueError('unrecognized layer type {}'.format(layerData.layerType))
return command


def submitJob(jobData):
"""Submits the job using the PyOutline API."""
ol = outline.Outline(
Expand Down
63 changes: 62 additions & 1 deletion cuesubmit/cuesubmit/Util.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from __future__ import absolute_import

import os
import re

import opencue
from cuesubmit import Constants
Expand All @@ -33,14 +34,37 @@ def getLimits():

def getServices():
"""Returns a list of service names from cuebot."""
return [service.name() for service in opencue.api.getDefaultServices()]
try:
services = opencue.api.getDefaultServices()
except opencue.exception.ConnectionException:
return []
else:
return [service.name() for service in services]


def getServiceOption(serviceName, option):
"""Returns the value of a service property."""
service = next(iter(service for service in opencue.api.getDefaultServices()
if service.name() == serviceName))
if service and hasattr(service, option):
return getattr(service, option)()
print(f'{service.name()} service has no {option} option.')
return None


def getShows():
"""Returns a list of show names from cuebot."""
return [show.name() for show in opencue.api.getShows()]


def getDefaultShow():
"""Returns the default show defined via environment variable or config file, if set."""
default_show = next(iter([show for show in getShows()
if re.match(show, Constants.DEFAULT_SHOW, re.IGNORECASE)]),
'no default show')
return default_show


def getAllocations():
"""Returns a list of Allocations from cuebot."""
return opencue.api.getAllocations()
Expand All @@ -60,3 +84,40 @@ def getFacilities(allocations):
default_facilities = [Constants.DEFAULT_FACILITY_TEXT]
facilities = set(alloc.data.facility for alloc in allocations)
return default_facilities + list(facilities)


def convertCommandOptions(options):
""" Parse command options from the config file
and return parameters to feed the UI (name, type, value)

:param options: All options for a given command (ex:{"-flag {Nice Name}": "default_value"})
:type options: dict
:return: list of dict of parameters
"""
parameters = []
for option_line, value in options.items():
parse_option = re.search(Constants.REGEX_COMMAND_OPTIONS,
option_line)
options = {
'option_line': option_line,
'label': parse_option.group('label'),
'command_flag': parse_option.group('command_flag'),
'value': value,
'type': type(value),
'hidden': bool(parse_option.group('hidden'))
or re.match(Constants.REGEX_CUETOKEN, str(value)),
'mandatory': bool(parse_option.group('mandatory')),
'browsable': parse_option.group('browsable'),
}
if isinstance(value, (tuple, list))\
and len(value) in (3, 4)\
and isinstance(value[0], (int, float)):
options.update({
'type': range,
'min': value[0],
'max': value[1],
'value': value[2],
'float_precision': value[3] if len(value)==4 else None
})
parameters.append(options)
return parameters
Loading
Loading