Skip to content

Commit 93c59de

Browse files
committed
WIP Refactor for clean structure to support custom locations
1 parent 9b252bf commit 93c59de

10 files changed

Lines changed: 569 additions & 409 deletions

File tree

src/pdfbaker/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@
22

33
from importlib.metadata import version
44

5-
__all__ = ["__version__"]
6-
75
__version__ = version("pdfbaker")
6+
__all__ = ["__version__"]

src/pdfbaker/__main__.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88

99
from pdfbaker import __version__
1010
from pdfbaker.baker import PDFBaker
11-
from pdfbaker.errors import PDFBakeError
11+
from pdfbaker.errors import PDFBakerError
1212

13-
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
1413
logger = logging.getLogger(__name__)
1514

1615

@@ -25,27 +24,27 @@ def cli() -> None:
2524
"config_file",
2625
type=click.Path(exists=True, dir_okay=False, path_type=Path),
2726
)
27+
@click.option("-q", "--quiet", is_flag=True, help="Show errors only")
28+
@click.option("-v", "--verbose", is_flag=True, help="Show debug information")
29+
@click.option("--keep-build", is_flag=True, help="Keep build artifacts")
2830
@click.option(
29-
"--debug", is_flag=True, help="Debug mode (implies --verbose, keeps build files)"
31+
"--debug", is_flag=True, help="Debug mode (implies --verbose and --keep-build)"
3032
)
31-
@click.option("-v", "--verbose", is_flag=True, help="Show debug information")
32-
@click.option("-q", "--quiet", is_flag=True, help="Show errors only")
33-
def bake(config_file: Path, debug: bool, verbose: bool, quiet: bool) -> int:
33+
def bake(
34+
config_file: Path, quiet: bool, verbose: bool, keep_build: bool, debug: bool
35+
) -> int:
3436
"""Parse config file and bake PDFs."""
3537
if debug:
3638
verbose = True
37-
if quiet:
38-
logging.getLogger().setLevel(logging.ERROR)
39-
elif verbose:
40-
logging.getLogger().setLevel(logging.DEBUG)
41-
else:
42-
logging.getLogger().setLevel(logging.INFO)
39+
keep_build = True
4340

4441
try:
45-
baker = PDFBaker(config_file)
46-
baker.bake(debug=debug)
42+
baker = PDFBaker(
43+
config_file, quiet=quiet, verbose=verbose, keep_build=keep_build
44+
)
45+
baker.bake()
4746
return 0
48-
except PDFBakeError as exc:
47+
except PDFBakerError as exc:
4948
logger.error(str(exc))
5049
return 1
5150

src/pdfbaker/baker.py

Lines changed: 94 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,75 @@
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

39
import logging
410
from pathlib import Path
511
from typing import Any
612

7-
import yaml
8-
9-
from . import errors
13+
from .config import PDFBakerConfiguration
1014
from .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+
1630
class 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

Comments
 (0)