maya-brew 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maya_brew/__init__.py +50 -0
- maya_brew/attributes/__init__.py +0 -0
- maya_brew/attributes/node_attribute.py +176 -0
- maya_brew/exceptions.py +4 -0
- maya_brew/log.py +338 -0
- maya_brew/maya_initializer/__init__.py +0 -0
- maya_brew/maya_initializer/interactive_init.py +18 -0
- maya_brew/maya_initializer/interactive_utils.py +26 -0
- maya_brew/maya_initializer/shared.py +10 -0
- maya_brew/maya_initializer/standalone_init.py +27 -0
- maya_brew/nodes/__init__.py +0 -0
- maya_brew/nodes/cast.py +65 -0
- maya_brew/nodes/node_types.py +87 -0
- maya_brew/scene.py +25 -0
- maya_brew/utils.py +26 -0
- maya_brew-0.0.4.dist-info/METADATA +71 -0
- maya_brew-0.0.4.dist-info/RECORD +19 -0
- maya_brew-0.0.4.dist-info/WHEEL +4 -0
- maya_brew-0.0.4.dist-info/licenses/LICENSE +21 -0
maya_brew/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import maya.api._OpenMaya_py2 as OpenMaya2 # noqa
|
|
5
|
+
import maya.cmds as cmds # noqa: F401
|
|
6
|
+
|
|
7
|
+
PACKAGE_NAME = "MAYA_BREW"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_bool_env_variable(name: str, default=True):
|
|
11
|
+
value = os.getenv(name)
|
|
12
|
+
if value is None:
|
|
13
|
+
return default
|
|
14
|
+
return value.lower() in ("true", "1", "t", "yes", "y")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
MAYA_BREW_AUTOINIT = get_bool_env_variable(f"{PACKAGE_NAME}_AUTOINIT", True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def init():
|
|
21
|
+
old_handlers = logging.root.handlers[:]
|
|
22
|
+
old_log_level = logging.getLogger().level
|
|
23
|
+
import maya_brew.log
|
|
24
|
+
from maya_brew.maya_initializer.shared import is_interactive_maya
|
|
25
|
+
|
|
26
|
+
interactive = is_interactive_maya()
|
|
27
|
+
if interactive:
|
|
28
|
+
maya_brew.log.configure(use_colors=False)
|
|
29
|
+
else:
|
|
30
|
+
maya_brew.log.configure(
|
|
31
|
+
use_colors=get_bool_env_variable(f"{PACKAGE_NAME}_USE_LOG_COLORS", True)
|
|
32
|
+
)
|
|
33
|
+
logger = maya_brew.log.get_logger(__name__)
|
|
34
|
+
logger.info("Initializing maya...")
|
|
35
|
+
os.environ["MAYA_NO_WARNING_FOR_MISSING_DEFAULT_RENDERER"] = "1"
|
|
36
|
+
if is_interactive_maya():
|
|
37
|
+
from maya_brew.maya_initializer import interactive_init
|
|
38
|
+
|
|
39
|
+
interactive_init.initialize_maya()
|
|
40
|
+
else:
|
|
41
|
+
from maya_brew.maya_initializer import standalone_init
|
|
42
|
+
|
|
43
|
+
standalone_init.initialize_maya()
|
|
44
|
+
logging.root.handlers = old_handlers
|
|
45
|
+
logging.getLogger().setLevel(old_log_level)
|
|
46
|
+
logging.debug("maya initialized")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if MAYA_BREW_AUTOINIT:
|
|
50
|
+
init()
|
|
File without changes
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
|
|
3
|
+
from .. import OpenMaya2, cmds
|
|
4
|
+
from ..nodes import cast
|
|
5
|
+
from ..nodes.node_types import DagNode, Node
|
|
6
|
+
|
|
7
|
+
PlugInput = typing.Union[OpenMaya2.MPlug, str]
|
|
8
|
+
A = typing.TypeVar("A", bound="Attribute")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Attribute:
|
|
12
|
+
_getter_type: str
|
|
13
|
+
|
|
14
|
+
def __new__(cls: type[A], plug_or_path: PlugInput) -> A:
|
|
15
|
+
if cls is not Attribute:
|
|
16
|
+
return typing.cast(A, super().__new__(cls))
|
|
17
|
+
|
|
18
|
+
if isinstance(plug_or_path, OpenMaya2.MPlug):
|
|
19
|
+
plug = plug_or_path
|
|
20
|
+
elif isinstance(plug_or_path, str):
|
|
21
|
+
plug = cls._plug_from_path(plug_or_path)
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError("plug_or_path must be an MPlug or attribute path string.")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
mobj_attr = plug.attribute()
|
|
27
|
+
except (RuntimeError, ValueError) as e:
|
|
28
|
+
raise RuntimeError(
|
|
29
|
+
f"Failed to obtain MObject attribute for plug {plug}"
|
|
30
|
+
) from e
|
|
31
|
+
|
|
32
|
+
api_type = getattr(mobj_attr, "apiTypeStr", None)
|
|
33
|
+
if not api_type:
|
|
34
|
+
raise RuntimeError(
|
|
35
|
+
f"Attribute object for plug {plug} has no apiTypeStr; cannot dispatch."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
subclass: type[Attribute] = _API_TYPE_SUBCLASS_MAP[api_type]
|
|
40
|
+
except KeyError:
|
|
41
|
+
raise NotImplementedError(
|
|
42
|
+
f"Unsupported attribute apiTypeStr '{api_type}'. "
|
|
43
|
+
f"Known types: {sorted(_API_TYPE_SUBCLASS_MAP)}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
instance = super().__new__(subclass)
|
|
47
|
+
setattr(instance, "_pre_init_plug", plug)
|
|
48
|
+
return typing.cast(A, instance)
|
|
49
|
+
|
|
50
|
+
@typing.overload
|
|
51
|
+
def __init__(self, plug_or_path: str): ...
|
|
52
|
+
|
|
53
|
+
@typing.overload
|
|
54
|
+
def __init__(self, plug_or_path: OpenMaya2.MPlug): ...
|
|
55
|
+
|
|
56
|
+
def __init__(self, plug_or_path: PlugInput):
|
|
57
|
+
# _pre_init_plug is injected by Attribute.__new__ ONLY when the user called
|
|
58
|
+
# Attribute(...) (base class factory dispatch). Direct subclass construction
|
|
59
|
+
# (e.g. FloatAttribute(...)) bypasses that path, so pre will be None and we
|
|
60
|
+
# must resolve plug_or_path here.
|
|
61
|
+
pre = getattr(self, "_pre_init_plug", None)
|
|
62
|
+
if pre is not None:
|
|
63
|
+
self.plug = pre
|
|
64
|
+
delattr(self, "_pre_init_plug")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if isinstance(plug_or_path, OpenMaya2.MPlug):
|
|
68
|
+
self.plug = plug_or_path
|
|
69
|
+
elif isinstance(plug_or_path, str):
|
|
70
|
+
self.plug = self._plug_from_path(plug_or_path)
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError("plug_or_path must be an MPlug or attribute path string.")
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _plug_from_path(path: str) -> OpenMaya2.MPlug:
|
|
76
|
+
"""
|
|
77
|
+
Resolve a string path to an MPlug.
|
|
78
|
+
:param path: The full path to the attribute, e.g. '|grp|node|nodeShape.visibility'
|
|
79
|
+
:return: The MPlug for the attribute.
|
|
80
|
+
"""
|
|
81
|
+
# Split path into node path and attribute name
|
|
82
|
+
if "." not in path:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"Attribute path must include a '.' separating node and attribute."
|
|
85
|
+
)
|
|
86
|
+
node_path, attr_name = path.rsplit(".", 1)
|
|
87
|
+
node_dag_path = cast.get_dag_path_from_string(node_path)
|
|
88
|
+
node = DagNode(node_dag_path.fullPathName())
|
|
89
|
+
plug = Attribute._get_plug_from_node(node, attr_name)
|
|
90
|
+
return plug
|
|
91
|
+
|
|
92
|
+
def __str__(self):
|
|
93
|
+
return self.name()
|
|
94
|
+
|
|
95
|
+
def get(self):
|
|
96
|
+
return self._get_plug_value(self.plug)
|
|
97
|
+
|
|
98
|
+
def set(self, value):
|
|
99
|
+
attr_name = self.plug.name()
|
|
100
|
+
cmds.setAttr(attr_name, value)
|
|
101
|
+
|
|
102
|
+
def node(self):
|
|
103
|
+
return self._get_node_from_plug(self.plug)
|
|
104
|
+
|
|
105
|
+
def name(self):
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
|
|
108
|
+
def connect(self, dest: "Attribute", force: bool = False, next_available=False):
|
|
109
|
+
"""
|
|
110
|
+
Connect this attribute to another attribute.
|
|
111
|
+
:param dest: The destination attribute to connect to.
|
|
112
|
+
:param force: Whether to force the connection if the destination is already connected.
|
|
113
|
+
:param next_available: Whether to connect to the next available index if the destination is an array attribute.
|
|
114
|
+
"""
|
|
115
|
+
cmds.connectAttr(
|
|
116
|
+
self.plug.name(),
|
|
117
|
+
dest.plug.name(),
|
|
118
|
+
force=force,
|
|
119
|
+
nextAvailable=next_available,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def disconnect(self, dest: "Attribute", next_available=False):
|
|
123
|
+
"""
|
|
124
|
+
Disconnect this attribute from another attribute.
|
|
125
|
+
:param dest: The destination attribute to disconnect from.
|
|
126
|
+
:param next_available: Whether to disconnect from the next available index if the destination is an
|
|
127
|
+
array attribute.
|
|
128
|
+
"""
|
|
129
|
+
cmds.disconnectAttr(
|
|
130
|
+
self.plug.name(), dest.plug.name(), nextAvailable=next_available
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
def _get_value(cls, node: DagNode, attr_name: str):
|
|
135
|
+
plug = cls._get_plug_from_node(node, attr_name)
|
|
136
|
+
return cls._get_plug_value(plug)
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _get_plug_from_node(node: DagNode, attr_name: str) -> OpenMaya2.MPlug:
|
|
140
|
+
fn_dep = node.get_mfndependency_node()
|
|
141
|
+
return fn_dep.findPlug(attr_name, False)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _get_node_from_plug(plug: OpenMaya2.MPlug) -> Node:
|
|
145
|
+
mobj = plug.node()
|
|
146
|
+
if mobj.hasFn(OpenMaya2.MFn.kDagNode):
|
|
147
|
+
return DagNode(OpenMaya2.MFnDagNode(mobj).fullPathName())
|
|
148
|
+
else:
|
|
149
|
+
name = OpenMaya2.MFnDependencyNode(mobj).name()
|
|
150
|
+
return Node(name)
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def _get_plug_value(cls, plug: OpenMaya2.MPlug):
|
|
154
|
+
return getattr(plug, cls._getter_type)()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class FloatAttribute(Attribute):
|
|
158
|
+
_getter_type = "asDouble"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class MessageAttribute(Attribute):
|
|
162
|
+
_getter_type = "kMessage"
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def _get_plug_value(cls, plug: OpenMaya2.MPlug):
|
|
166
|
+
raise AttributeError("Message attributes do not hold data.")
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def set(cls, value):
|
|
170
|
+
raise AttributeError("Message attributes are not settable.")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
_API_TYPE_SUBCLASS_MAP = {
|
|
174
|
+
"kDoubleLinearAttribute": FloatAttribute,
|
|
175
|
+
"kMessageAttribute": MessageAttribute,
|
|
176
|
+
}
|
maya_brew/exceptions.py
ADDED
maya_brew/log.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for setting up logging in modules
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import fnmatch
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from typing import Callable, List, Optional, Union
|
|
13
|
+
|
|
14
|
+
import structlog
|
|
15
|
+
|
|
16
|
+
from . import PACKAGE_NAME, get_bool_env_variable
|
|
17
|
+
|
|
18
|
+
PROPAGATE = get_bool_env_variable(f"{PACKAGE_NAME}_LOG_PROPAGATE", False)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
TELEMETRY_PROCESSORS: List[Callable] = []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def telemetry_runner(target_logger, method_name, event_dict):
|
|
25
|
+
"""
|
|
26
|
+
Runs processors for telemetry events or events where logger is enabled for event level.
|
|
27
|
+
Drops telemetry events after processing
|
|
28
|
+
"""
|
|
29
|
+
is_telemetry_event = event_dict.get("event_type") == "telemetry"
|
|
30
|
+
if is_telemetry_event:
|
|
31
|
+
run_telemetry_processors(target_logger, method_name, event_dict)
|
|
32
|
+
raise structlog.DropEvent
|
|
33
|
+
event_level = event_dict.get("level")
|
|
34
|
+
numeric_level = getattr(logging, event_level.upper(), None)
|
|
35
|
+
if target_logger.isEnabledFor(numeric_level):
|
|
36
|
+
run_telemetry_processors(target_logger, method_name, event_dict)
|
|
37
|
+
return event_dict
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run_telemetry_processors(target_logger, method_name, event_dict):
|
|
41
|
+
for processor in TELEMETRY_PROCESSORS:
|
|
42
|
+
processor(target_logger, method_name, event_dict)
|
|
43
|
+
return event_dict
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def format_console_time(target_logger, method_name, event_dict):
|
|
47
|
+
event_dict["timestamp"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
48
|
+
return event_dict
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def configure(use_colors=False):
|
|
52
|
+
# maya doesn't seem to play nice with colors, and once structlog is configured to use colors it seems to
|
|
53
|
+
# cause problems when reconfiguring without
|
|
54
|
+
|
|
55
|
+
if not structlog.is_configured():
|
|
56
|
+
logging.basicConfig(level=logging.INFO)
|
|
57
|
+
logging.Logger.propagate = bool(PROPAGATE)
|
|
58
|
+
|
|
59
|
+
structlog.configure(
|
|
60
|
+
processors=[
|
|
61
|
+
structlog.stdlib.add_log_level,
|
|
62
|
+
structlog.contextvars.merge_contextvars,
|
|
63
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
64
|
+
structlog.processors.TimeStamper(fmt="iso", utc=False),
|
|
65
|
+
structlog.processors.CallsiteParameterAdder(
|
|
66
|
+
{
|
|
67
|
+
structlog.processors.CallsiteParameter.FILENAME,
|
|
68
|
+
structlog.processors.CallsiteParameter.PATHNAME,
|
|
69
|
+
structlog.processors.CallsiteParameter.MODULE,
|
|
70
|
+
structlog.processors.CallsiteParameter.FUNC_NAME,
|
|
71
|
+
structlog.processors.CallsiteParameter.LINENO,
|
|
72
|
+
}
|
|
73
|
+
),
|
|
74
|
+
structlog.processors.StackInfoRenderer(),
|
|
75
|
+
structlog.processors.UnicodeDecoder(),
|
|
76
|
+
telemetry_runner,
|
|
77
|
+
format_console_time,
|
|
78
|
+
structlog.dev.ConsoleRenderer(colors=use_colors),
|
|
79
|
+
],
|
|
80
|
+
context_class=dict,
|
|
81
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
82
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
83
|
+
cache_logger_on_first_use=True,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LogLevelContextManager:
|
|
88
|
+
def __init__(self, target_logger, level):
|
|
89
|
+
self.logger = target_logger
|
|
90
|
+
self.level = level
|
|
91
|
+
self.previous_level = target_logger.getEffectiveLevel()
|
|
92
|
+
|
|
93
|
+
def __enter__(self):
|
|
94
|
+
self.logger.setLevel(self.level)
|
|
95
|
+
|
|
96
|
+
def __call__(self, func):
|
|
97
|
+
"""Allow class to be used as decorator"""
|
|
98
|
+
|
|
99
|
+
def decorator(*args, **kwargs):
|
|
100
|
+
with self:
|
|
101
|
+
return func(*args, **kwargs)
|
|
102
|
+
|
|
103
|
+
return decorator
|
|
104
|
+
|
|
105
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
106
|
+
self.logger.setLevel(self.previous_level)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class LogAllLevelsContextManager:
|
|
110
|
+
def __init__(self, min_level=logging.CRITICAL):
|
|
111
|
+
self.min_level = min_level
|
|
112
|
+
|
|
113
|
+
def __enter__(self):
|
|
114
|
+
logging.disable(self.min_level)
|
|
115
|
+
|
|
116
|
+
def __call__(self, func):
|
|
117
|
+
"""Allow class to be used as decorator"""
|
|
118
|
+
|
|
119
|
+
def decorator(*args, **kwargs):
|
|
120
|
+
with self:
|
|
121
|
+
return func(*args, **kwargs)
|
|
122
|
+
|
|
123
|
+
return decorator
|
|
124
|
+
|
|
125
|
+
def __exit__(self, exit_type, exit_value, exit_traceback):
|
|
126
|
+
logging.disable(logging.NOTSET)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class SuppressStdOutStdErr:
|
|
130
|
+
"""
|
|
131
|
+
A context manager for doing a "deep suppression" of stdout and stderr in
|
|
132
|
+
Python, i.e. will suppress all print, even if the print originates in a
|
|
133
|
+
compiled C/Fortran sub-function.
|
|
134
|
+
This will not suppress raised exceptions, since exceptions are printed
|
|
135
|
+
to stderr just before a script exits, and after the context manager has
|
|
136
|
+
exited (at least, I think that is why it lets exceptions through).
|
|
137
|
+
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self, filter_list: Optional[Union[List[str], str]] = None, enable=True
|
|
142
|
+
) -> None:
|
|
143
|
+
self.enable = enable
|
|
144
|
+
if self.enable:
|
|
145
|
+
if filter_list is None:
|
|
146
|
+
self.filter_words = []
|
|
147
|
+
elif isinstance(filter_list, str):
|
|
148
|
+
self.filter_words = [filter_list]
|
|
149
|
+
else:
|
|
150
|
+
self.filter_words = filter_list
|
|
151
|
+
|
|
152
|
+
def __enter__(self):
|
|
153
|
+
if self.enable:
|
|
154
|
+
self._stdout = sys.stdout
|
|
155
|
+
self._stderr = sys.stderr
|
|
156
|
+
self._stdout_pipe = os.pipe()
|
|
157
|
+
self._stderr_pipe = os.pipe()
|
|
158
|
+
self._stdout_fd = os.dup(1)
|
|
159
|
+
self._stderr_fd = os.dup(2)
|
|
160
|
+
os.dup2(self._stdout_pipe[1], 1)
|
|
161
|
+
os.dup2(self._stderr_pipe[1], 2)
|
|
162
|
+
self._stdout_thread = self._start_thread(self._stdout_pipe[0], self._stdout)
|
|
163
|
+
self._stderr_thread = self._start_thread(self._stderr_pipe[0], self._stderr)
|
|
164
|
+
os.close(self._stdout_pipe[1])
|
|
165
|
+
os.close(self._stderr_pipe[1])
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
def __exit__(self, exc_type, exc_value, traceback):
|
|
169
|
+
if self.enable:
|
|
170
|
+
os.dup2(self._stdout_fd, 1)
|
|
171
|
+
os.dup2(self._stderr_fd, 2)
|
|
172
|
+
|
|
173
|
+
os.close(self._stdout_fd)
|
|
174
|
+
os.close(self._stderr_fd)
|
|
175
|
+
|
|
176
|
+
if self._stdout_thread.is_alive():
|
|
177
|
+
self._stdout_thread.join()
|
|
178
|
+
if self._stderr_thread.is_alive():
|
|
179
|
+
self._stderr_thread.join()
|
|
180
|
+
|
|
181
|
+
def _should_filter(self, line) -> bool:
|
|
182
|
+
if not self.filter_words:
|
|
183
|
+
return True
|
|
184
|
+
line = line.strip()
|
|
185
|
+
for pattern in self.filter_words:
|
|
186
|
+
pattern = pattern
|
|
187
|
+
if "*" in pattern:
|
|
188
|
+
if fnmatch.fnmatch(line, pattern):
|
|
189
|
+
return True
|
|
190
|
+
else:
|
|
191
|
+
if pattern in line:
|
|
192
|
+
return True
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
def _start_thread(self, read_fd, write_stream):
|
|
196
|
+
def _filter_output():
|
|
197
|
+
read_file = os.fdopen(read_fd)
|
|
198
|
+
try:
|
|
199
|
+
for line in read_file:
|
|
200
|
+
if not self._should_filter(line):
|
|
201
|
+
write_stream.write(line)
|
|
202
|
+
write_stream.flush()
|
|
203
|
+
finally:
|
|
204
|
+
read_file.close()
|
|
205
|
+
|
|
206
|
+
thread = threading.Thread(target=_filter_output)
|
|
207
|
+
thread.daemon = True
|
|
208
|
+
thread.start()
|
|
209
|
+
return thread
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class ExecutionTimer:
|
|
213
|
+
def __init__(self):
|
|
214
|
+
self.elapsed_time = None
|
|
215
|
+
|
|
216
|
+
def __enter__(self):
|
|
217
|
+
self.start_time = time.time()
|
|
218
|
+
return self
|
|
219
|
+
|
|
220
|
+
def __exit__(self, type, value, traceback):
|
|
221
|
+
end_time = time.time()
|
|
222
|
+
self.elapsed_time = end_time - self.start_time
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class SilenceContextManager(SuppressStdOutStdErr):
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class SingleLevelFilter(logging.Filter):
|
|
230
|
+
def __init__(self, passlevel, reject):
|
|
231
|
+
super(SingleLevelFilter, self).__init__()
|
|
232
|
+
self.passlevel = passlevel
|
|
233
|
+
self.reject = reject
|
|
234
|
+
|
|
235
|
+
def filter(self, record):
|
|
236
|
+
if self.reject:
|
|
237
|
+
return record.levelno != self.passlevel
|
|
238
|
+
else:
|
|
239
|
+
return record.levelno == self.passlevel
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class CustomLogger(structlog.stdlib.BoundLogger):
|
|
243
|
+
timer: ExecutionTimer
|
|
244
|
+
info_handler: logging.StreamHandler
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def all_log_levels(min_level=logging.CRITICAL):
|
|
248
|
+
return LogAllLevelsContextManager(min_level)
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def silence(filter_list: Optional[List[str]] = None, enable=True):
|
|
252
|
+
return SilenceContextManager(filter_list=filter_list, enable=enable)
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def at_level(target_logger, level):
|
|
256
|
+
return LogLevelContextManager(target_logger, level)
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def info_to_stdout(target_logger=None):
|
|
260
|
+
h1 = logging.StreamHandler(sys.stdout)
|
|
261
|
+
f1 = SingleLevelFilter(logging.INFO, False)
|
|
262
|
+
h1.addFilter(f1)
|
|
263
|
+
target_logger.addHandler(h1)
|
|
264
|
+
|
|
265
|
+
@staticmethod
|
|
266
|
+
def send_telemetry(*args, **kwargs):
|
|
267
|
+
"""Sends message to any telemetry handlers on logger"""
|
|
268
|
+
raise NotImplementedError
|
|
269
|
+
|
|
270
|
+
def setLevel(self, level: Union[int, str]) -> None:
|
|
271
|
+
super().setLevel(level) # type: ignore[arg-type]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class TelemetryHandler:
|
|
275
|
+
"""Abstract class for logging handlers to send telemetry data"""
|
|
276
|
+
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def get_logger(name) -> CustomLogger:
|
|
281
|
+
"""
|
|
282
|
+
Simple stream-logger that can also be used as a context manager.
|
|
283
|
+
Be aware of the hacky adding of methods to existing logger,
|
|
284
|
+
but overriding return type to get correct inspection in IDEs
|
|
285
|
+
"""
|
|
286
|
+
logger = structlog.stdlib.get_logger(name) # noqa
|
|
287
|
+
|
|
288
|
+
# Hacks since current versions of pytest doesn't allow subclassing
|
|
289
|
+
# of logging.getLogger
|
|
290
|
+
setattr(logger, "all_log_levels", CustomLogger.all_log_levels)
|
|
291
|
+
|
|
292
|
+
def _silence(*args, **kwargs):
|
|
293
|
+
return CustomLogger.silence(*args, **kwargs) # noqa
|
|
294
|
+
|
|
295
|
+
setattr(logger, "silence", _silence)
|
|
296
|
+
|
|
297
|
+
setattr(logger, "timer", ExecutionTimer)
|
|
298
|
+
|
|
299
|
+
def _send_telemetry(*args, **kwargs):
|
|
300
|
+
kwargs["event_type"] = "telemetry"
|
|
301
|
+
logger.info(*args, **kwargs)
|
|
302
|
+
|
|
303
|
+
setattr(logger, "send_telemetry", _send_telemetry)
|
|
304
|
+
|
|
305
|
+
def _info_to_stdout():
|
|
306
|
+
return CustomLogger.info_to_stdout(logger)
|
|
307
|
+
|
|
308
|
+
setattr(logger, "info_to_stdout", _info_to_stdout)
|
|
309
|
+
|
|
310
|
+
def at_level(level):
|
|
311
|
+
return CustomLogger.at_level(logger, level)
|
|
312
|
+
|
|
313
|
+
setattr(logger, "at_level", at_level)
|
|
314
|
+
return logger # type: ignore[return-value]
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def mock_telemetry_processor(logger, method_name, event_dict):
|
|
318
|
+
print(f"Sending to some telemetry service: {event_dict}")
|
|
319
|
+
return event_dict
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
configure()
|
|
323
|
+
|
|
324
|
+
if __name__ == "__main__":
|
|
325
|
+
TELEMETRY_PROCESSORS.append(mock_telemetry_processor)
|
|
326
|
+
configure(use_colors=True)
|
|
327
|
+
example_logger = get_logger(__name__)
|
|
328
|
+
example_logger.info("Logging configured")
|
|
329
|
+
example_logger.debug(
|
|
330
|
+
"This is a debug message and shouldn't be displayed as logging level is set to info"
|
|
331
|
+
)
|
|
332
|
+
example_logger.setLevel("DEBUG")
|
|
333
|
+
with example_logger.silence(filter_list=["filtered"]):
|
|
334
|
+
example_logger.info("this should be filtered out")
|
|
335
|
+
example_logger.info("this should not be silenced!")
|
|
336
|
+
example_logger.debug(
|
|
337
|
+
"This is a debug message and should be displayed as logging level is set to debug"
|
|
338
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .. import log
|
|
2
|
+
from .interactive_utils import SuppressScriptEditorOutput
|
|
3
|
+
|
|
4
|
+
# note this logger is defined before SilenceMayaContextManager is added so will not
|
|
5
|
+
# silence script editor output
|
|
6
|
+
logger = log.get_logger(__name__)
|
|
7
|
+
# logger.setLevel("DEBUG")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SilenceMayaContextManager(
|
|
11
|
+
SuppressScriptEditorOutput, log.LogAllLevelsContextManager
|
|
12
|
+
):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def initialize_maya():
|
|
17
|
+
log.SilenceContextManager = SilenceMayaContextManager
|
|
18
|
+
logger.debug("Initializing interactive maya...")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import maya.OpenMaya as OpenMaya1
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SuppressScriptEditorOutput:
|
|
5
|
+
"""
|
|
6
|
+
Suppresses output to the scriptEditor
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.callback_id = OpenMaya1.MCommandMessage.addCommandOutputFilterCallback(
|
|
11
|
+
self.suppress
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def suppress(_, __, filter_output, ___):
|
|
16
|
+
"""
|
|
17
|
+
This is the callback function that gets called when Maya wants to
|
|
18
|
+
print something suppressing the print
|
|
19
|
+
"""
|
|
20
|
+
OpenMaya1.MScriptUtil.setBool(filter_output, True)
|
|
21
|
+
|
|
22
|
+
def __enter__(self):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
26
|
+
OpenMaya1.MMessage.removeCallback(self.callback_id)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from ..log import get_logger
|
|
2
|
+
|
|
3
|
+
logger = get_logger(__name__)
|
|
4
|
+
# logger.setLevel("DEBUG")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def initialize_maya():
|
|
8
|
+
if cmds_about_exists():
|
|
9
|
+
return
|
|
10
|
+
with logger.silence(["This plugin does not support createPlatformOpenGLContext!"]):
|
|
11
|
+
import maya.standalone
|
|
12
|
+
|
|
13
|
+
maya.standalone.initialize(name="initialized_by_maya_brew")
|
|
14
|
+
logger.debug("initialized maya standalone")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmds_about_exists():
|
|
18
|
+
try:
|
|
19
|
+
from maya.cmds import about # noqa: F401
|
|
20
|
+
|
|
21
|
+
return True
|
|
22
|
+
except ImportError:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
initialize_maya()
|
|
File without changes
|
maya_brew/nodes/cast.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from .. import OpenMaya2, cmds, exceptions
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NodeCastingException(exceptions.MayaBrewException):
|
|
5
|
+
"""Base class for node casting exceptions."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InvalidDagPath(NodeCastingException, RuntimeError, ValueError, TypeError):
|
|
11
|
+
"""Exception raised when a DAG path is invalid."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NonExistingDagPath(NodeCastingException, RuntimeError, ValueError, TypeError):
|
|
17
|
+
"""Exception raised when a DAG path is invalid."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MultipleMatchingNodes(NodeCastingException):
|
|
23
|
+
"""Exception raised when multiple nodes match a given name."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NoMatchingNodes(NodeCastingException):
|
|
29
|
+
"""Exception raised when no nodes match a given name."""
|
|
30
|
+
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_dag_path_from_string(node_path: str) -> OpenMaya2.MDagPath:
|
|
35
|
+
"""
|
|
36
|
+
Convert a string representation of a node path to an OpenMaya2.MDagPath object.
|
|
37
|
+
"""
|
|
38
|
+
selection_list = OpenMaya2.MSelectionList()
|
|
39
|
+
try:
|
|
40
|
+
selection_list.add(node_path)
|
|
41
|
+
dag_path = selection_list.getDagPath(0)
|
|
42
|
+
except RuntimeError as e:
|
|
43
|
+
if "Object does not exist" in str(e):
|
|
44
|
+
raise NonExistingDagPath(f"Dag Path does not exist: {node_path}") from e
|
|
45
|
+
raise
|
|
46
|
+
except TypeError as e:
|
|
47
|
+
if "item is not a DAG path" in str(e):
|
|
48
|
+
raise InvalidDagPath(f"{node_path} exists but is not a dag object") from e
|
|
49
|
+
raise
|
|
50
|
+
return dag_path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_long_name_from_maya_string(name: str) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Convert a unique maya.cmds node string to a long name.
|
|
56
|
+
"""
|
|
57
|
+
matching_nodes = cmds.ls(name, long=True)
|
|
58
|
+
if not matching_nodes:
|
|
59
|
+
raise NoMatchingNodes(f"No objects matching '{name}' in scene")
|
|
60
|
+
num_nodes = len(matching_nodes)
|
|
61
|
+
if num_nodes > 1:
|
|
62
|
+
raise MultipleMatchingNodes(
|
|
63
|
+
f"Multiple objects matching '{name}' in scene. Found {num_nodes} nodes: {matching_nodes}"
|
|
64
|
+
)
|
|
65
|
+
return matching_nodes[0]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from typing import Any, Callable, Dict, Self
|
|
2
|
+
|
|
3
|
+
from .. import OpenMaya2, cmds
|
|
4
|
+
from ..log import get_logger
|
|
5
|
+
from ..nodes import cast
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
logger.setLevel("DEBUG")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Node:
|
|
12
|
+
_cmds_creator: Callable[..., str]
|
|
13
|
+
_cmds_creator_args: Dict[str, Any] = {}
|
|
14
|
+
|
|
15
|
+
def __init__(self, node_path: str):
|
|
16
|
+
self.node_path = node_path
|
|
17
|
+
|
|
18
|
+
def __str__(self):
|
|
19
|
+
return self.node_path
|
|
20
|
+
|
|
21
|
+
def __repr__(self):
|
|
22
|
+
return f'{type(self).__name__}("{str(self)}")'
|
|
23
|
+
|
|
24
|
+
def rename(self, new_name: str, *args, **kwargs) -> str:
|
|
25
|
+
"""
|
|
26
|
+
Rename the current node. If new name is not unique maya will resolve a new unique name.
|
|
27
|
+
:param new_name: The new name of the node.
|
|
28
|
+
:return: The new name of the node.
|
|
29
|
+
"""
|
|
30
|
+
actual_new_name = cmds.rename(str(self), new_name, *args, **kwargs)
|
|
31
|
+
self.node_path = actual_new_name
|
|
32
|
+
return actual_new_name
|
|
33
|
+
|
|
34
|
+
def _cmd_caller(self, func_name, *args, **kwargs):
|
|
35
|
+
return cmds.attr(func_name, self.node_path, *args, **kwargs)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def create(cls, name: str, **kwargs) -> Self:
|
|
39
|
+
"""
|
|
40
|
+
Create a new transform node.
|
|
41
|
+
:param name: The name of the new transform node.
|
|
42
|
+
:return: The new transform node.
|
|
43
|
+
"""
|
|
44
|
+
kwargs.update(cls._cmds_creator_args)
|
|
45
|
+
kwargs["name"] = name
|
|
46
|
+
cmds_value = cls._cmds_creator(**kwargs) # noqa: E1102
|
|
47
|
+
if cmds_value != name:
|
|
48
|
+
logger.debug(f"Name was not unique. New name: {cmds_value}.")
|
|
49
|
+
return cls(cmds_value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DagNode(Node):
|
|
53
|
+
def __init__(self, node_path: str):
|
|
54
|
+
super().__init__(node_path)
|
|
55
|
+
self.dag_path = cast.get_dag_path_from_string(node_path)
|
|
56
|
+
|
|
57
|
+
def __str__(self):
|
|
58
|
+
return self.get_full_path()
|
|
59
|
+
|
|
60
|
+
def get_depend_node(self) -> OpenMaya2.MObject:
|
|
61
|
+
"""
|
|
62
|
+
Get the depend node of the current node.
|
|
63
|
+
:return: The depend node of the current node.
|
|
64
|
+
"""
|
|
65
|
+
return self.dag_path.node()
|
|
66
|
+
|
|
67
|
+
def get_full_path(self) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Get the full path of the current node.
|
|
70
|
+
:return: The full path of the current node.
|
|
71
|
+
"""
|
|
72
|
+
full_path = self.dag_path.fullPathName()
|
|
73
|
+
if not full_path:
|
|
74
|
+
logger.warning(f"Node has been deleted. Original path: {self.node_path}")
|
|
75
|
+
return full_path
|
|
76
|
+
|
|
77
|
+
def get_mfndependency_node(self) -> OpenMaya2.MFnDependencyNode:
|
|
78
|
+
"""
|
|
79
|
+
Get the MFnDependencyNode of the current node.
|
|
80
|
+
:return: The MFnDependencyNode of the current node.
|
|
81
|
+
"""
|
|
82
|
+
return OpenMaya2.MFnDependencyNode(self.get_depend_node())
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Transform(DagNode):
|
|
86
|
+
_cmds_creator = cmds.group
|
|
87
|
+
_cmds_creator_args = {"empty": True}
|
maya_brew/scene.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import maya_brew
|
|
2
|
+
|
|
3
|
+
from .exceptions import MayaBrewException
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SceneException(MayaBrewException):
|
|
7
|
+
"""Base class for scene-related exceptions."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnsavedChanges(SceneException, RuntimeError):
|
|
13
|
+
"""Exception raised when there are unsaved changes in the scene."""
|
|
14
|
+
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def new_file(force=True):
|
|
19
|
+
try:
|
|
20
|
+
return maya_brew.cmds.file(newFile=True, force=force)
|
|
21
|
+
except RuntimeError as e:
|
|
22
|
+
message = str(e).strip()
|
|
23
|
+
if message == "Unsaved changes.":
|
|
24
|
+
raise UnsavedChanges(message) from e
|
|
25
|
+
raise SceneException(message) from e
|
maya_brew/utils.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .log import get_logger
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
logger.setLevel("DEBUG")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def reload_mb():
|
|
12
|
+
"""
|
|
13
|
+
Workaround to reload entire maya-brew library.
|
|
14
|
+
Removes all maya-brew modules from 'sys.modules' and imports it again
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import maya_brew
|
|
18
|
+
|
|
19
|
+
mb_path = Path(maya_brew.__file__)
|
|
20
|
+
mb_modules = list()
|
|
21
|
+
for m in [p for p in sys.modules if p.startswith(mb_path.parent.name)]:
|
|
22
|
+
mb_modules.append(m)
|
|
23
|
+
sys.modules.pop(m)
|
|
24
|
+
for m in mb_modules:
|
|
25
|
+
logger.debug(f"re-importing {m}")
|
|
26
|
+
importlib.import_module(m)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: maya-brew
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: Safe interaction with python in maya
|
|
5
|
+
Project-URL: Homepage, https://github.com/mortenbohne/maya-brew
|
|
6
|
+
Project-URL: Repository, https://github.com/mortenbohne/maya-brew
|
|
7
|
+
Project-URL: Documentation, https://github.com/mortenbohne/maya-brew#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/mortenbohne/maya-brew/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/mortenbohne/maya-brew/releases
|
|
10
|
+
Author-email: Morten Bøhne <github@mortenbohne.dk>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: 3d,autodesk,maya,pipeline
|
|
14
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: colorama
|
|
22
|
+
Requires-Dist: structlog
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: maya-stubs; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# maya-brew
|
|
29
|
+
|
|
30
|
+
> **Status: Pre-Alpha** – Interfaces are experimental and may change without notice. Pin exact versions if using in
|
|
31
|
+
> automation.
|
|
32
|
+
|
|
33
|
+
What you need after a long day of wrestling with Maya Python.
|
|
34
|
+
|
|
35
|
+
[](https://github.com/marketplace/actions/super-linter)
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Overview
|
|
40
|
+
|
|
41
|
+
`maya-brew` aims to provide safer, more structured interaction patterns when scripting or tooling inside Autodesk Maya.
|
|
42
|
+
|
|
43
|
+
Goals:
|
|
44
|
+
|
|
45
|
+
- Reduce foot‑guns when using Python inside Maya
|
|
46
|
+
- Offer utilities that behave well with Maya’s quirks
|
|
47
|
+
|
|
48
|
+
## Background
|
|
49
|
+
|
|
50
|
+
`maya-brew` draws inspiration from some of the ergonomic goals that motivated
|
|
51
|
+
[PyMEL](https://github.com/LumaPictures/pymel): making Maya’s Python interface feel more “Pythonic,” safer, and more
|
|
52
|
+
discoverable. PyMEL historically provided rich object wrappers around nodes, attributes, and other scene elements;
|
|
53
|
+
for many workflows this improved readability and reduced boilerplate.
|
|
54
|
+
|
|
55
|
+
However, over time several practical factors led to the decision to start a lightweight alternative instead of
|
|
56
|
+
extending PyMEL itself:
|
|
57
|
+
|
|
58
|
+
- Scope & Complexity: PyMEL’s dynamic wrapping and layered abstractions can introduce startup overhead, indirection, and
|
|
59
|
+
occasional debugging friction.
|
|
60
|
+
- Historical Churn: A long evolution, multiple contributors, and some legacy design choices make selective modernization
|
|
61
|
+
non‑trivial.
|
|
62
|
+
- Maintenance & Momentum: While still usable, PyMEL no longer appears to have the same pace of active development or
|
|
63
|
+
broad community push it once did.
|
|
64
|
+
- Adoption Fit: Many pipelines only need a thin set of safety/ergonomic helpers—not a full object layer over
|
|
65
|
+
`maya.cmds`.
|
|
66
|
+
|
|
67
|
+
### Acknowledgment
|
|
68
|
+
|
|
69
|
+
PyMEL significantly influenced how many technical artists approach Maya scripting, and this project stands on that
|
|
70
|
+
conceptual foundation. `maya-brew` simply narrows the focus: take the useful ergonomic ideas, shed the heavy wrapping,
|
|
71
|
+
and keep to the canonical APIs.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
maya_brew/__init__.py,sha256=OkRwh4MmWcBtsttw3kkR8SEZhlK7rNLoQQexzRLEmTI,1431
|
|
2
|
+
maya_brew/exceptions.py,sha256=se87z8Fo6phDroKCtNd4detMQuS6nabNeqjeqYW41_Y,96
|
|
3
|
+
maya_brew/log.py,sha256=bK8sIJjD8Hziy3_N2maz3HWU0OFPl9SO1SoqpZoRf_A,10735
|
|
4
|
+
maya_brew/scene.py,sha256=8gNR1bIr99cUiyH9PQUTiqcWKOEZzflRFOBsSMRe5qQ,598
|
|
5
|
+
maya_brew/utils.py,sha256=HeqXcMHHilT4B5bOobAH9ZHwtCuKe4nVu_3DiS7aFsw,618
|
|
6
|
+
maya_brew/attributes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
maya_brew/attributes/node_attribute.py,sha256=J8Vf_qWDDfzdIPWx1PLo82sb8bKKVac6U_FuWRusiCc,5997
|
|
8
|
+
maya_brew/maya_initializer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
maya_brew/maya_initializer/interactive_init.py,sha256=a6KCLDRUUuK9W9rzKzb2Lf0t6p-8yVmGkVshIffob5A,500
|
|
10
|
+
maya_brew/maya_initializer/interactive_utils.py,sha256=vpq7TVjAj0L1Wcbaq3IEqLZODcfeeytLC5bDzL05QxQ,692
|
|
11
|
+
maya_brew/maya_initializer/shared.py,sha256=Ot58zYLhHCOMR_AU9Rw65BwvKXioGRsD9_jSUqfRIcs,245
|
|
12
|
+
maya_brew/maya_initializer/standalone_init.py,sha256=iWV3eMSzeYO1uqT9oRYSyB6Tn-XlPOeTmI8l4LajUmw,594
|
|
13
|
+
maya_brew/nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
maya_brew/nodes/cast.py,sha256=fuEsSbkulwzQ2r12Pk7Ux6ays85u9e5SsTRAW8ejsQo,1906
|
|
15
|
+
maya_brew/nodes/node_types.py,sha256=NYZ1-Eqh5ks-org6xeXhWMQx-jroXspUMRx_Qk8RmvI,2707
|
|
16
|
+
maya_brew-0.0.4.dist-info/METADATA,sha256=iR0pNoE1OHXIHbZ4wuwowYVhE7WaHjED_2HeuQpUojM,3112
|
|
17
|
+
maya_brew-0.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
18
|
+
maya_brew-0.0.4.dist-info/licenses/LICENSE,sha256=M8rkKXCsDdVH3gxjabYl5pH9TzX9GrMmwlXyjlwjnkE,1070
|
|
19
|
+
maya_brew-0.0.4.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Morten Bøhne
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|