nestedpdfmerger 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nested-pdf-merger contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: nestedpdfmerger
3
+ Version: 1.0.0
4
+ Summary: Merge PDFs recursively from a folder tree into a single PDF with hierarchical bookmarks.
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 nested-pdf-merger contributors
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Project-URL: Repository, https://github.com/Lyutenant/nested-pdf-merger
28
+ Keywords: pdf,merge,bookmarks,cli
29
+ Classifier: Development Status :: 5 - Production/Stable
30
+ Classifier: Environment :: Console
31
+ Classifier: Intended Audience :: End Users/Desktop
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Operating System :: OS Independent
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3.10
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Topic :: Utilities
39
+ Requires-Python: >=3.10
40
+ Description-Content-Type: text/markdown
41
+ License-File: LICENSE
42
+ Requires-Dist: pypdf>=4.0
43
+ Requires-Dist: natsort>=8.0
44
+ Provides-Extra: dev
45
+ Requires-Dist: pytest>=7.0; extra == "dev"
46
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
47
+ Requires-Dist: ruff>=0.1; extra == "dev"
48
+ Dynamic: license-file
49
+
50
+ # nestedpdfmerger
51
+
52
+ Merge PDFs recursively from a folder tree into a single PDF with automatic hierarchical bookmarks based on the directory structure.
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install nestedpdfmerger
58
+ ```
59
+
60
+ ## Quick start
61
+
62
+ ```bash
63
+ nestedpdfmerger ./reports -o merged.pdf
64
+ ```
65
+
66
+ ## Example input tree
67
+
68
+ ```
69
+ reports/
70
+ ├── intro.pdf
71
+ ├── chapter1/
72
+ │ ├── part1.pdf
73
+ │ └── part2.pdf
74
+ └── appendix/
75
+ └── appendixA.pdf
76
+ ```
77
+
78
+ Resulting bookmarks in `merged.pdf`:
79
+
80
+ ```
81
+ intro
82
+ chapter1
83
+ ├─ part1
84
+ └─ part2
85
+ appendix
86
+ └─ appendixA
87
+ ```
88
+
89
+ ## Usage
90
+
91
+ ```
92
+ nestedpdfmerger INPUT_DIR -o OUTPUT.pdf [options]
93
+ ```
94
+
95
+ ### Options
96
+
97
+ | Flag | Description |
98
+ |---|---|
99
+ | `-o, --output PATH` | Output PDF path (default: `<INPUT_DIR>.pdf`) |
100
+ | `--sort {natural,alpha,mtime}` | Sort mode (default: `natural`) |
101
+ | `--reverse` | Reverse sort order |
102
+ | `--exclude NAME [NAME ...]` | Directory names to skip |
103
+ | `--exclude-hidden` | Skip hidden directories (starting with `.`) |
104
+ | `--no-bookmarks` | Disable hierarchical bookmarks |
105
+ | `--dry-run` | Preview merge order without writing output |
106
+ | `--strict` | Stop on first PDF error instead of skipping |
107
+ | `--verbose` | Show detailed progress |
108
+ | `--quiet` | Suppress non-error output |
109
+ | `--version` | Show version and exit |
110
+
111
+ ### Examples
112
+
113
+ Preview what would be merged:
114
+
115
+ ```bash
116
+ nestedpdfmerger ./reports --dry-run
117
+ ```
118
+
119
+ Natural sort, exclude backup folders, verbose output:
120
+
121
+ ```bash
122
+ nestedpdfmerger ./reports \
123
+ --output merged.pdf \
124
+ --sort natural \
125
+ --exclude Backup Data \
126
+ --verbose
127
+ ```
128
+
129
+ Sort by modification time, newest last:
130
+
131
+ ```bash
132
+ nestedpdfmerger ./reports -o merged.pdf --sort mtime --reverse
133
+ ```
134
+
135
+ ## Python API
136
+
137
+ ```python
138
+ from nestedpdfmerger import merge_pdf_tree
139
+
140
+ merge_pdf_tree(
141
+ input_dir="reports",
142
+ output_file="merged.pdf",
143
+ sort_mode="natural",
144
+ exclude=["Backup", "Data"],
145
+ bookmarks=True,
146
+ )
147
+ ```
148
+
149
+ ## Sort modes
150
+
151
+ | Mode | Description |
152
+ |---|---|
153
+ | `natural` | Human-friendly natural sort (1, 2, 10 not 1, 10, 2) |
154
+ | `alpha` | Case-insensitive alphabetical |
155
+ | `mtime` | File/directory modification time (oldest first) |
156
+
157
+ ## Error handling
158
+
159
+ By default, corrupted, encrypted, or unreadable PDFs are **warned and skipped**. Use `--strict` to stop on the first error.
160
+
161
+ ## License
162
+
163
+ MIT
@@ -0,0 +1,114 @@
1
+ # nestedpdfmerger
2
+
3
+ Merge PDFs recursively from a folder tree into a single PDF with automatic hierarchical bookmarks based on the directory structure.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install nestedpdfmerger
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ nestedpdfmerger ./reports -o merged.pdf
15
+ ```
16
+
17
+ ## Example input tree
18
+
19
+ ```
20
+ reports/
21
+ ├── intro.pdf
22
+ ├── chapter1/
23
+ │ ├── part1.pdf
24
+ │ └── part2.pdf
25
+ └── appendix/
26
+ └── appendixA.pdf
27
+ ```
28
+
29
+ Resulting bookmarks in `merged.pdf`:
30
+
31
+ ```
32
+ intro
33
+ chapter1
34
+ ├─ part1
35
+ └─ part2
36
+ appendix
37
+ └─ appendixA
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```
43
+ nestedpdfmerger INPUT_DIR -o OUTPUT.pdf [options]
44
+ ```
45
+
46
+ ### Options
47
+
48
+ | Flag | Description |
49
+ |---|---|
50
+ | `-o, --output PATH` | Output PDF path (default: `<INPUT_DIR>.pdf`) |
51
+ | `--sort {natural,alpha,mtime}` | Sort mode (default: `natural`) |
52
+ | `--reverse` | Reverse sort order |
53
+ | `--exclude NAME [NAME ...]` | Directory names to skip |
54
+ | `--exclude-hidden` | Skip hidden directories (starting with `.`) |
55
+ | `--no-bookmarks` | Disable hierarchical bookmarks |
56
+ | `--dry-run` | Preview merge order without writing output |
57
+ | `--strict` | Stop on first PDF error instead of skipping |
58
+ | `--verbose` | Show detailed progress |
59
+ | `--quiet` | Suppress non-error output |
60
+ | `--version` | Show version and exit |
61
+
62
+ ### Examples
63
+
64
+ Preview what would be merged:
65
+
66
+ ```bash
67
+ nestedpdfmerger ./reports --dry-run
68
+ ```
69
+
70
+ Natural sort, exclude backup folders, verbose output:
71
+
72
+ ```bash
73
+ nestedpdfmerger ./reports \
74
+ --output merged.pdf \
75
+ --sort natural \
76
+ --exclude Backup Data \
77
+ --verbose
78
+ ```
79
+
80
+ Sort by modification time, newest last:
81
+
82
+ ```bash
83
+ nestedpdfmerger ./reports -o merged.pdf --sort mtime --reverse
84
+ ```
85
+
86
+ ## Python API
87
+
88
+ ```python
89
+ from nestedpdfmerger import merge_pdf_tree
90
+
91
+ merge_pdf_tree(
92
+ input_dir="reports",
93
+ output_file="merged.pdf",
94
+ sort_mode="natural",
95
+ exclude=["Backup", "Data"],
96
+ bookmarks=True,
97
+ )
98
+ ```
99
+
100
+ ## Sort modes
101
+
102
+ | Mode | Description |
103
+ |---|---|
104
+ | `natural` | Human-friendly natural sort (1, 2, 10 not 1, 10, 2) |
105
+ | `alpha` | Case-insensitive alphabetical |
106
+ | `mtime` | File/directory modification time (oldest first) |
107
+
108
+ ## Error handling
109
+
110
+ By default, corrupted, encrypted, or unreadable PDFs are **warned and skipped**. Use `--strict` to stop on the first error.
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,55 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nestedpdfmerger"
7
+ version = "1.0.0"
8
+ description = "Merge PDFs recursively from a folder tree into a single PDF with hierarchical bookmarks."
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.10"
12
+ keywords = ["pdf", "merge", "bookmarks", "cli"]
13
+ classifiers = [
14
+ "Development Status :: 5 - Production/Stable",
15
+ "Environment :: Console",
16
+ "Intended Audience :: End Users/Desktop",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Utilities",
24
+ ]
25
+ dependencies = [
26
+ "pypdf>=4.0",
27
+ "natsort>=8.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=7.0",
33
+ "pytest-cov>=4.0",
34
+ "ruff>=0.1",
35
+ ]
36
+
37
+ [project.scripts]
38
+ nestedpdfmerger = "nestedpdfmerger.cli:main"
39
+
40
+ [project.urls]
41
+ Repository = "https://github.com/Lyutenant/nested-pdf-merger"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
48
+
49
+ [tool.ruff]
50
+ line-length = 88
51
+ target-version = "py310"
52
+ exclude = ["script.py", "examples/"]
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "W", "I"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """nestedpdfmerger — merge PDFs recursively with hierarchical bookmarks."""
2
+
3
+ from .merger import merge_pdf_tree
4
+
5
+ __version__ = "1.0.0"
6
+ __all__ = ["merge_pdf_tree", "__version__"]
@@ -0,0 +1,5 @@
1
+ """Entry point for ``python -m nestedpdfmerger``."""
2
+
3
+ from .cli import main
4
+
5
+ main()
@@ -0,0 +1,37 @@
1
+ """Bookmark data structure for hierarchical PDF outline entries."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class Bookmark:
7
+ """Represents a single PDF outline (bookmark) entry with optional children."""
8
+
9
+ __slots__ = ("_page", "_title", "_children")
10
+
11
+ def __init__(self, page: int, title: str) -> None:
12
+ self._page = page
13
+ self._title = title
14
+ self._children: list[Bookmark] = []
15
+
16
+ @property
17
+ def page(self) -> int:
18
+ return self._page
19
+
20
+ @property
21
+ def title(self) -> str:
22
+ return self._title
23
+
24
+ @property
25
+ def children(self) -> list[Bookmark]:
26
+ return self._children
27
+
28
+ def add_child(self, child: Bookmark) -> None:
29
+ if not isinstance(child, Bookmark):
30
+ raise TypeError("child must be a Bookmark instance")
31
+ self._children.append(child)
32
+
33
+ def __repr__(self) -> str:
34
+ return (
35
+ f"Bookmark(page={self._page!r}, title={self._title!r},"
36
+ f" children={len(self._children)})"
37
+ )
@@ -0,0 +1,132 @@
1
+ """Command-line interface for nestedpdfmerger."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from . import __version__
11
+ from .config import DEFAULT_SORT_MODE, VALID_SORT_MODES
12
+ from .errors import MergeError
13
+ from .merger import merge_pdf_tree
14
+
15
+
16
+ def _setup_logging(verbose: bool, quiet: bool) -> None:
17
+ if quiet:
18
+ level = logging.ERROR
19
+ elif verbose:
20
+ level = logging.DEBUG
21
+ else:
22
+ level = logging.INFO
23
+ logging.basicConfig(level=level, format="%(message)s")
24
+
25
+
26
+ def build_parser() -> argparse.ArgumentParser:
27
+ parser = argparse.ArgumentParser(
28
+ prog="nestedpdfmerger",
29
+ description=(
30
+ "Merge PDFs recursively from a folder tree into a single PDF "
31
+ "with automatic hierarchical bookmarks."
32
+ ),
33
+ )
34
+ parser.add_argument(
35
+ "input_dir",
36
+ metavar="INPUT_DIR",
37
+ help="Root directory to scan for PDFs.",
38
+ )
39
+ parser.add_argument(
40
+ "-o",
41
+ "--output",
42
+ metavar="PATH",
43
+ default=None,
44
+ help=(
45
+ "Output PDF file path. "
46
+ "Defaults to <INPUT_DIR>.pdf in the same parent directory."
47
+ ),
48
+ )
49
+ parser.add_argument(
50
+ "--sort",
51
+ choices=list(VALID_SORT_MODES),
52
+ default=DEFAULT_SORT_MODE,
53
+ metavar="{" + ",".join(VALID_SORT_MODES) + "}",
54
+ help="Sort mode for files and directories (default: %(default)s).",
55
+ )
56
+ parser.add_argument(
57
+ "--reverse",
58
+ action="store_true",
59
+ help="Reverse the sort order.",
60
+ )
61
+ parser.add_argument(
62
+ "--exclude",
63
+ nargs="+",
64
+ metavar="NAME",
65
+ default=[],
66
+ help="Directory names to exclude (space-separated).",
67
+ )
68
+ parser.add_argument(
69
+ "--exclude-hidden",
70
+ action="store_true",
71
+ help="Exclude hidden directories (names starting with '.').",
72
+ )
73
+ parser.add_argument(
74
+ "--no-bookmarks",
75
+ action="store_true",
76
+ help="Disable hierarchical bookmarks in the output PDF.",
77
+ )
78
+ parser.add_argument(
79
+ "--dry-run",
80
+ action="store_true",
81
+ help="Preview merge order and bookmark structure without writing output.",
82
+ )
83
+ parser.add_argument(
84
+ "--strict",
85
+ action="store_true",
86
+ help="Stop on the first PDF error instead of skipping the file.",
87
+ )
88
+ parser.add_argument(
89
+ "--verbose",
90
+ action="store_true",
91
+ help="Show detailed progress (scanned folders, detected PDFs, etc.).",
92
+ )
93
+ parser.add_argument(
94
+ "--quiet",
95
+ action="store_true",
96
+ help="Suppress all non-error output.",
97
+ )
98
+ parser.add_argument(
99
+ "--version",
100
+ action="version",
101
+ version=f"%(prog)s {__version__}",
102
+ )
103
+ return parser
104
+
105
+
106
+ def main(argv: list[str] | None = None) -> None:
107
+ parser = build_parser()
108
+ args = parser.parse_args(argv)
109
+
110
+ _setup_logging(args.verbose, args.quiet)
111
+
112
+ output = args.output
113
+ if output is None and not args.dry_run:
114
+ output = str(Path(args.input_dir).resolve().with_suffix(".pdf"))
115
+
116
+ try:
117
+ merge_pdf_tree(
118
+ input_dir=args.input_dir,
119
+ output_file=output,
120
+ sort_mode=args.sort,
121
+ reverse=args.reverse,
122
+ exclude=args.exclude,
123
+ bookmarks=not args.no_bookmarks,
124
+ exclude_hidden=args.exclude_hidden,
125
+ dry_run=args.dry_run,
126
+ strict=args.strict,
127
+ verbose=args.verbose,
128
+ quiet=args.quiet,
129
+ )
130
+ except (MergeError, ValueError) as exc:
131
+ print(f"Error: {exc}", file=sys.stderr)
132
+ sys.exit(1)
@@ -0,0 +1,8 @@
1
+ """Default configuration values for nestedpdfmerger."""
2
+
3
+ DEFAULT_SORT_MODE = "natural"
4
+ VALID_SORT_MODES = ("natural", "alpha", "mtime")
5
+ DEFAULT_REVERSE = False
6
+ DEFAULT_BOOKMARKS = True
7
+ DEFAULT_STRICT = False
8
+ DEFAULT_EXCLUDE_HIDDEN = False
@@ -0,0 +1,13 @@
1
+ """Custom exceptions for nestedpdfmerger."""
2
+
3
+
4
+ class MergeError(Exception):
5
+ """Raised in strict mode when a PDF cannot be processed."""
6
+
7
+
8
+ class EncryptedPDFError(MergeError):
9
+ """Raised when a PDF file is encrypted and cannot be merged."""
10
+
11
+
12
+ class CorruptedPDFError(MergeError):
13
+ """Raised when a PDF file is corrupted and cannot be read."""