Skip to content

alc.path

The addressing system. See PathObject — Addressing for the concepts.

alc.path.path_object

CoordinateError

Bases: ValueError

A tool URL could not be encoded or decoded.

The message always names the token that was missing or malformed. This is deliberately loud: a link that cannot be resolved is a broken link, and quietly opening a default page instead is how you end up reviewing (or screenshotting) something other than what you were sent.

UnknownTokenError

Bases: ValueError

A PathObject was built with a token the model does not carry.

Pydantic's extra="ignore" drops unknown keys without a word, which is the right policy for the token dicts the route engine hands the validator (a route's vocabulary is deliberately wider than the model's — category, asset, video are all route-side names for tokens the model stores as parent/child). It is the wrong policy for what a caller typed: PathObject(video="theaxe") silently produced parent="" and an object that addressed the wrong thing — the coordinate was accepted and the identity was gone. A coordinate that names a token nobody has is not a coordinate.

PathObject

PathObject(
    input_val: Union[
        str, Path, Dict[str, Any], None
    ] = None,
    **kwargs
)

Bases: BaseModel

Unified entry point. We wrap strings/paths into a dict key so the 'before' validator can find them reliably.

Source code in alc\path\path_object.py
def __init__(
    self, input_val: Union[str, Path, Dict[str, Any], None] = None, **kwargs
):
    """
    Unified entry point. We wrap strings/paths into a dict key
    so the 'before' validator can find them reliably.
    """
    self._event_service = None
    if isinstance(input_val, dict):
        # If it's already a dict, merge with kwargs
        kwargs.update(input_val)
    elif input_val is not None:
        # If it's a string or Path, put it in a special key for the validator
        kwargs["input_data"] = input_val

    # An unknown token in a coordinate is a typo or a stale name, and either
    # way the token it was MEANT to set stays empty. Refuse it here rather
    # than let ``extra="ignore"`` hand back a half-built address.
    #
    # Deliberately scoped to what the caller passed. Tokens the route engine
    # discovers while PARSING a path are merged inside ``hydrate_context``
    # (below), after this check, so a route's wider vocabulary still resolves
    # exactly as before — only hand-written coordinates are held to the model.
    unknown = set(kwargs) - self._accepted_keys()
    if unknown:
        raise UnknownTokenError(
            f"PathObject does not carry {sorted(unknown)}. "
            f"Known tokens: {sorted(self._accepted_keys())}. "
            "(Route-side names map onto the model: a video/asset slug is "
            "`parent`, a shot/asset name is `child`.)"
        )

    super().__init__(**kwargs)

version_thumbnail property

version_thumbnail: Path

Version thumbnail under the resolution-rooted .thumb convention ({res}/.thumb/{subpath}/{base}.png). Mirrors :meth:get_thumb_path (thumb_type="version").

task_id property

task_id: str

The 'Manufacturing ID' used for MSD filenames.

task_label property

task_label: str

The print friendly label for this task. Returns:

publish_object property

publish_object: PathObject

Quick switch to publish sync_root.

relative_path property

relative_path: str

Returns the relative path of the pathObject Returns:

path property

path: Path

get the absolute path of the current PathObject Returns:

media_url property

media_url

returns the /media url of the current PathObject Returns:

abs_path property

abs_path

alais for getting the absolute path of the current PathObject Returns:

web_prefix property

web_prefix: str

/c/{company}/project/{project} — the grounded URL prefix every tool route already mounts under (AlcContextMiddleware binds the alc request context from exactly these two tokens).

web_coordinate property

web_coordinate: str

The at/{entity_type}/{parent}/{child}/{task}?… tail on its own.

Raises :class:CoordinateError naming every address token that is missing — an unaddressable coordinate is a loud failure, never a quiet fall back to a partial address that would open the wrong thing.

source_object property

source_object

returns the "source" version of the current PathObject, this is useful when working with source files. Returns:

render_object property

render_object

Returns the "render" version of the current pathObject, this is useful when working with "rendered" or exported files.

Returns:

next_minor_version property

next_minor_version

The next minor version as a string Returns:

next_major_version property

next_major_version

the next major version as a string Returns:

next_major_object property

next_major_object

the next major version as a pathObject Returns:

next_minor_object property

next_minor_object

the next minor version as a pathObject Returns:

source_path property

source_path

returns the "source" path of the current PathObject Returns:

render_path property

render_path

Returns the render path of the current pathObject Returns:

render_files property

render_files

Returns the files in the current fully resolved "render" directory, "resolution" as the last token

Returns:

source_files property

source_files

Returns the files in the current fully resolved "source" directory, "resolution" as the last token Returns:

versions property

versions

lists the versions on disk for the current pathObject context

Only NNN.NNN directories count (see :data:VERSION_DIR_RE). Anything else sitting in the user folder is somebody's stray directory, not a rung on the rail, and listing it poisons every reader downstream — latest_version_object would hand it to copy(version=...) and raise a pydantic ValidationError for the whole task. Returns:

latest_version_object property

latest_version_object

Return the latest pathObject for the current context. Returns:

preview_path property

preview_path: str

Get the preview path for the current context (the .preview convention). Back-compat shim — prefer :meth:get_preview_path.

msd_root property

msd_root

Return the root directory where all models files are located. Returns:

task_msd_root property

task_msd_root

Return the root directory where all task specific models files are located Returns:

event_msd_root property

event_msd_root

Return the root directory where all event specific models files are located Returns:

bug_msd_root property

bug_msd_root

Return the root directory where all bug specific models files are located Returns:

review_msd_root property

review_msd_root

Return the root directory where all review specific models files are located Returns:

playlist_msd_root property

playlist_msd_root

Root dir for playlist manifests — the project's ordered review lists (one project-level playlists.json, since a playlist crosses tasks by definition).

todo_msd_root property

todo_msd_root

Root dir for todo manifests — the Art-phase work lists (one JSON per task, todos tagged by working version).

dirname property

dirname

Returns the directory portion of the path, excluding the filename and extension. Returns:

msd_path property

msd_path: Path

Returns the path for the models file for the current context. Returns:

pub_msd_path property

pub_msd_path: Path

Returns the publish models path for the current context. Returns:

tracker property

tracker

The active production tracker (ShotGrid / Ayon / ...) for this context.

Convenience: po.tracker.get_shot(po) / po.tracker.publish(po).

version_msd property

version_msd

Returns the actual version manifest object for the current context. Returns:

task_msd property

task_msd

Returns the actual task manifest object for the current context. Returns:

asset_details property

asset_details: Any

This coordinate's registry entry, looked up in ITS OWN project.

Resolved from self.company / self.project — never from the ambient "current" project. A path IS a statement of which project it belongs to, so asking a global which project we are in, while holding a coordinate that already says so, can only ever be right by luck or by someone having remembered to bind a context first. It was wrong on the farm for exactly that reason: an loj model build read jlt's registries, found no asset, and died on a height that was set the whole time.

Returns None when this coordinate is not an asset (no matching parent), or when the asset genuinely is not in that project's registry — this is a LOOKUP, and "is it registered?" is a fair question to ask it. Callers that cannot proceed without one should say so themselves; :attr:height does.

Raises ValueError via :func:store_for if the coordinate carries no project, because there is then nothing to look it up in and guessing is the whole bug.

Warns when the project it resolves against was INFERRED rather than stated (see :attr:project_inferred). Resolving from the coordinate is only as trustworthy as the coordinate; measurement says every asset path in the repo and the cookbook names its own project, so this should stay silent — and if it ever speaks, it is naming the exact silent cross-project read that took a farm outage to notice.

height property

height

This asset's real-world height, in centimetres.

Raises rather than letting a missing registry entry surface as AttributeError: 'NoneType' object has no attribute 'height' several frames away — that traceback said nothing about WHICH asset, or which project it had been looked for in, which is why a farm job reading the wrong project's registries took so long to read as a routing problem at all.

version_basename property

version_basename: str

The ONE canonical name shared by a version and every file derived from it.

Resolved from the pathmap (filenames config) via the route engine — the single source of truth — so it honors per-task token rules: default_tokens (assets/shots → parent_child_task, e.g. 010_SEQ_svz, chr_jack_mdl) and by_task custom tokens (e.g. a tex channel → chr_jack_tex_metal_rough). Every media sibling is this basename plus a kind suffix in a known dot-folder (.preview/{base}.mp4, .thumb/{base}.png, .proxy/{base}.####.png), so the derived paths agree no matter how the po was built (working file, bare directory, set_filename path). This is the rule humans and machines reach for; the working file (get_filename) and set_filename resolve from the same config.

unreal_asset_name property

unreal_asset_name: str

Content-browser asset name following the naming convention (parent_child_task, e.g. chr_jack_mdl).

hydrate_context classmethod

hydrate_context(data: Any) -> Any

The 'Brain' - Normalizes dicts, strings, and Paths into the final attribute dictionary.

Source code in alc\path\path_object.py
@model_validator(mode="before")
@classmethod
def hydrate_context(cls, data: Any) -> Any:
    """
    The 'Brain' - Normalizes dicts, strings, and Paths into
    the final attribute dictionary.
    """
    if not isinstance(data, dict):
        return data

    # 1. Extract the raw input if it exists
    raw_input = data.get("input_data")

    # 2. Handle string/Path inputs (The File System Resolver)
    if isinstance(raw_input, (str, Path)):
        from alc.config.route import AlchemyRouteEngine

        # Use the RouteEngine to parse the string into tokens
        tokens, route = AlchemyRouteEngine().resolve_path(str(raw_input))
        if not tokens:
            raise ValueError(f"RouteEngine could not resolve: {raw_input}")

        # Merge tokens into the data dict (preserving existing data)
        for k, v in tokens.items():
            data.setdefault(k, v)
        data["route"] = route

    if "ext" in data and "filename" in data:
        if data["ext"]:
            filename = data["filename"]
            base, ext_ = os.path.splitext(filename)
            data["filename"] = (
                f"{base}{data['ext']}"  # override filename with new ext.
            )

    # 3. Inject Alchemy Infrastructure Defaults
    # We do this AFTER resolution so we don't overwrite resolved tokens.
    # Grounded via the *active* context (per-request in the web app,
    # CFG-current everywhere else) so two concurrent requests on different
    # companies/projects never inherit each other's tokens.
    from alc.context import active_company, active_project, active_prod_root

    _company = active_company()
    _project = active_project()
    defaults = {
        "company": getattr(_company, "id", ""),
        "root": active_prod_root() or "",
        "project": getattr(_project, "id", ""),
    }

    for key, val in defaults.items():
        if not data.get(key) and val:
            data[key] = val
            # Record that the PROJECT specifically was a guess, not a statement.
            # Marked here because this is the only place that can tell: by the
            # time anyone holds the object, an inferred project and a stated one
            # are the same string.
            if key == "project":
                data["project_inferred"] = True

    return data

cookbook_relative_path

cookbook_relative_path(
    absolute_fallback: bool = True,
) -> str

This path expressed in the cookbook files addressing form.

Cookbook-authored files are a first-class path category — they live under the cookbook root (CFG.cookbook_dir), routinely a different drive than the repo. This returns the path RELATIVE to that root (blender/build/mdl/Build.py) when it sits inside the cookbook tree, so it can be run back through classify_path(repo_label="cookbook") or matched against a cookbook area's software-first path_prefixes.

When the path is OUTSIDE the cookbook tree, there is no meaningful cookbook-relative form, so it returns the absolute posix path (the honest addressable form) — or "" when absolute_fallback is False and the caller wants "cookbook-relative or nothing". Prefers the object's own cookbook_root token when set, else grounds on CFG.cookbook_dir.

Source code in alc\path\path_object.py
def cookbook_relative_path(self, absolute_fallback: bool = True) -> str:
    """This path expressed in the *cookbook files* addressing form.

    Cookbook-authored files are a first-class path category — they live under
    the cookbook root (``CFG.cookbook_dir``), routinely a different drive than
    the repo. This returns the path RELATIVE to that root
    (``blender/build/mdl/Build.py``) when it sits inside the cookbook tree, so
    it can be run back through ``classify_path(repo_label="cookbook")`` or
    matched against a cookbook area's software-first ``path_prefixes``.

    When the path is OUTSIDE the cookbook tree, there is no meaningful
    cookbook-relative form, so it returns the absolute posix path (the honest
    addressable form) — or ``""`` when ``absolute_fallback`` is False and the
    caller wants "cookbook-relative or nothing". Prefers the object's own
    ``cookbook_root`` token when set, else grounds on ``CFG.cookbook_dir``."""
    p = Path(self.path).as_posix()
    root = self.cookbook_root or ""
    if not root:
        try:
            from alc.config import CFG

            root = CFG.cookbook_dir.as_posix() if CFG.cookbook_dir else ""
        except Exception:
            root = ""
    if root:
        root = Path(root).as_posix().rstrip("/")
        if p == root or p.startswith(root + "/"):
            return Path(p).relative_to(root).as_posix()
    return p if absolute_fallback else ""

path_with_ext

path_with_ext(ext: str, set_filename=True) -> Path

gets the absolute path of the current PathObject with ext - uses "set_filename" by default. Returns:

Source code in alc\path\path_object.py
def path_with_ext(self, ext: str, set_filename=True) -> Path:
    """
    gets the absolute path of the current PathObject with ext - uses "set_filename" by default.
    Returns:

    """
    path_ = Path(self.path)
    dirname = path_.parent
    if set_filename:
        filename = self.get_filename(ext=ext)
    else:
        filename = path_.with_suffix(ext).name
    return dirname / filename

tool_url

tool_url(tool: str) -> str

The shareable URL for tool opened on THIS coordinate.

tool is the tool's own path under the project prefix — transcript, media, or a nested one like cookbook/task. This is what tools push with hx-push-url as the user navigates, and what a headless re-visit (screengrab, request pill, tutorial) can replay to land on the same state in a fresh session.

Source code in alc\path\path_object.py
def tool_url(self, tool: str) -> str:
    """The shareable URL for *tool* opened on THIS coordinate.

    ``tool`` is the tool's own path under the project prefix — ``transcript``,
    ``media``, or a nested one like ``cookbook/task``. This is what tools
    push with ``hx-push-url`` as the user navigates, and what a headless
    re-visit (screengrab, request pill, tutorial) can replay to land on the
    same state in a fresh session.
    """
    if not tool or tool.strip("/") == "":
        raise CoordinateError("tool_url() needs a tool path (e.g. 'transcript')")
    return f"{self.web_prefix}/{tool.strip('/')}/{self.web_coordinate}"

from_tool_url classmethod

from_tool_url(url: str) -> tuple[str, 'PathObject']

Decode a coordinate URL back into (tool, PathObject).

The inverse of :meth:tool_url; the pair round-trips every token in the coordinate set. Unknown query params (?panel=, ?view=) are ignored on purpose — tools carry their own view state in the same query string and it is none of the coordinate's business.

Company grounds root from the registered company, so the URL — not whatever the server last had selected — decides where the path resolves.

Source code in alc\path\path_object.py
@classmethod
def from_tool_url(cls, url: str) -> tuple[str, "PathObject"]:
    """Decode a coordinate URL back into ``(tool, PathObject)``.

    The inverse of :meth:`tool_url`; the pair round-trips every token in the
    coordinate set. Unknown query params (``?panel=``, ``?view=``) are
    ignored on purpose — tools carry their own view state in the same query
    string and it is none of the coordinate's business.

    Company grounds ``root`` from the registered company, so the URL — not
    whatever the server last had selected — decides where the path resolves.
    """
    match = _WEB_COORDINATE_RE.match(urlsplit(url).path)
    if not match:
        raise CoordinateError(
            f"not a coordinate URL: {url!r} (expected "
            f"/c/{{company}}/project/{{project}}/{{tool}}/{WEB_COORDINATE_MARKER}/"
            "{entity_type}/{parent}/{child}/{task})"
        )
    company, project, tool, tail = match.groups()
    segments = [unquote(s) for s in tail.split("/") if s]
    if len(segments) != len(WEB_ADDRESS_SEGMENTS):
        raise CoordinateError(
            f"coordinate URL needs exactly "
            f"{len(WEB_ADDRESS_SEGMENTS)} address segments "
            f"({'/'.join(WEB_ADDRESS_SEGMENTS)}), got {len(segments)}: {url!r}"
        )

    from alc.config import CFG

    company_cfg = CFG.companies.get(company)
    if company_cfg is None:
        raise CoordinateError(
            f"unknown company {company!r} in {url!r} "
            f"(known: {', '.join(sorted(CFG.companies)) or 'none'})"
        )

    tokens: dict[str, Any] = dict(zip(WEB_ADDRESS_SEGMENTS, segments))
    tokens.update(
        root=str(company_cfg.prod_root),
        company=company,
        project=project,
    )
    # keep_blank_values: a blank ``u=``/``v=`` is a real statement — this
    # coordinate sits ABOVE the user/version tokens — not an absent param.
    query = parse_qs(urlsplit(url).query, keep_blank_values=True)
    for key, (field, default) in WEB_REVISION_PARAMS.items():
        values = query.get(key)
        tokens[field] = values[0] if values else default
    for field in WEB_EXTRA_PARAMS:
        values = query.get(field)
        if values:
            tokens[field] = values[0]
    return tool, cls(**tokens)

copy

copy(**overrides) -> 'PathObject'

An 'Identity-Preserving' copy. Ensures every token is passed to the new instance so the RouteEngine has a full map to work with.

Source code in alc\path\path_object.py
def copy(self, **overrides) -> "PathObject":
    """
    An 'Identity-Preserving' copy.
    Ensures every token is passed to the new instance so the
    RouteEngine has a full map to work with.
    """
    current_state = self.model_dump(by_alias=False, exclude_none=False)
    current_state.update(overrides)
    return self.__class__(**current_state)

path_at

path_at(
    end_at: Optional[str] = None,
    set_filename: bool = False,
    ext: Optional[str] = None,
    po: Optional[bool] = False,
    sequence: bool = False,
) -> Union[Path, "PathObject"]

The core 'GPS' method. Resolves the path up to a specific token.

sequence=True (with set_filename=True) returns a frame-sequence path — …/chr_genMan_mdl.####.png — so render outputs use the same naming convention as every other file.

Source code in alc\path\path_object.py
def path_at(
    self,
    end_at: Optional[str] = None,
    set_filename: bool = False,
    ext: Optional[str] = None,
    po: Optional[bool] = False,
    sequence: bool = False,
) -> Union[Path, "PathObject"]:
    """
    The core 'GPS' method. Resolves the path up to a specific token.

    ``sequence=True`` (with ``set_filename=True``) returns a frame-sequence
    path — ``…/chr_genMan_mdl.####.png`` — so render outputs use the same
    naming convention as every other file.
    """
    # 1. Prepare data using aliases (RouteEngine expects 'type', not 'entity_type')
    #
    # ``root`` STAYS IN the dump. Excluding it made the assembler fall back to
    # prod_root and emit an absolute production path, which the trailing
    # ``Path(self.root) / ...`` join then swallowed whole — so path_at
    # disagreed with ``path`` about which volume a coordinate lives on, and
    # anything grounded off the live production volume (a tmp root in tests, a
    # second studio root, a sandbox) read and WROTE on prod anyway. It also
    # starved the root-gated routes (perforce/{PROD_ROOT}//Game) of the one
    # token they match on. (fix 2026-08-12)
    exclude = {"input_data", "task_id", "major_version", "minor_version"}
    data = self.model_dump(exclude=exclude, by_alias=False)

    # 2. Inject extension overrides (crucial for pivoting to .models or .jpg)
    if ext:
        data["ext"] = ext
        if data["filename"]:
            filename = Path(data["filename"])
            filename = filename.with_suffix(ext).as_posix()
            data["filename"] = filename

    # 3. Resolve via the Singleton Engine
    # We pass the full data dict; the engine will stop at 'end_at'
    resolved_path_str = AlchemyRouteEngine().path_from_dict(
        data, end_at=end_at, set_filename=set_filename, sequence=sequence
    )

    # 4. The engine already anchored the path on this coordinate's own root
    # (the template's first token), exactly as ``path`` does — no second join.
    full_path = Path(resolved_path_str)

    if po:
        # return a fresh class from full_path
        return PathObject(full_path)

    return full_path

get_preview_path

get_preview_path(
    ext: str = ".mp4", relative: bool = False, as_posix=True
) -> str

The quicktime preview path: {res}/.preview/{subpath}/{base}{ext}.

Source code in alc\path\path_object.py
def get_preview_path(
    self, ext: str = ".mp4", relative: bool = False, as_posix=True
) -> str:
    """The quicktime preview path: ``{res}/.preview/{subpath}/{base}{ext}``."""
    path = self._media_path(".preview", f"{self._media_basename()}{ext}")
    return self._format_return(path, relative, as_posix)

get_proxy_path

get_proxy_path(
    ext: str = ".png",
    relative: bool = False,
    as_posix=True,
    frames: bool = True,
) -> str

The proxy path: {res}/.proxy/{subpath}/{base}.####{ext}.

frames=True (default) returns a 4-padded printf pattern for a proxy SEQUENCE; frames=False returns a single {base}{ext} (a still proxy). Proxies are PNG by convention (color-managed for EXR via oiio).

Source code in alc\path\path_object.py
def get_proxy_path(
    self,
    ext: str = ".png",
    relative: bool = False,
    as_posix=True,
    frames: bool = True,
) -> str:
    """The proxy path: ``{res}/.proxy/{subpath}/{base}.####{ext}``.

    ``frames=True`` (default) returns a 4-padded printf pattern for a proxy
    SEQUENCE; ``frames=False`` returns a single ``{base}{ext}`` (a still
    proxy). Proxies are PNG by convention (color-managed for EXR via oiio)."""
    base = self._media_basename()
    name = f"{base}.%04d{ext}" if frames else f"{base}{ext}"
    path = self._media_path(".proxy", name)
    return self._format_return(path, relative, as_posix)

get_script_media_path

get_script_media_path(
    kind: str,
    name: str,
    relative: bool = False,
    as_posix=True,
) -> str

A flat home for a script/visual's attached media: {res}/{kind}/{name}.

kind is a plain folder — "images" for stills, "videos" for moving formats — so a visual beat's dropped/generated file lands at …/images/0010_A.png "for free", without the per-block/per-board nesting the svz storyboard convention uses. The Script editor stays flat here; a publish emits an OTIO editorial unit, so exact paths don't need to encode the cut. Rooted in the render tree (an attached/created artifact, like a voice take).

Source code in alc\path\path_object.py
def get_script_media_path(
    self, kind: str, name: str, relative: bool = False, as_posix=True
) -> str:
    """A flat home for a script/visual's attached media: ``{res}/{kind}/{name}``.

    ``kind`` is a plain folder — ``"images"`` for stills, ``"videos"`` for moving
    formats — so a visual beat's dropped/generated file lands at
    ``…/images/0010_A.png`` "for free", without the per-block/per-board nesting the
    ``svz`` storyboard convention uses. The Script editor stays flat here; a publish
    emits an OTIO editorial unit, so exact paths don't need to encode the cut. Rooted
    in the render tree (an attached/created artifact, like a voice take)."""
    path = self._media_path(kind, name)
    return self._format_return(path, relative, as_posix)

get_thumb_path

get_thumb_path(
    thumb_type="version",
    relative=False,
    as_posix=True,
    ext=".png",
) -> str

Returns the path to the thumbnail for this task or version. Args: thumb_type: "version" (resolution-rooted .thumb convention), "task" (task-level thumb), or "file" (beside an arbitrary file). ext: thumbnail extension (default .png). relative: return the path relative to root rather than absolute.

Source code in alc\path\path_object.py
def get_thumb_path(
    self, thumb_type="version", relative=False, as_posix=True, ext=".png"
) -> str:
    """
    Returns the path to the thumbnail for this task or version.
    Args:
        thumb_type: "version" (resolution-rooted ``.thumb`` convention),
            "task" (task-level thumb), or "file" (beside an arbitrary file).
        ext: thumbnail extension (default ``.png``).
        relative: return the path relative to root rather than absolute.
    """
    if thumb_type == "task":
        thumb_path = self.task_thumbnail
    elif thumb_type == "file":
        thumb_path = self.file_thumbnail
    elif thumb_type == "asset":
        thumb_path = self.asset_thumbnail
    else:  # "version" — the resolution-rooted convention
        thumb_path = self._media_path(".thumb", f"{self._media_basename()}{ext}")

    return self._format_return(thumb_path, relative, as_posix=as_posix)

get_msd_path

get_msd_path(pub=False, relative=False, as_posix=True)

Returns the path to the MSD file for this version. Args: pub: if True, returns the path to the publish MSD rather than the version MSD. relative: if True, returns the relative path from the root rather than the full path.

Source code in alc\path\path_object.py
def get_msd_path(self, pub=False, relative=False, as_posix=True):
    """
    Returns the path to the MSD file for this version.
    Args:
        pub: if True, returns the path to the publish MSD rather than the version MSD.
        relative: if True, returns the relative path from the root rather than the full path.
    """
    if pub:
        msd_path = self.pub_msd_path
    else:
        msd_path = self.msd_path

    return self._format_return(msd_path, relative, as_posix=as_posix)

list_dir

list_dir(
    token: str = "resolution", full_path: bool = False
) -> list[str]

Lists contents at a specific token level. Useful for finding available versions or resolutions.

Source code in alc\path\path_object.py
def list_dir(self, token: str = "resolution", full_path: bool = False) -> list[str]:
    """
    Lists contents at a specific token level.
    Useful for finding available versions or resolutions.
    """

    target_folder = self.path_at(end_at=token)

    if not target_folder.exists():
        return []
    basename = not full_path

    return lj_list_dir(target_folder.as_posix(), basename=basename)

get_users

get_users()

Returns a list of users who have published versions for this task. Returns:

Source code in alc\path\path_object.py
def get_users(self):
    """
    Returns a list of users who have published versions for this task.
    Returns:

    """
    users = self.list_dir(token="users")

    return users

get_source_path

get_source_path(
    dirname=False, relative=False, as_posix=True
) -> Path | str

Returns the path to the source version of this task. Args: dirname: if True, returns the directory containing the source version rather than the full path to the source version.

Returns:

Source code in alc\path\path_object.py
def get_source_path(
    self, dirname=False, relative=False, as_posix=True
) -> Path | str:
    """
    Returns the path to the source version of this task.
    Args:
        dirname: if True, returns the directory containing the source version rather than the full path to the source version.

    Returns:

    """
    source_po = self.copy(tree="source")
    # Always hand _format_return the ABSOLUTE path; it owns the single
    # root-strip when relative=True (matches get_preview/thumb/msd_path).
    path = source_po.dirname if dirname else source_po.path
    return self._format_return(path, relative, as_posix)

get_render_path

get_render_path(
    dirname=False, relative=False, as_posix=True, **kwargs
) -> Path | str

Returns the path to the render version of this task. Args: dirname: if True, returns the directory containing the render version rather than the full path to the render version.

Returns:

Source code in alc\path\path_object.py
def get_render_path(
    self, dirname=False, relative=False, as_posix=True, **kwargs
) -> Path | str:
    """
    Returns the path to the render version of this task.
    Args:
        dirname: if True, returns the directory containing the render version rather than the full path to the render version.

    Returns:

    """

    render_po = self.copy(tree="render", **kwargs)
    if dirname:
        render_po = render_po.path_at(end_at="resolution", po=True)

    # Always hand _format_return the ABSOLUTE path; it owns the single
    # root-strip when relative=True (was double-stripping before).
    path = render_po.dirname if dirname else render_po.path
    return self._format_return(path, relative, as_posix=as_posix)

unreal_game_path

unreal_game_path(as_posix: bool = True)

In-engine /Game content path for this asset/shot — the Unreal scene organization convention: /Game/{entity_type}/{category}/{asset}/{task}/ {variant} (assets) or …/{sequence}/{shot}/{task}/{variant} (shots).

Built from a minimal token set so the /Game route wins (PathObject's company/project/sync_root/tree defaults would otherwise pull a disk route). Returns the content-browser FOLDER; pair with unreal_asset_name.

Source code in alc\path\path_object.py
def unreal_game_path(self, as_posix: bool = True):
    """In-engine ``/Game`` content path for this asset/shot — the Unreal scene
    organization convention: ``/Game/{entity_type}/{category}/{asset}/{task}/
    {variant}`` (assets) or ``…/{sequence}/{shot}/{task}/{variant}`` (shots).

    Built from a minimal token set so the ``/Game`` route wins (PathObject's
    company/project/sync_root/tree defaults would otherwise pull a disk route).
    Returns the content-browser FOLDER; pair with ``unreal_asset_name``.
    """
    d = {
        "root": "/Game",
        "entity_type": self.entity_type,
        "parent": self.parent,
        "child": self.child,
        "task": self.task,
        "variant": self.variant or "default",
    }
    p = AlchemyRouteEngine().path_from_dict(d).as_posix()
    return p if as_posix else Path(p)

unreal_level_path

unreal_level_path(kind: str = 'TT') -> str

/Game path of a LEVEL (map) belonging to this asset — colocated with the model at {unreal_game_path}/{asset_name}_{kind}_Level so the map is findable by name in the folder structure instead of a loose /Game/… dump (which gets unwieldy fast in Unreal). kind tags the purpose (TT = turntable). Pair with :meth:unreal_sequence_path.

Source code in alc\path\path_object.py
def unreal_level_path(self, kind: str = "TT") -> str:
    """``/Game`` path of a LEVEL (map) belonging to this asset — colocated
    with the model at ``{unreal_game_path}/{asset_name}_{kind}_Level`` so the
    map is findable by name in the folder structure instead of a loose
    ``/Game/…`` dump (which gets unwieldy fast in Unreal). ``kind`` tags the
    purpose (``TT`` = turntable). Pair with :meth:`unreal_sequence_path`.
    """
    return f"{self.unreal_game_path()}/{self.unreal_asset_name}_{kind}_Level"

unreal_sequence_path

unreal_sequence_path(kind: str = 'TT') -> str

/Game path of a LevelSequence belonging to this asset, colocated with the model at {unreal_game_path}/{asset_name}_{kind}_Sequence.

Source code in alc\path\path_object.py
def unreal_sequence_path(self, kind: str = "TT") -> str:
    """``/Game`` path of a LevelSequence belonging to this asset, colocated
    with the model at ``{unreal_game_path}/{asset_name}_{kind}_Sequence``."""
    return f"{self.unreal_game_path()}/{self.unreal_asset_name}_{kind}_Sequence"

unreal_disk_path

unreal_disk_path(as_posix: bool = True)

On-disk Perforce path for this asset/shot inside the Unreal project: {perforce_root}/{company}/{project}/PERFORCE/{world}/Content/ {entity_type}/{category}/{asset}/{task}/{variant}.

The physical content location the /Game path maps to on disk.

Source code in alc\path\path_object.py
def unreal_disk_path(self, as_posix: bool = True):
    """On-disk Perforce path for this asset/shot inside the Unreal project:
    ``{perforce_root}/{company}/{project}/PERFORCE/{world}/Content/
    {entity_type}/{category}/{asset}/{task}/{variant}``.

    The physical content location the ``/Game`` path maps to on disk.
    """
    from alc.config import CFG

    d = {
        "root": str(CFG.perforce_root or ""),
        "company": self.company,
        "project": self.project,
        "sync_root": "PERFORCE",
        "world": self.world or "assets",
        "content_root": self.content_root or "Content",
        "entity_type": self.entity_type,
        "parent": self.parent,
        "child": self.child,
        "task": self.task,
        "variant": self.variant or "default",
    }
    p = AlchemyRouteEngine().path_from_dict(d).as_posix()
    return p if as_posix else Path(p)

get_default_files

get_default_files()

Returns the default file path for the task of this pathObject Returns:

Source code in alc\path\path_object.py
def get_default_files(self):
    """
    Returns the default file path for the task of this pathObject
    Returns:

    """
    from alc.config import CFG

    if not self.task:
        logger.error("No task set in path object to copy default file for")
        return

    task_folder = Path(CFG.cookbook_dir) / "default_files" / self.task
    default_files = list(task_folder.glob("*default*"))
    # find all the files that match *default* in the task folder:
    if not default_files:
        logger.warning(f"No default file found in {task_folder}")
        return None
    return default_files

get_filename

get_filename(ext=None)

The version's working-file name: :attr:version_basename + ext (e.g. 010_SEQ_svz.blend).

Source code in alc\path\path_object.py
def get_filename(self, ext=None):
    """The version's working-file name: :attr:`version_basename` + *ext*
    (e.g. ``010_SEQ_svz.blend``)."""
    if not ext:
        ext = self.ext
        if not ext:
            raise ValueError("No extension provided")
    return f"{self.version_basename}{ext}"

copy_default_files

copy_default_files(overwrite=False)

Seed this version from default_files/{task}/.

Never overwrites an existing working file. The default file is a STARTING point, so it only applies where there is nothing to start from. A version that already holds a file at the destination name is either a version-up carrying the artist's work forward (:meth:start_version -> version_up_minor(copy_source_files=True)) or a version somebody has already worked in — and the studio's start recipe runs CopyDefaultFiles on whatever coordinate it is handed. Without this guard, running start on a carried-forward version replaces the artist's scene with the empty default and says nothing (measured 2026-08-11: a 668,800-byte working file came back as the 667,648-byte default). Pass overwrite=True to deliberately reset a version to the studio default.

Returns the list of files actually written.

Source code in alc\path\path_object.py
def copy_default_files(self, overwrite=False):
    """Seed this version from ``default_files/{task}/``.

    **Never overwrites an existing working file.** The default file is a
    STARTING point, so it only applies where there is nothing to start from.
    A version that already holds a file at the destination name is either a
    version-up carrying the artist's work forward
    (:meth:`start_version` -> ``version_up_minor(copy_source_files=True)``)
    or a version somebody has already worked in — and the studio's start
    recipe runs ``CopyDefaultFiles`` on whatever coordinate it is handed.
    Without this guard, running start on a carried-forward version replaces
    the artist's scene with the empty default and says nothing (measured
    2026-08-11: a 668,800-byte working file came back as the 667,648-byte
    default). Pass ``overwrite=True`` to deliberately reset a version to the
    studio default.

    Returns the list of files actually written.
    """
    default_files = self.get_default_files()
    if not default_files:
        return []
    written = []
    dest_folder = self.get_source_path(dirname=True, relative=False, as_posix=False)
    for default_file in default_files:
        filename = self.get_filename(ext=default_file.suffix)
        dest_path = dest_folder / filename
        if dest_path.exists() and not overwrite:
            logger.info(
                "default file skipped, %s already exists (not overwriting "
                "existing work)",
                dest_path,
            )
            continue
        logger.info("copying default file %s -> %s", default_file, dest_path)
        shutil.copy(default_file, dest_path)
        written.append(dest_path)
    return written

create_empty_version

create_empty_version(
    status="ip", default_file=True, comment=""
)

Creates the current path object as a version on disk.

comment is the ARTIST's statement about this version (#3796). It rides through to the task register's VersionPointer — the authority the version view reads — because a PathObject has no comment field for update_user_versions to pick up, which is why every pointer in the register carried an empty comment. Machine-minted versions pass nothing and stay blank.

Raises:

Type Description
ValueError

if this coordinate has no resolution.

Returns:

Source code in alc\path\path_object.py
def create_empty_version(self, status="ip", default_file=True, comment=""):
    """
    Creates the current path object as a version on disk.

    ``comment`` is the ARTIST's statement about this version (#3796). It
    rides through to the task register's ``VersionPointer`` — the authority
    the version view reads — because a PathObject has no ``comment`` field
    for ``update_user_versions`` to pick up, which is why every pointer in
    the register carried an empty comment. Machine-minted versions pass
    nothing and stay blank.

    Raises:
        ValueError: if this coordinate has no ``resolution``.

    Returns:

    """
    self._require_resolution("create_empty_version")
    new_folder = self.get_source_path(dirname=True, as_posix=False)
    msd_path = Path(self.version_msd.path)
    if new_folder.exists():
        if not msd_path.exists():
            self.version_msd.commit()
            self.version_msd.update_status(
                status,
                comment="Status updates with PathObject().create_empty_version()",
            )
            tmsd = self.task_msd
            tmsd.update_user_versions(po=self, status="ip", comment=comment)
        else:
            # Folder + version MSD exist but the version may still be missing
            # from the task register (e.g. an interrupted start, or folders
            # made by an older code path). Heal the register instead of
            # silently returning, or the UI shows no versions and the
            # auto-start loop re-runs forever.
            tmsd = self.task_msd
            user_group = tmsd.user_versions.get(self.user)
            if not user_group or self.version not in user_group.version_map:
                tmsd.update_user_versions(po=self, status="ip", comment=comment)
        return new_folder
    else:
        self.makedirs()
        if default_file:
            self.copy_default_files()
    self.version_msd.commit()
    self.version_msd.update_status(
        status, comment="Status updates with PathObject().create_empty_version()"
    )
    # update the task models with the new version
    tmsd = self.task_msd
    tmsd.update_user_versions(po=self, status="ip", comment=comment)
    return self.path

start

start(software='alchemy')

Bring this task version into existence by running the start cookbook recipe: create the source + render folders, copy the task's default files, register the version in Alchemy (version + task MSDs), and — if a tracker is configured — register the context and set it in-progress in ShotGrid.

Resolves the task-specific alchemy/start/{task} recipe, falling back to the shared alchemy/start/default. This replaces the imperative create_empty_version path so start is a first-class recipe in the start → build → render → review → publish arc (every task inherits the default unless it ships its own).

Source code in alc\path\path_object.py
def start(self, software="alchemy"):
    """Bring this task version into existence by running the ``start`` cookbook
    recipe: create the source + render folders, copy the task's default files,
    register the version in Alchemy (version + task MSDs), and — if a tracker
    is configured — register the context and set it in-progress in ShotGrid.

    Resolves the task-specific ``alchemy/start/{task}`` recipe, falling back to
    the shared ``alchemy/start/default``. This replaces the imperative
    ``create_empty_version`` path so ``start`` is a first-class recipe in the
    ``start → build → render → review → publish`` arc (every task inherits the
    default unless it ships its own)."""
    if not self.resolution:
        raise ValueError(
            "Full path object with resolution is needed to start a task"
        )
    from alc.cookbook.recipe import Recipe
    from alc.cookbook.preflight.cli_runner import run_preflights

    # Recipe.resolve owns the fallback semantics (task → default), including
    # skipping step-less stub recipes that would otherwise shadow the default
    # and "finish" without registering the version anywhere.
    recipe = Recipe.resolve(software, "start", self.task)
    if recipe is None:
        raise FileNotFoundError(
            f"No start recipe resolves for task '{self.task}' "
            f"(software '{software}')"
        )
    return run_preflights(recipe.path_string, self.path)

ensure_started

ensure_started(software='alchemy')

Make sure THIS coordinate's user has a version rail, starting the task if they don't. Idempotent, and the single home for the "materialize the task the first time someone opens it" decision.

Callers used to spell this inline as if not po.task_msd.user_versions: po.start(), which tests whether the register is empty rather than whether this person has anything — see :meth:alc.models.task.TaskManifest.has_versions_for. A task holding only a publish entry therefore skipped the start forever, leaving the version view with a published chip and no versions to select.

Returns True when a start was actually run. The publish pseudo-user never starts anything — it names the published snapshot, not a rail.

The start always happens at FIRST_VERSION, never at self.version. The coordinate that triggers this routinely carries a publish-tree token — ?v=latest, the address of the published snapshot — and starting on it would mint VERSIONS/.../{user}/latest/, a user version named "latest" that no version comparison can order and that every +version would then build on.

Source code in alc\path\path_object.py
def ensure_started(self, software="alchemy"):
    """Make sure THIS coordinate's user has a version rail, starting the task
    if they don't. Idempotent, and the single home for the "materialize the
    task the first time someone opens it" decision.

    Callers used to spell this inline as
    ``if not po.task_msd.user_versions: po.start()``, which tests whether the
    register is empty rather than whether *this person* has anything — see
    :meth:`alc.models.task.TaskManifest.has_versions_for`. A task holding only
    a ``publish`` entry therefore skipped the start forever, leaving the
    version view with a published chip and no versions to select.

    Returns True when a start was actually run. The ``publish`` pseudo-user
    never starts anything — it names the published snapshot, not a rail.

    The start always happens at ``FIRST_VERSION``, never at ``self.version``.
    The coordinate that triggers this routinely carries a publish-tree token
    — ``?v=latest``, the address of the published snapshot — and starting on
    it would mint ``VERSIONS/.../{user}/latest/``, a user version named
    "latest" that no version comparison can order and that every ``+version``
    would then build on.
    """
    from alc.models.task import PUBLISH_USER

    if not self.user or str(self.user).lower() == PUBLISH_USER:
        return False
    if self.task_msd.has_versions_for(self.user):
        return False
    self.copy(version=FIRST_VERSION).start(software=software)
    return True

version_up_minor

version_up_minor(
    copy_source_files=False,
    status="ip",
    comment=AUTO_VERSION_COMMENT,
)

Mint the next minor version and log what actually happened (#3796).

The event used to say from_version=self.version — the version the CALLER happened to be sitting on, which for the orchestrator is the default 000.000 every single time, so fourteen version-ups all claimed the same origin and none recorded the result. The real origin is the task's latest version on disk (the rung this one is stacked on), and the result is the version being created; both go on the event, so the stream can be reconciled against the version MSD authority.

Raises:

Type Description
ValueError

if this coordinate has no resolution. Checked BEFORE anything is written, because the copy below resolves its destination from the same tokens — with no resolution it lands in the user folder, next to the version directories rather than inside one.

Source code in alc\path\path_object.py
def version_up_minor(
    self,
    copy_source_files=False,
    status="ip",
    comment=AUTO_VERSION_COMMENT,
):
    """Mint the next minor version and log what actually happened (#3796).

    The event used to say ``from_version=self.version`` — the version the
    CALLER happened to be sitting on, which for the orchestrator is the
    default ``000.000`` every single time, so fourteen version-ups all
    claimed the same origin and none recorded the result. The real origin
    is the task's latest version on disk (the rung this one is stacked on),
    and the result is the version being created; both go on the event, so
    the stream can be reconciled against the version MSD authority.

    Raises:
        ValueError: if this coordinate has no ``resolution``. Checked BEFORE
            anything is written, because the copy below resolves its
            destination from the same tokens — with no resolution it lands in
            the user folder, next to the version directories rather than
            inside one.
    """
    self._require_resolution("version_up_minor")
    latest_obj = self.latest_version_object  # the rung we are stacking on
    next_minor_obj = self.next_minor_object
    artist_comment = "" if comment == AUTO_VERSION_COMMENT else (comment or "")
    print("creating version", next_minor_obj.path)
    if not copy_source_files:
        print("creating empty version with default files")
        next_minor_obj.create_empty_version(
            status=status, default_file=True, comment=artist_comment
        )
    else:
        print("copying source files")
        next_minor_obj.create_empty_version(
            status=status, default_file=False, comment=artist_comment
        )
        source_files = self.source_files
        print(source_files)
        dest_folder = next_minor_obj.get_source_path(dirname=True, as_posix=False)
        # The version directory is the contract for this copy: writing into a
        # folder create_empty_version did not make means writing outside a
        # version. Better to say so than to scatter files.
        if not dest_folder.is_dir():
            raise RuntimeError(
                f"[version_up_minor] refusing to copy source files: the "
                f"version directory was not created: {dest_folder}"
            )
        for source_file in source_files:
            dest_path = dest_folder / Path(source_file).name
            print(source_file, dest_path)

            cgl_copy(source_file, dest_path)
    # Telemetry: every minor version-up is an event (publishes were already
    # tracked; this captures the versions created BETWEEN publishes). Best-
    # effort — a telemetry failure must never break version creation.
    try:
        next_minor_obj.event_service.log_event(
            action="version | create",
            name="create",
            recipe_type="version",
            version=next_minor_obj.version,
            details={
                "from_version": latest_obj.version,
                "to_version": next_minor_obj.version,
                "copy_source": copy_source_files,
                "comment": comment,
                # did a PERSON say this, or did a run mint a container?
                "auto": not artist_comment,
            },
        )
    except Exception:
        logger.warning(
            "telemetry: failed to log version create for %s",
            next_minor_obj.task_id,
            exc_info=True,
        )
    return next_minor_obj

start_version

start_version(
    copy_source_files=True,
    status="ip",
    comment=AUTO_VERSION_COMMENT,
    require_default_file=True,
)

The start mechanism: the version a task should be opened at.

Every DCC's start phase asks the same question and it is not worth answering twice: is there already work on disk?

  • Versions exist -> version UP from the latest, carrying its source files forward, so the artist continues from what is there.
  • No versions -> seed 000.000 from default_files/{task}/, so the task begins from the studio's configured scene rather than whatever happened to be open.

Three traps this exists to own, so no recipe step has to know them:

  • version_up_minor copies its own source_files, not the latest version's. Called on any other coordinate it mints the right version number and fills it from the wrong version, silently. It must be called on :attr:latest_version_object, which is what happens here.
  • With no versions AND no default file, the honest outcome is a hard error naming the missing file — not an empty version that looks like a successful start and hands the artist a blank scene. Pass require_default_file=False for a task that legitimately has none.
  • A coordinate parsed at the USER level carries no resolution, and every path below the version token then resolves one level too high. Checked here so the error names the coordinate the caller passed in.

Returns the :class:PathObject for the version to open.

Raises:

Type Description
ValueError

if this coordinate has no resolution.

FileNotFoundError

if there is nothing on disk and no default file.

Source code in alc\path\path_object.py
def start_version(
    self,
    copy_source_files=True,
    status="ip",
    comment=AUTO_VERSION_COMMENT,
    require_default_file=True,
):
    """The **start** mechanism: the version a task should be opened at.

    Every DCC's ``start`` phase asks the same question and it is not worth
    answering twice: is there already work on disk?

    * **Versions exist** -> version UP from the latest, carrying its source
      files forward, so the artist continues from what is there.
    * **No versions** -> seed ``000.000`` from ``default_files/{task}/``, so
      the task begins from the studio's configured scene rather than whatever
      happened to be open.

    Three traps this exists to own, so no recipe step has to know them:

    * ``version_up_minor`` copies **its own** ``source_files``, not the
      latest version's. Called on any other coordinate it mints the right
      version number and fills it from the wrong version, silently. It must
      be called on :attr:`latest_version_object`, which is what happens here.
    * With no versions AND no default file, the honest outcome is a hard
      error naming the missing file — not an empty version that looks like a
      successful start and hands the artist a blank scene. Pass
      ``require_default_file=False`` for a task that legitimately has none.
    * A coordinate parsed at the USER level carries no ``resolution``, and
      every path below the version token then resolves one level too high.
      Checked here so the error names the coordinate the caller passed in.

    Returns the :class:`PathObject` for the version to open.

    Raises:
        ValueError: if this coordinate has no ``resolution``.
        FileNotFoundError: if there is nothing on disk and no default file.
    """
    self._require_resolution("start_version")
    if self.versions:
        return self.latest_version_object.version_up_minor(
            copy_source_files=copy_source_files, status=status, comment=comment
        )

    from alc.config import CFG

    first = self.copy(version=FIRST_VERSION)
    if require_default_file and not first.get_default_files():
        raise FileNotFoundError(
            f"no versions on disk for {self.task_id} and no default file to "
            f"seed one from — expected something matching '*default*' in "
            f"{Path(CFG.cookbook_dir) / 'default_files' / (self.task or '?')}. "
            "Add a default file for this task, or call start_version("
            "require_default_file=False) if this task legitimately has none."
        )
    artist_comment = "" if comment == AUTO_VERSION_COMMENT else (comment or "")
    first.create_empty_version(
        status=status, default_file=True, comment=artist_comment
    )
    return first

run_cookbook

run_cookbook(
    recipe_types=None,
    deadline=True,
    dependency_ids=None,
    sub_dependencies=False,
    job_name=None,
    batch_name=None,
    software=None,
)

Run this task's cookbook (build -> publish -> ...).

Parameters:

Name Type Description Default
recipe_types list[str] | None

recipe order to run (default ['build', 'publish']).

None
deadline bool

submit to the farm instead of running locally.

True
dependency_ids list[str] | None

upstream Deadline job IDs to wait on — chain task cookbooks (dsn -> mdl -> rig) by passing the previous job id.

None
sub_dependencies bool

when False (default), the WHOLE cookbook is a single Deadline job (no per-step jobs / heartbeat waits). When True, each step is its own job chained by dependency (the original granular behaviour).

False
job_name str | None

Deadline Monitor job label (e.g. "[ALC Render] chr/genMan mdl (turntable 1-180)"); defaults to a generic [ALC Cookbook] … when omitted.

None
batch_name str | None

Deadline Monitor batch label; same default behaviour as job_name.

None

Returns:

Type Description

str | None: On Deadline, the job id (for chaining). Locally, None.

Source code in alc\path\path_object.py
def run_cookbook(
    self,
    recipe_types=None,
    deadline=True,
    dependency_ids=None,
    sub_dependencies=False,
    job_name=None,
    batch_name=None,
    software=None,
):
    """Run this task's cookbook (build -> publish -> ...).

    Args:
        recipe_types (list[str] | None): recipe order to run (default
            ``['build', 'publish']``).
        deadline (bool): submit to the farm instead of running locally.
        dependency_ids (list[str] | None): upstream Deadline job IDs to
            wait on — chain task cookbooks (dsn -> mdl -> rig) by passing
            the previous job id.
        sub_dependencies (bool): when False (default), the WHOLE cookbook
            is a single Deadline job (no per-step jobs / heartbeat waits).
            When True, each step is its own job chained by dependency (the
            original granular behaviour).
        job_name (str | None): Deadline Monitor job label (e.g.
            ``"[ALC Render] chr/genMan mdl (turntable 1-180)"``); defaults
            to a generic ``[ALC Cookbook] …`` when omitted.
        batch_name (str | None): Deadline Monitor batch label; same
            default behaviour as ``job_name``.

    Returns:
        str | None: On Deadline, the job id (for chaining). Locally, None.
    """
    if not self.resolution:
        logger.warning(
            "Run cookbook can only be run on full paths with resolution, \n\tCurrent Path:{}".format(
                self.path
            )
        )
        return None
    if recipe_types is None:
        recipe_types = ["build", "publish"]

    # Default farm path: submit the entire cookbook as ONE job.
    if deadline and not sub_dependencies:
        from alc.services.deadline.create import deadline_submit_cookbook

        return deadline_submit_cookbook(
            self,
            recipe_types=recipe_types,
            dependency_ids=dependency_ids,
            job_name=job_name,
            batch_name=batch_name,
            software=software,
        )

    # Local run, or the granular per-step Deadline path (sub_dependencies=True).
    cookbook = self._get_cookbook(software=software)
    if cookbook:
        return cookbook.run_task(
            path_object=self,
            recipe_types=recipe_types,
            deadline=deadline,
            dependency_ids=dependency_ids,
            sub_dependencies=sub_dependencies,
        )
    return None

copy_files

copy_files(
    dest_po: PathObject,
    status: str = "wtg",
    description: str = "Version Created with Alchemy",
    new_msd: bool = False,
    render: bool = True,
    status_comment: str = "Status auto-updated with Alchemy",
)

Copies all the data from the current path object into the dest_po's folder structure. Args: dest_po: destination pathObject status: description: new_msd: render:

Returns:

Source code in alc\path\path_object.py
def copy_files(
    self,
    dest_po: PathObject,
    status: str = "wtg",
    description: str = "Version Created with Alchemy",
    new_msd: bool = False,
    render: bool = True,
    status_comment: str = "Status auto-updated with Alchemy",
):
    """
    Copies all the data from the current path object into the dest_po's folder structure.
    Args:
        dest_po: destination pathObject
        status:
        description:
        new_msd:
        render:

    Returns:

    """
    source_dir = self.get_source_path(dirname=True)
    dest_dir = dest_po.get_source_path(dirname=True)

    print("Copying source files", source_dir, dest_dir)
    cgl_copy(
        source_dir,
        dest_dir,
    )

    if render:
        cgl_copy(
            self.get_render_path(dirname=True),
            dest_po.get_render_path(dirname=True),
        )
        os.remove(
            dest_po.msd_path
        )  # we don't want the models file in the copy, it needs to be clean.
    else:
        dest_po.get_render_path(dirname=True, as_posix=False).mkdir(
            parents=True, exist_ok=True
        )
    # ``version_msd`` is a fresh-load property — every access returns a NEW
    # manifest read from disk. Hold ONE instance so the data payload we copy
    # below is committed on the same object we set it on (otherwise the
    # assignment lands on a throwaway instance and the following commit
    # re-loads an empty manifest, silently dropping ``data`` at publish time).
    dest_msd = dest_po.version_msd
    dest_msd.commit()
    dest_msd.update_status(status, comment=status_comment)
    if not new_msd:
        # we're copying values from this models to the next.
        dest_msd.data = self.version_msd.data
        dest_msd.commit()
    tmsd = self.task_msd
    tmsd.update_user_versions(po=dest_po, status=status)

apply_recipe_step

apply_recipe_step(software: str, script: str) -> int

Retrieves the execution packet from the DCC driver and executes it headlessly via a monitored subprocess.

Parameters:

Name Type Description Default
software str

The target DCC (e.g., 'blender', 'maya')

required
script str

Path to the script to execute inside the DCC

required

Returns:

Name Type Description
int int

The exit code of the subprocess (0 for success)

Source code in alc\path\path_object.py
def apply_recipe_step(self, software: str, script: str) -> int:
    """
    Retrieves the execution packet from the DCC driver and executes
    it headlessly via a monitored subprocess.

    Args:
        software (str): The target DCC (e.g., 'blender', 'maya')
        script (str): Path to the script to execute inside the DCC

    Returns:
        int: The exit code of the subprocess (0 for success)
    """

    if not self.path:
        logger.error("Cannot apply script: PathObject has no valid file path.")
        return 1

    # 1. Resolve the formulaic driver module
    try:
        setup_module_string = f"alc.plugins.{software}.setup"
        setup_module = import_module(setup_module_string)
        # Expecting a dict back: {'command': [...], 'env': {...}}
        print(setup_module_string)
        print(type(script))
        setup_module.run_recipe(self.path.as_posix(), script)
    except (ModuleNotFoundError, AttributeError) as e:
        logger.error(f"Failed to load setup driver for {software}: {e}")
        return 1