exlab_wizard.template.authoring#
Author-time template service: scaffold, edit, and validate templates.
This is the non-UI backend the GUI template authoring form (Frontend Spec
§5) drives. It owns every mutation of a template directory under
config.paths.templates_dir so the NiceGUI page stays a thin view:
create_template_dir()– scaffold a new minimal Copier template (the same shapeexlab_wizard.ui.pages.templates.create_template()historically produced, now routed through here so the page can delegate).read_manifest()/write_manifest()– round-trip the structuredTemplateManifestthroughcopier.yml, gating every write on the sharedexlab_wizard.template.lintrule set.read_content()/write_content_file()/upload_file()– read and write the template’s content files (*.jinjaand the editable text allowlist), with a Jinja2 parse gate on*.jinjasaves and size / file-count caps on uploads.rename_path()/delete_path()– move and remove files within the template, never touchingcopier.ymlon delete.list_files()– a pure directory walk the GUI tree consumes.
Two invariants run through the whole module:
Every new or edited path is resolved through
_safe_target(), the single chokepoint that rejects traversal (..), absolute paths, path separators inside a segment, Windows-reserved / control / non-ASCII segment names, and any resolved path that escapes the template root. The per-segment rule reusesexlab_wizard.paths.project_name_violations()so author-time filenames obey the same filesystem-safety contract as project names.Every write goes through
exlab_wizard.io.atomic_write_bytes()– the temp-file +fsync+os.replacerecipe – so a crash mid-write never leaves a half-writtencopier.ymlor content file.
The write_manifest() / write_content_file() family also take an
optional expected_stat tuple (st_mtime, st_size) captured at read
time; if the on-disk file changed since, the write raises
StaleEditError rather than clobbering a concurrent edit (the
optimistic-concurrency guard the GUI surfaces as “reload, your copy is
stale”).
Functions
|
Scaffold a new minimal Copier template under |
|
Delete an in-template file or directory at |
|
Return |
|
Walk |
|
Read an editable text file as UTF-8 and return its content + stat. |
|
Read |
|
Move an in-template path from |
|
Write uploaded |
|
Write |
|
Serialise |
Exceptions
Raised when an optimistic-concurrency write loses a stat race. |
|
Raised on an author-time template-edit failure. |
|
Raised when a requested path is not a safe in-template location. |
- exception exlab_wizard.template.authoring.StaleEditError[source]#
Bases:
TemplateAuthoringErrorRaised when an optimistic-concurrency write loses a stat race.
The file changed on disk between the caller’s read (which captured
expected_stat) and the write, so applying the edit would clobber a concurrent change. The GUI surfaces this as “reload – your copy is stale” rather than silently overwriting.
- exception exlab_wizard.template.authoring.TemplateAuthoringError[source]#
Bases:
ExLabErrorRaised on an author-time template-edit failure.
Covers lint-rejected manifest saves, Jinja-syntax-rejected content saves, over-cap uploads, and edits to non-editable / disallowed paths. The two narrower failures below subclass this so callers can catch the specific case or the whole family.
- exception exlab_wizard.template.authoring.UnsafePathError[source]#
Bases:
TemplateAuthoringErrorRaised when a requested path is not a safe in-template location.
Covers traversal (
..), absolute paths, path separators or Windows-reserved / control / non-ASCII characters inside a segment, and any resolved target that escapes the template root.
- exlab_wizard.template.authoring.create_template_dir(base_dir, *, name, template_type, description='', run_scope=None)[source]#
Scaffold a new minimal Copier template under
base_dir.Writes
<base_dir>/<name>/copier.yml(serialised from aTemplateManifestso the author-time and structured-edit paths emit byte-identical YAML) plus onenotes.md.jinjacontent file. Both writes go throughatomic_write_bytes(). The result is immediately loadable byTemplateEngine.- Parameters:
base_dir (
Path) – Thetemplates_dirthe new template is created under.name (
str) – The template directory name. Stripped of surrounding whitespace, then validated as a single safe filesystem segment viaproject_name_violations().template_type (
str) – One ofTemplateTypevalues.description (
str) – Free-form_exlab_descriptiontext (stripped).run_scope (
str|None) – Required forruntemplates; one ofRunScopevalues. Must beNone/ unused otherwise.
- Return type:
- Returns:
The new template’s root directory.
- Raises:
ValueError – Empty / duplicate
name, unknowntemplate_type, or a run template missing / with an invalidrun_scope.UnsafePathError –
nameis not a safe single filesystem segment.
- exlab_wizard.template.authoring.delete_path(template_dir, rel)[source]#
Delete an in-template file or directory at
rel.Resolved through
_safe_target(). A file isunlink-ed, a directory is removed recursively withshutil.rmtree()(only ever within the template root).copier.ymlmay not be deleted.- Parameters:
- Raises:
UnsafePathError –
relis not a safe in-template path.TemplateAuthoringError –
reliscopier.ymlor does not exist.
- Return type:
- exlab_wizard.template.authoring.is_editable(path)[source]#
Return
Trueifpathis text the GUI may edit inline.The decision is purely by suffix against
EDITABLE_SUFFIXES(case-insensitive); the file need not exist. Afoo.md.jinjais editable (its final suffix.jinjais in the set), as is a bare.md/.csv/.json; a.xlsx/.pngis not.
- exlab_wizard.template.authoring.list_files(template_dir)[source]#
Walk
template_dirand return a sorted entry list for the GUI tree.Each entry is
{"rel": str, "is_dir": bool, "editable": bool, "size": int}–relis the POSIX-style path relative to the template root,editableisis_editable()(alwaysFalsefor directories), andsizeis the file size in bytes (0for directories). Entries are sorted byrelfor a stable tree.
- exlab_wizard.template.authoring.read_content(path)[source]#
Read an editable text file as UTF-8 and return its content + stat.
- Parameters:
path (
Path) – The file to read. Its suffix must be inEDITABLE_SUFFIXES.- Return type:
- Returns:
(text, (st_mtime, st_size))– the stat is the optimistic-concurrency signature for a laterwrite_content_file().- Raises:
TemplateAuthoringError – The suffix is not editable, or the file is missing / unreadable / not valid UTF-8.
- exlab_wizard.template.authoring.read_manifest(template_dir)[source]#
Read
copier.ymland return the parsed manifest + its stat signature.The returned
(st_mtime, st_size)tuple is passed back towrite_manifest()asexpected_statto detect a concurrent edit. The manifest is parsed viaTemplateManifest.from_yaml(), which is tolerant of missing_exlab_*keys.- Parameters:
template_dir (
Path) – The template root.- Return type:
- Returns:
(manifest, (st_mtime, st_size)).- Raises:
TemplateAuthoringError –
copier.ymlis missing or unreadable.
- exlab_wizard.template.authoring.rename_path(template_dir, src_rel, dst_rel)[source]#
Move an in-template path from
src_reltodst_rel.Both ends are resolved through
_safe_target(), so neither may escape the template root. The move isos.replace(atomic on the same filesystem);copier.ymlmay not be renamed away.- Parameters:
- Return type:
- Returns:
The resolved absolute destination path.
- Raises:
UnsafePathError – Either end is not a safe in-template path.
TemplateAuthoringError –
src_reliscopier.ymlor does not exist.
- exlab_wizard.template.authoring.upload_file(template_dir, filename, data, *, render_as_template=False)[source]#
Write uploaded
datainto the template asfilename.The filename is resolved through
_safe_target()(so a traversal or absolute path is rejected). Whenrender_as_templateis set and the name is not already*.jinja, a.jinjasuffix is appended so Copier renders the file. Two caps gate the write:the upload may not exceed
TEMPLATE_UPLOAD_MAX_BYTES;the template may not already hold
TEMPLATE_MAX_FILESfiles.
A
.jinjaupload that decodes as UTF-8 text is Jinja-parse-checked (a binary.jinja– unusual but possible – skips the parse). The write goes throughatomic_write_bytes().- Parameters:
- Return type:
- Returns:
The resolved absolute path written.
- Raises:
UnsafePathError –
filenameis not a safe in-template path.TemplateAuthoringError – The upload exceeds the size cap, the template is at the file-count cap, or a UTF-8
.jinjaupload has a Jinja syntax error.
- exlab_wizard.template.authoring.write_content_file(template_dir, rel, text, *, expected_stat=None)[source]#
Write
textto an in-template content file atrel.The target is resolved through
_safe_target(). Whenrelends in.jinjathe text is parsed with Jinja2 first; a syntax error refuses the save with aTemplateAuthoringError(so a broken template never lands on disk). The write itself goes throughatomic_write_bytes().- Parameters:
template_dir (
Path) – The template root.rel (
str) – The in-template relative path to write.text (
str) – The UTF-8 content to write.expected_stat (
tuple[float,int] |None) – Optional(st_mtime, st_size)fromread_content(); if given and the on-disk file differs,StaleEditErroris raised before any write.
- Return type:
- Returns:
The resolved absolute path written.
- Raises:
UnsafePathError –
relis not a safe in-template path.StaleEditError –
expected_statgiven and the file changed.TemplateAuthoringError – A
.jinjatarget with a syntax error.
- exlab_wizard.template.authoring.write_manifest(template_dir, manifest, *, expected_stat=None)[source]#
Serialise
manifesttocopier.yml, gated on the lint rule set.The manifest is rendered with
TemplateManifest.to_yaml(), then re-parsed and run throughexlab_wizard.template.lint.lint_manifest_dict(). If any finding is an ERROR the file is not written and aTemplateAuthoringErrorcarrying the joined error messages is raised, so a manifest thatTemplateEngine.resolvewould reject can never be saved. WARN findings do not block the save. On success the bytes are written throughatomic_write_bytes().- Parameters:
template_dir (
Path) – The template root.manifest (
TemplateManifest) – The manifest to serialise.expected_stat (
tuple[float,int] |None) – Optional(st_mtime, st_size)fromread_manifest(); if given and the on-disk file differs,StaleEditErroris raised before any write.
- Raises:
StaleEditError –
expected_statgiven and the file changed.TemplateAuthoringError – The serialised manifest has lint ERRORs.
- Return type: