# Copyright 2025–2026 European Union
# Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
# SPDX-License-Identifier: EUPL-1.2
"""
Project scaffolding service for the ``mafw init`` command.
This package turns a validated :class:`~mafw.scaffold.spec.ProjectSpec` into a
ready-to-use MAFw analysis project on disk. It is deliberately separated from the
command line presentation layer in :mod:`mafw.scripts.mafw_exe`: the service
validates inputs, renders the canonical template bundled with MAFw, and writes the
result atomically, while the command line layer only parses options, prompts the
user, and reports results.
The public entry point is :func:`generate_project`. The typed error hierarchy
(:class:`~mafw.scaffold.errors.ScaffoldError` and subclasses) lets the caller map
every failure to a precise message and a non-zero exit code.
.. versionadded:: 2.4
"""
from pathlib import Path
from mafw.scaffold.errors import (
DestinationNotEmptyError,
GenerationError,
InvalidNameError,
MissingTemplateError,
MissingValueError,
NameCollisionError,
ScaffoldError,
)
from mafw.scaffold.renderer import render_project
from mafw.scaffold.spec import ProjectSpec
from mafw.scaffold.steering import write_example_steering_file
from mafw.scaffold.writer import write_atomically
# The re-exports above are part of this package's public surface. Their canonical
# documented location is their defining module (``mafw.scaffold.spec`` and
# ``mafw.scaffold.errors``); docstrings that reference them use the fully
# qualified name to avoid duplicate Sphinx cross-reference targets.
__all__ = [
'ProjectSpec',
'ScaffoldError',
'InvalidNameError',
'NameCollisionError',
'DestinationNotEmptyError',
'MissingTemplateError',
'MissingValueError',
'GenerationError',
'generate_project',
]
[docs]
def generate_project(spec: ProjectSpec) -> Path:
"""Generate a MAFw analysis project described by ``spec`` and return its root.
The project is rendered into a temporary staging directory and only moved to
the final destination once every file has been produced successfully, so a
failure never leaves a partially generated project behind.
:param spec: The validated description of the project to generate.
:type spec: mafw.scaffold.spec.ProjectSpec
:return: The absolute path to the generated project root.
:rtype: Path
:raises mafw.scaffold.errors.DestinationNotEmptyError: If the destination already contains entries.
:raises mafw.scaffold.errors.MissingTemplateError: If the bundled template cannot be located.
:raises mafw.scaffold.errors.GenerationError: If rendering or writing fails partway through.
"""
# Render the template once, up front, so a missing-template or bad-placeholder
# error is raised before any staging directory is created.
rendered_files = render_project(spec)
steering_name = f'{spec.import_name}_steering.toml'
def _render_into(staging_dir: Path) -> None:
"""Write every rendered file and the steering file into the staging tree."""
for rendered in rendered_files:
target = staging_dir / Path(*rendered.relative_path.parts)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(rendered.content, encoding='utf-8')
# The steering file is generated (not templated) so it matches the real
# parameter schema of the example processor.
write_example_steering_file(staging_dir / 'conf' / steering_name)
return write_atomically(spec, _render_into)