Skip to content

Commit ec24465

Browse files
committed
feat: Implement --create-from
If used in conjunction with `--dry-run`, don't create any files. Otherwise, also process the new configs immediately.
1 parent 05cb2c1 commit ec24465

File tree

3 files changed

+128
-4
lines changed

3 files changed

+128
-4
lines changed

src/pdfbaker/__main__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88

99
from pdfbaker import __version__
1010
from pdfbaker.baker import Baker, BakerOptions
11-
from pdfbaker.errors import DocumentNotFoundError, PDFBakerError
11+
from pdfbaker.errors import (
12+
DocumentNotFoundError,
13+
DryRunCreateFromCompleted,
14+
PDFBakerError,
15+
)
1216

1317
logger = logging.getLogger(__name__)
1418

@@ -80,6 +84,11 @@ def cli(
8084
baker = Baker(config_file, options=options)
8185
success = baker.bake(document_names=document_names)
8286
sys.exit(0 if success else 1)
87+
except DryRunCreateFromCompleted:
88+
sys.exit(0)
89+
except FileExistsError as exc:
90+
logger.error("❌ %s", str(exc))
91+
sys.exit(2)
8392
except FileNotFoundError as exc:
8493
logger.error("❌ %s", str(exc))
8594
sys.exit(2)

src/pdfbaker/baker.py

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@
66
bake() delegates to its documents and reports back the end result.
77
"""
88

9+
import shutil
910
from pathlib import Path
1011

1112
from pydantic import BaseModel, ValidationError
13+
from ruamel.yaml import YAML
1214

1315
from .config import PathSpec
1416
from .config.baker import BakerConfig
1517
from .document import Document
16-
from .errors import DocumentNotFoundError
18+
from .errors import DocumentNotFoundError, DryRunCreateFromCompleted
1719
from .logging import LoggingMixin, setup_logging
1820

1921
__all__ = ["Baker", "BakerOptions"]
@@ -53,8 +55,17 @@ def __init__(
5355
"""Set up logging and load configuration."""
5456
options = options or BakerOptions()
5557
setup_logging(quiet=options.quiet, trace=options.trace, verbose=options.verbose)
56-
self.create_from = options.create_from
57-
# FIXME: use create_from to create a new config file
58+
59+
if options.create_from:
60+
self.create_from(
61+
svg_path=options.create_from,
62+
config_path=config_file,
63+
dry_run=options.dry_run,
64+
)
65+
if options.dry_run:
66+
# Dry run creations don't continue with dry run processing
67+
raise DryRunCreateFromCompleted()
68+
5869
self.log_debug_section("Loading main configuration: %s", config_file)
5970
self.config = BakerConfig(
6071
config_file=config_file,
@@ -186,3 +197,102 @@ def teardown(self) -> None:
186197
self.log_warning("Top-level build directory not empty - not removing")
187198
else:
188199
self.log_debug("Top-level build directory does not exist")
200+
201+
def create_from(
202+
self, svg_path: Path, config_path: Path, dry_run: bool = False
203+
) -> None:
204+
"""Create a minimal project structure from an SVG and config path."""
205+
project_dir = config_path.parent
206+
doc_name = svg_path.stem
207+
doc_dir = project_dir / doc_name
208+
template_file = doc_dir / "templates" / "main.svg.j2"
209+
page_file = doc_dir / "pages" / "main.yaml"
210+
doc_config_file = doc_dir / "config.yaml"
211+
files_to_create = [config_path, doc_config_file, page_file, template_file]
212+
dirs_to_create = [
213+
d
214+
for d in [project_dir, doc_dir, doc_dir / "pages", doc_dir / "templates"]
215+
if not d.exists()
216+
]
217+
218+
for f in files_to_create:
219+
if f.exists():
220+
raise FileExistsError(f"File already exists: {f}")
221+
222+
if dry_run:
223+
for d in dirs_to_create:
224+
self.log_info("👀 [DRY RUN] Would create directory: %s", d)
225+
for f in files_to_create:
226+
self.log_info("👀 [DRY RUN] Would create file: %s", f)
227+
self.log_info("👀 [DRY RUN] No files created.")
228+
raise DryRunCreateFromCompleted()
229+
230+
for d in dirs_to_create:
231+
d.mkdir(parents=True, exist_ok=True)
232+
233+
yaml = YAML()
234+
yaml.indent(mapping=2, sequence=4, offset=2)
235+
236+
with open(config_path, "w", encoding="utf-8") as f:
237+
f.write("# PDFBaker main config\n\n")
238+
yaml.dump({"documents": [doc_name]}, f)
239+
f.write(
240+
"\n"
241+
"# directories: # Override default directories below\n"
242+
"# dist: dist # Final PDF files are written here\n"
243+
"# documents: . # Location of document configurations\n"
244+
"# images: images # Location of image files\n"
245+
"# pages: pages # Location of page configurations\n"
246+
"# templates: templates # Location of SVG template files\n"
247+
"# jinja2_extensions: []"
248+
" # Jinja2 extensions to load and use in templates\n"
249+
"# template_renderers: # List of automatically applied renderers\n"
250+
"# - render_highlight\n"
251+
"# template_filters: # List of filters made available to templates\n"
252+
"# - wordwrap\n"
253+
"# svg2pdf_backend: cairosvg"
254+
" # Backend to use for SVG to PDF conversion\n"
255+
"# compress_pdf: false # Whether to compress the final PDF\n"
256+
"# keep_build: false"
257+
" # Whether to keep the build directory and its intermediary files\n"
258+
"\n"
259+
"# Example custom variables for all pages of all documents:\n"
260+
"# style:\n"
261+
"# font: Arial\n"
262+
"# color: black\n"
263+
)
264+
self.log_info("Created main config: %s", config_path)
265+
266+
with open(doc_config_file, "w", encoding="utf-8") as f:
267+
f.write("# Document config\n\n")
268+
yaml.dump({"filename": doc_name, "pages": ["main"]}, f)
269+
f.write(
270+
"\n"
271+
"# compress_pdf: false"
272+
" # Whether to compress the final PDF for this document\n"
273+
"# custom_bake: bake.py"
274+
" # Python file used for custom processing (if found)\n"
275+
"# variants: # List of document variants\n"
276+
"\n"
277+
"# Example custom variables for all pages of this document:\n"
278+
"# style:\n"
279+
"# font: Arial\n"
280+
"# color: black\n"
281+
)
282+
self.log_info("Created document config: %s", doc_config_file)
283+
284+
with open(page_file, "w", encoding="utf-8") as f:
285+
f.write("# Page config\n\n")
286+
yaml.dump({"template": "main.svg.j2", "name": "main"}, f)
287+
f.write(
288+
"\n"
289+
"# images: # List of images to use in the page\n"
290+
"\n"
291+
"# Example custom variables for this page:\n"
292+
"# title: My Document\n"
293+
"# date: 2025-05-19\n"
294+
)
295+
self.log_info("Created page: %s", page_file)
296+
297+
shutil.copy(svg_path, template_file)
298+
self.log_info("Created template: %s", template_file)

src/pdfbaker/errors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
__all__ = [
66
"ConfigurationError",
77
"DocumentNotFoundError",
8+
"DryRunCreateFromCompleted",
89
"PDFBakerError",
910
"PDFCombineError",
1011
"PDFCompressionError",
@@ -13,6 +14,10 @@
1314
]
1415

1516

17+
class DryRunCreateFromCompleted(Exception):
18+
"""Dry-run create-from completed - no further processing."""
19+
20+
1621
class PDFBakerError(Exception):
1722
"""Base exception for PDF baking errors."""
1823

0 commit comments

Comments
 (0)