synology_apm_repo.sdk.units.node_ref module¶
NodeRef — a stable, round-trippable string address for any browsable
node. CLI arguments, TUI breadcrumbs/bookmarks, and error messages all
share this one addressing scheme instead of a pile of mutually-exclusive
flags.
Three segment shapes share the same <repo-path>#<seg>/<seg>/...
structure — which shape a ref is is entirely a property of its
segments’ content, not a separate stored field:
shape |
example |
|---|---|
canonical ( |
|
raw ( |
|
human (display names, everything else) |
|
Canonical refs are stable across display-name changes — the right form
for scripts and bookmarks. Human refs are what breadcrumbs and ls
show; disambiguate appends a hash suffix when two same-level nodes
would otherwise show the same name.
Escaping: a segment may itself contain #, /, %, or control
characters — each is percent-encoded before segments join with /,
decoded back on split, so round-tripping through str(ref) /
NodeRef.parse is always safe. repo_path itself is never encoded
(split off at the first #), so a literal # inside it can’t
round-trip — accepted, since real filesystem paths essentially never
contain one. A lone empty-string segment (NodeRef(path, ("",))) also
isn’t representable, since it encodes identically to no segments at all
— no real display name or path component is ever the empty string, so
this never arises.
- class synology_apm_repo.sdk.units.node_ref.RefKind(value)¶
Bases:
EnumWhich of the three segment shapes a
NodeRefis: canonical (cat:/wl:/ver:prefixed), raw (file_mapfallback axis, diagnostic-mode only), or human (display names, everything else).- CANONICAL = 'canonical'¶
- RAW = 'raw'¶
- HUMAN = 'human'¶
- class synology_apm_repo.sdk.units.node_ref.NodeRef(repo_path, segments=())¶
Bases:
objectrepo_pathis a store-relative or filesystem path to the repository root (unencoded — it is a path, not ref content);segmentsare the decoded logical path components after the#.- classmethod parse(text)¶
Inverse of
__str__.- Raises:
ValueError –
texthas no#at all (not a ref).
- property canonical_ids: tuple[CatalogId, WorkloadId, VersionUid] | None¶
(catalog_id, workload_id, version_uid)if this is a canonical ref with a well-formed prefix, elseNone.catalog_idis a plain string, never parsed as an int here — an object-storageCatalogIdis a repo-id string, not always an integer the way a vault’sconnection_config_idis.
- property extra_segments: tuple[str, ...]¶
Segments beyond the fixed prefix — the provider-internal path (mail folder, mail subject, …) for canonical refs; the
file_mappath’s own components (one per path segment — join with"/"to recover the literal path) for raw; everything, for human refs.
- classmethod canonical(repo_path, *, catalog_id, workload_id, version_uid, extra=())¶
- classmethod raw(repo_path, file_map_path)¶
- classmethod human(repo_path, *names)¶
- child(*extra)¶
Extends this ref’s own segments by
extra— for a provider appending one or more segments to an already-resolved node’s own ref (a diagnostic leaf, a synthetic partition/entry node, …), not the version root (canonical_ref_forcovers that case) — every call site uses this instead of hand-rollingNodeRef(node.ref.repo_path, (*node.ref.segments, "x")).
- synology_apm_repo.sdk.units.node_ref.canonical_ref_for(repo, version, extra=())¶
NodeRef.canonicalforversion, withinrepo— the(repo.layout.repo_root, catalog_id, workload_id, version_uid)prefix every provider’s ownref_for/_refmethod shares; onlyextradiffers per call site.catalog_id(identifiers.resolve_catalog_id’s shared formula) isrepo.layout.repo_idwhen set — object storage’s own repo-id, unique per bucket by construction — falling back toversion.connection_config_idotherwise: always correct for a vault (whoseconnection_config_idis already unique within it, and whoseRepoLayout.repo_idis alwaysNone), and also correct for the one object-storage edge case with no derivable repo-id (seestorage.layout’s_data_dir_ancestor) — that case only arises when thisDedupRepois the sole catalog reachable from here anyway, so nothing else could collide with it.
- synology_apm_repo.sdk.units.node_ref.catalog_pairs(catalogs)¶
(display_name, catalog_id)pairs, incatalogs’ own order — feedsdisambiguate()/match_display_name(), shared byRepository.walk_human_refand the CLI’sls/tree.
- synology_apm_repo.sdk.units.node_ref.workload_pairs(workloads)¶
Same shape as
catalog_pairs, forWorkload, plus each workload’stype_hint— feedsdisambiguate()’shintsalongside itspairs.
- synology_apm_repo.sdk.units.node_ref.version_pairs(versions)¶
Same shape as
catalog_pairs, forVersion.
- synology_apm_repo.sdk.units.node_ref.disambiguate(names_and_ids, *, hints=None)¶
Append a disambiguating suffix to every name that collides with another at the same tree level (the same call’s list); names with no collision are returned unchanged.
names_and_idsis(display_name, canonical_id)— the stable id, never the display name, is what the hash fallback is derived from, so a suffix stays the same across renames.hints(parallel,Nonewhere inapplicable) is a human differentiator such assub_type: one GWS/M365 account can produce severalWorkloadrows sharing onedisplay_name(the account’s name/email), so a hash alone reads as duplicates. A hint that resolves the collision is used instead ("Alice Example <...> · MAIL"); the hash still applies, appended after the hint, to whatever the hint doesn’t resolve.
- synology_apm_repo.sdk.units.node_ref.disambiguate_catalogs(catalogs)¶
Disambiguated display names for
catalogs, in the same order — thecatalog_pairs()+disambiguate()sequence the CLI’s own catalog-listing helpers and the browser’s own catalog-tree selector each built independently before this helper existed. Zipping the result back againstcatalogs(or any other parallel sequence) is left to the caller — consumers zip against different shapes (live domain objects, index pairs).
- synology_apm_repo.sdk.units.node_ref.disambiguate_workloads(workloads, *, use_type_hint=True)¶
Same idea as
disambiguate_catalogs, forWorkload, folding inworkload_pairs()’s owntype_hintdisambiguation hint by default.use_type_hint=Falseskips it — for a caller whose ownworkloadsare already grouped bytype_hintbefore reaching here (the browser’s own per-sub_type leaf list): every sibling there already shares onetype_hint, so the hint could never actually differentiate any of them, and showing it again in the display name would just repeat what the grouping already conveys.
- synology_apm_repo.sdk.units.node_ref.disambiguate_versions(versions)¶
Same idea as
disambiguate_catalogs, forVersion.
- synology_apm_repo.sdk.units.node_ref.match_display_name(target, pairs, objects, *, hints=None)¶
Match
targetagainst the displayed form ofpairs((display_name, stable_id)) after running the collision suffix throughdisambiguate— the same transform the CLI/TUI apply before showing these names, so a name copied straight out of a breadcrumb resolves back exactly, suffix included when one collided.hintsmust match whatever the display side passed to its owndisambiguatecall for these samepairs— workload resolution passes each workload’s owntype_hinthere for exactly that reason, so a ref showing"Alice Example <...> · MAIL"resolves back to the Mail workload, not just any workload with that name.
- synology_apm_repo.sdk.units.node_ref.ambiguous_matches(target, pairs, *, hints=None)¶
The disambiguated names among
pairswhose pre-suffix display name equalstarget. Meant to be called only aftermatch_display_namealready returnedNonefor the sametarget/pairs/hints— under that precondition, a non-empty result meanstargetcollided at this level and needed a suffix (real ambiguity), while an empty result meanstargetgenuinely doesn’t exist here at all. Called on its own, outside that precondition, it’s a plain filter with no such guarantee: atargetthat matches exactly one row’s raw name comes back as a one-element list even thoughmatch_display_namewould have already resolved it cleanly.