1- """PDFBaker class.
1+ """Baker class.
22
33Overall orchestration and logging.
44
77"""
88
99from pathlib import Path
10- from typing import Any
1110
12- from .config import BakerOptions , PDFBakerConfiguration , deep_merge
13- from .document import PDFBakerDocument
14- from .errors import ConfigurationError , DocumentNotFoundError
11+ from pydantic import BaseModel , ValidationError
12+
13+ from .config import (
14+ BakerConfig ,
15+ PathSpec ,
16+ )
17+ from .document import Document
18+ from .errors import DocumentNotFoundError
1519from .logging import LoggingMixin , setup_logging
1620
17- __all__ = ["PDFBaker" ]
18-
19-
20- DEFAULT_BAKER_CONFIG = {
21- # Default to directories relative to the config file
22- "directories" : {
23- "documents" : "." ,
24- "build" : "build" ,
25- "dist" : "dist" ,
26- },
27- # Highlighting support enabled by default
28- "template_renderers" : ["render_highlight" ],
29- # Make all filters available by default
30- "template_filters" : ["wordwrap" ],
31- }
32-
33-
34- class PDFBaker (LoggingMixin ):
35- """Main class for PDF document generation."""
36-
37- class Configuration (PDFBakerConfiguration ):
38- """PDFBaker configuration."""
39-
40- def __init__ (
41- self , baker : "PDFBaker" , base_config : dict [str , Any ], config_file : Path
42- ) -> None :
43- """Initialize baker configuration (needs documents)."""
44- self .baker = baker
45- self .name = config_file .name
46- self .baker .log_debug_section ("Loading main configuration: %s" , config_file )
47- super ().__init__ (base_config , config_file )
48- self .baker .log_trace (self .pretty ())
49- if "documents" not in self :
50- raise ConfigurationError (
51- 'Key "documents" missing - is this the main configuration file?'
52- )
53- self .build_dir = self ["directories" ]["build" ]
54- self .documents = []
55- for doc_spec in self ["documents" ]:
56- doc_path = self .resolve_path (
57- doc_spec , directory = self ["directories" ]["documents" ]
58- )
59- self .documents .append ({"name" : doc_path .name , "path" : doc_path })
21+ __all__ = ["Baker" , "BakerOptions" ]
22+
23+
24+ class BakerOptions (BaseModel ):
25+ """Options for controlling PDFBaker behavior.
26+
27+ Attributes:
28+ quiet: Show errors only
29+ verbose: Show debug information
30+ trace: Show trace information (even more detailed than debug)
31+ keep_build: Keep build artifacts after processing
32+ default_config_overrides: Dictionary of values to override the built-in defaults
33+ before loading the main configuration
34+ """
35+
36+ quiet : bool = False
37+ verbose : bool = False
38+ trace : bool = False
39+ keep_build : bool = False
40+
41+
42+ class Baker (LoggingMixin ):
43+ """Baker class."""
6044
6145 def __init__ (
6246 self ,
6347 config_file : Path ,
6448 options : BakerOptions | None = None ,
49+ ** kwargs ,
6550 ) -> None :
66- """Initialize PDFBaker with config file path. Set logging level.
67-
68- Args:
69- config_file: Path to config file
70- options: Optional options for logging and build behavior
71- """
72- super ().__init__ ()
51+ """Set up logging and load configuration."""
7352 options = options or BakerOptions ()
7453 setup_logging (quiet = options .quiet , trace = options .trace , verbose = options .verbose )
75- self .keep_build = options .keep_build
76-
77- base_config = DEFAULT_BAKER_CONFIG .copy ()
78- if options and options .default_config_overrides :
79- base_config = deep_merge (base_config , options .default_config_overrides )
80- base_config ["directories" ]["config" ] = config_file .parent .resolve ()
81-
82- self .config = self .Configuration (
83- baker = self ,
84- base_config = base_config ,
54+ self .log_debug_section ("Loading main configuration: %s" , config_file )
55+ self .config = BakerConfig (
8556 config_file = config_file ,
57+ keep_build = options and options .keep_build or False ,
58+ ** kwargs ,
8659 )
60+ self .log_trace (self .config .readable ())
8761
8862 def _get_documents_to_process (
8963 self , selected_document_names : tuple [str , ...] | None = None
90- ) -> list [Path ]:
64+ ) -> list [PathSpec ]:
9165 """Get the document paths to process based on optional filtering.
9266
9367 Args:
@@ -99,61 +73,57 @@ def _get_documents_to_process(
9973 if not selected_document_names :
10074 return self .config .documents
10175
102- available_doc_names = [doc [ " name" ] for doc in self .config .documents ]
76+ available_doc_names = [doc . name for doc in self .config .documents ]
10377 missing_docs = [
10478 name for name in selected_document_names if name not in available_doc_names
10579 ]
10680 if missing_docs :
10781 available_str = ", " .join ([f'"{ name } "' for name in available_doc_names ])
108- self .log_info (f"Documents in { self .config .name } : { available_str } " )
82+ self .log_info (
83+ f"Documents in { self .config .config_file .name } : { available_str } "
84+ )
10985 missing_str = ", " .join ([f'"{ name } "' for name in missing_docs ])
11086 raise DocumentNotFoundError (
11187 f"Document{ 's' if len (missing_docs ) != 1 else '' } not found "
11288 f"in configuration: { missing_str } ."
11389 )
11490
11591 return [
116- doc
117- for doc in self .config .documents
118- if doc ["name" ] in selected_document_names
92+ doc for doc in self .config .documents if doc .name in selected_document_names
11993 ]
12094
121- def bake (self , document_names : tuple [str , ...] | None = None ) -> bool :
122- """Create PDFs for all documents or only the specified ones.
123-
124- Args:
125- document_names: Optional tuple of document names to process
126-
127- Returns:
128- bool: True if all documents were processed successfully, False if any failed
129- """
95+ def bake (self , document_names : tuple [str , ...] | None = None ) -> None :
96+ """Bake the documents."""
13097 pdfs_created : list [Path ] = []
131- failed_docs : list [tuple [str , str ]] = []
98+ failed_docs : list [tuple [PathSpec , str ]] = []
13299
133- documents = self ._get_documents_to_process (document_names )
100+ doc_configs = self ._get_documents_to_process (document_names )
134101
135102 self .log_debug_subsection ("Documents to process:" )
136- self .log_debug (documents )
137- for doc_config in documents :
138- doc = PDFBakerDocument (
139- baker = self ,
140- base_config = self .config ,
141- config_path = doc_config ["path" ],
142- )
143- pdf_files , error_message = doc .process_document ()
103+ self .log_debug (doc_configs )
104+ for doc_config in doc_configs :
105+ try :
106+ document = Document (
107+ config_path = doc_config , ** self .config .document_settings
108+ )
109+ except ValidationError as e :
110+ self .log_error (f'Invalid config for document "{ doc_config .name } ": { e } ' )
111+ continue
112+
113+ pdf_files , error_message = document .process_document ()
144114 if error_message :
145115 self .log_error (
146116 "Failed to process document '%s': %s" ,
147- doc .config .name ,
117+ document .config .name ,
148118 error_message ,
149119 )
150- failed_docs .append ((doc . config . name , error_message ))
120+ failed_docs .append ((document , error_message ))
151121 else :
152122 if isinstance (pdf_files , Path ):
153123 pdf_files = [pdf_files ]
154124 pdfs_created .extend (pdf_files )
155- if not self .keep_build :
156- doc .teardown ()
125+ if not self .config . keep_build :
126+ document .teardown ()
157127
158128 if pdfs_created :
159129 self .log_info ("Successfully created PDFs:" )
@@ -171,19 +141,20 @@ def bake(self, document_names: tuple[str, ...] | None = None) -> bool:
171141 for doc_name , error in failed_docs :
172142 self .log_error (" %s: %s" , doc_name , error )
173143
174- if not self .keep_build :
144+ if not self .config . keep_build :
175145 self .teardown ()
176146
177147 return not failed_docs
178148
179149 def teardown (self ) -> None :
180150 """Clean up (top-level) build directory after processing."""
151+ build_dir = self .config .directories .build
181152 self .log_debug_subsection (
182- "Tearing down top-level build directory: %s" , self . config . build_dir
153+ "Tearing down top-level build directory: %s" , build_dir
183154 )
184- if self . config . build_dir .exists ():
155+ if build_dir .exists ():
185156 try :
186157 self .log_debug ("Removing top-level build directory..." )
187- self . config . build_dir .rmdir ()
158+ build_dir .rmdir ()
188159 except OSError :
189160 self .log_warning ("Top-level build directory not empty - not removing" )
0 commit comments