1- """Main PDF baker class."""
1+ """PDFBaker class.
2+
3+ Overall orchestration and logging.
4+
5+ Is given a configuration file and sets up logging.
6+ bake() delegates to its documents and reports back the end result.
7+ """
28
39import logging
410from pathlib import Path
511from typing import Any
612
7- import yaml
8-
9- from . import errors
13+ from .config import PDFBakerConfiguration
1014from .document import PDFBakerDocument
11- from .errors import PDFBakeError
15+ from .errors import ConfigurationError
1216
1317__all__ = ["PDFBaker" ]
1418
1519
20+ DEFAULT_CONFIG = {
21+ "documents_dir" : "." ,
22+ "pages_dir" : "pages" ,
23+ "templates_dir" : "templates" ,
24+ "images_dir" : "images" ,
25+ "build_dir" : "build" ,
26+ "dist_dir" : "dist" ,
27+ }
28+
29+
1630class PDFBaker :
1731 """Main class for PDF document generation."""
1832
19- def __init__ (self , config_file : Path ) -> None :
33+ class Configuration (PDFBakerConfiguration ):
34+ """PDFBaker configuration."""
35+
36+ def __init__ (self , base_config : dict [str , Any ], config_file : Path ) -> None :
37+ """Initialize baker configuration (needs documents)."""
38+ super ().__init__ (base_config , config_file )
39+ if "documents" not in self :
40+ raise ConfigurationError (
41+ 'Key "documents" missing - is this the main configuration file?'
42+ )
43+ self .documents = [
44+ self .resolve_path (doc_spec ) for doc_spec in self ["documents" ]
45+ ]
46+
47+ def __init__ (
48+ self ,
49+ config_file : Path ,
50+ quiet : bool = False ,
51+ verbose : bool = False ,
52+ keep_build : bool = False ,
53+ ) -> None :
2054 """Initialize PDFBaker with config file path.
2155
2256 Args:
2357 config_file: Path to config file, document directory is its parent
2458 """
59+ logging .basicConfig (level = logging .INFO , format = "%(levelname)s: %(message)s" )
2560 self .logger = logging .getLogger (__name__ )
26- self .base_dir = config_file .parent
27- self .build_dir = self .base_dir / "build"
28- self .dist_dir = self .base_dir / "dist"
29- self .config = self ._load_config (config_file )
61+ if quiet :
62+ logging .getLogger ().setLevel (logging .ERROR )
63+ elif verbose :
64+ logging .getLogger ().setLevel (logging .DEBUG )
65+ else :
66+ logging .getLogger ().setLevel (logging .INFO )
67+ self .keep_build = keep_build
68+ self .config = self .Configuration (
69+ base_config = DEFAULT_CONFIG ,
70+ config_file = config_file ,
71+ )
3072
31- # Add convenience methods for logging
3273 def debug (self , msg : str , * args : Any , ** kwargs : Any ) -> None :
3374 """Log a debug message."""
3475 self .logger .debug (msg , * args , ** kwargs )
@@ -37,125 +78,68 @@ def info(self, msg: str, *args: Any, **kwargs: Any) -> None:
3778 """Log an info message."""
3879 self .logger .info (msg , * args , ** kwargs )
3980
81+ def info_section (self , msg : str , * args : Any , ** kwargs : Any ) -> None :
82+ """Log an info message as a main section header."""
83+ self .logger .info (f"──── { msg } ────" , * args , ** kwargs )
84+
85+ def info_subsection (self , msg : str , * args : Any , ** kwargs : Any ) -> None :
86+ """Log an info message as a subsection header."""
87+ self .logger .info (f" ── { msg } ──" , * args , ** kwargs )
88+
4089 def warning (self , msg : str , * args : Any , ** kwargs : Any ) -> None :
4190 """Log a warning message."""
4291 self .logger .warning (msg , * args , ** kwargs )
4392
4493 def error (self , msg : str , * args : Any , ** kwargs : Any ) -> None :
4594 """Log an error message."""
46- self .logger .error (msg , * args , ** kwargs )
95+ self .logger .error (f"**** { msg } ****" , * args , ** kwargs )
4796
4897 def critical (self , msg : str , * args : Any , ** kwargs : Any ) -> None :
4998 """Log a critical message."""
5099 self .logger .critical (msg , * args , ** kwargs )
51100
52- def bake (self , debug : bool = False ) -> None :
53- """Generate PDFs from configuration.
54-
55- Args:
56- debug: If True, keep build files for debugging
57- """
58- document_paths = self ._get_document_paths (self .config .get ("documents" , []))
59- pdfs_created : list [str ] = []
101+ def bake (self ) -> None :
102+ """Generate PDFs from documents."""
103+ pdfs_created : list [Path ] = []
60104 failed_docs : list [tuple [str , str ]] = []
61105
62- for doc_name , doc_path in document_paths .items ():
106+ self .debug ("Main configuration:" )
107+ self .debug (self .config .pprint ())
108+ self .debug ("Documents to process:" )
109+ self .debug (self .config .documents )
110+ for doc_config in self .config .documents :
63111 doc = PDFBakerDocument (
64- name = doc_name ,
65- doc_dir = doc_path ,
66112 baker = self ,
113+ base_config = self .config ,
114+ config = doc_config ,
67115 )
68- doc .setup_directories ()
69- pdf_file , error_message = doc .process_document ()
70- if pdf_file is None :
116+ pdf_files , error_message = doc .process_document ()
117+ if pdf_files is None :
71118 self .error (
72- "Failed to process document '%s': %s" , doc_name , error_message
119+ "Failed to process document '%s': %s" ,
120+ doc .config .name ,
121+ error_message ,
73122 )
74- failed_docs .append ((doc_name , error_message ))
123+ failed_docs .append ((doc . config . name , error_message ))
75124 else :
76- pdfs_created .append (pdf_file )
77-
78- if not debug :
79- self ._teardown_build_directories (list (document_paths .keys ()))
125+ if isinstance (pdf_files , Path ):
126+ pdf_files = [pdf_files ]
127+ pdfs_created .extend (pdf_files )
128+ if not self .keep_build :
129+ doc .teardown ()
80130
81- self .info ("Done." )
82131 if pdfs_created :
83- self .info ("PDF files created in %s" , self .dist_dir .resolve ())
132+ self .info ("Created PDFs:" )
133+ for pdf in pdfs_created :
134+ self .info (" %s" , pdf )
84135 else :
85- self .warning ("No PDF files created." )
86- if failed_docs :
87- self .warning ("There were errors." )
88-
89- def _load_config (self , config_file : Path ) -> dict [str , Any ]:
90- """Load configuration from YAML file."""
91- try :
92- with open (config_file , encoding = "utf-8" ) as f :
93- config = yaml .safe_load (f )
94- if "documents" not in config :
95- raise errors .PDFBakeError (
96- 'Not a main configuration file - "documents" key missing'
97- )
98- return config
99- except Exception as exc :
100- raise errors .PDFBakeError (f"Failed to load config file: { exc } " ) from exc
101-
102- def _get_document_paths (
103- self , documents : list [dict [str , str ] | str ] | None
104- ) -> dict [str , Path ]:
105- """Resolve document paths to absolute paths.
106-
107- Args:
108- documents: List of document names or dicts with name/path,
109- or None if no documents specified
136+ self .warning ("No PDFs were created." )
110137
111- Returns:
112- Dictionary mapping document names to their paths
113- """
114- if not documents :
115- return {}
116-
117- document_paths : dict [str , Path ] = {}
118- for doc_name in documents :
119- if isinstance (doc_name , dict ):
120- # Format: {"name": "doc_name", "path": "/absolute/path/to/doc"}
121- doc_path = Path (doc_name ["path" ])
122- doc_name = doc_name ["name" ]
123- else :
124- # Default: document in subdirectory with same name as doc_name
125- doc_path = self .base_dir / doc_name
126-
127- if not doc_path .exists ():
128- raise PDFBakeError (f"Document directory not found: { doc_path } " )
129- document_paths [doc_name ] = doc_path .resolve ()
130-
131- return document_paths
132-
133- def _teardown_build_directories (self , doc_names : list [str ]) -> None :
134- """Clean up build directories after successful processing."""
135- for doc_name in doc_names :
136- doc_build_dir = self .build_dir / doc_name
137- if doc_build_dir .exists ():
138- # Remove all files in the document's build directory
139- for file_path in doc_build_dir .iterdir ():
140- if file_path .is_file ():
141- file_path .unlink ()
142-
143- # Try to remove the document's build directory if empty
144- try :
145- doc_build_dir .rmdir ()
146- except OSError :
147- # Directory not empty (might contain subdirectories)
148- self .logger .warning (
149- "Build directory of document not empty, keeping %s" ,
150- doc_build_dir ,
151- )
152-
153- # Try to remove the base build directory if it exists and is empty
154- if self .build_dir .exists ():
155- try :
156- self .build_dir .rmdir ()
157- except OSError :
158- # Directory not empty
159- self .logger .warning (
160- "Build directory not empty, keeping %s" , self .build_dir
161- )
138+ if failed_docs :
139+ self .warning (
140+ "Failed to process %d document%s:" ,
141+ len (failed_docs ),
142+ "" if len (failed_docs ) == 1 else "s" ,
143+ )
144+ for doc_name , error in failed_docs :
145+ self .error (" %s: %s" , doc_name , error )
0 commit comments