convert-docs 0.1.0__tar.gz → 0.2.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.
- {convert_docs-0.1.0 → convert_docs-0.2.0}/PKG-INFO +26 -3
- {convert_docs-0.1.0 → convert_docs-0.2.0}/README.md +24 -1
- {convert_docs-0.1.0 → convert_docs-0.2.0}/pyproject.toml +2 -2
- {convert_docs-0.1.0 → convert_docs-0.2.0}/src/convert_docs/cli.py +68 -5
- {convert_docs-0.1.0 → convert_docs-0.2.0}/.github/workflows/publish.yml +0 -0
- {convert_docs-0.1.0 → convert_docs-0.2.0}/.gitignore +0 -0
- {convert_docs-0.1.0 → convert_docs-0.2.0}/src/convert_docs/__init__.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: convert-docs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Recursively convert documents (pdf, docx, pptx, xlsx, ...) to Markdown via markitdown, mirroring the source folder structure into a destination folder.
|
|
5
|
-
Requires-Python: >=3.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: markitdown[docx,outlook,pdf,pptx,xls,xlsx]>=0.1.5
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
|
|
@@ -14,11 +14,23 @@ structure into a destination folder.
|
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
17
|
+
Requires [uv](https://docs.astral.sh/uv/). If you don't have it yet:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# macOS / Linux
|
|
21
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
22
|
+
|
|
23
|
+
# Windows (PowerShell)
|
|
24
|
+
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then install the tool:
|
|
28
|
+
|
|
17
29
|
```bash
|
|
18
30
|
uv tool install convert-docs
|
|
19
31
|
```
|
|
20
32
|
|
|
21
|
-
|
|
33
|
+
No `uv`? Plain `pip` also works, as long as it's Python 3.10+: `pip install convert-docs`.
|
|
22
34
|
|
|
23
35
|
## Usage
|
|
24
36
|
|
|
@@ -34,6 +46,7 @@ Or pass flags to skip the prompts:
|
|
|
34
46
|
convert-docs -s ~/Documents/Reports -o ~/Documents/Reports_MD
|
|
35
47
|
convert-docs -e pdf,docx # restrict which extensions get converted
|
|
36
48
|
convert-docs -f # force re-conversion, ignoring the up-to-date skip
|
|
49
|
+
convert-docs --last # re-run with the same source/destination/extensions as last time
|
|
37
50
|
```
|
|
38
51
|
|
|
39
52
|
| Flag | Description |
|
|
@@ -42,10 +55,20 @@ convert-docs -f # force re-conversion, ignoring the up-to-date s
|
|
|
42
55
|
| `-o, --output` | Output directory, mirrors source structure (skips the interactive prompt) |
|
|
43
56
|
| `-e, --ext` | Comma-separated extensions to convert |
|
|
44
57
|
| `-f, --force` | Force re-conversion even if output `.md` already exists and is up to date |
|
|
58
|
+
| `-l, --last` | Reuse the source, destination, and extensions from the last run (skips prompts; cannot be combined with `-s`/`-o`) |
|
|
45
59
|
|
|
46
60
|
Files with unsupported extensions are listed at the end instead of being silently
|
|
47
61
|
skipped, and any conversion failures are reported with the underlying error.
|
|
48
62
|
|
|
63
|
+
### Re-running on new files
|
|
64
|
+
|
|
65
|
+
Every run saves its source/destination/extensions to `~/.config/convert-docs/last_run.json`.
|
|
66
|
+
Since conversion already skips files whose output is up to date, `convert-docs --last` is a
|
|
67
|
+
cheap way to pick up newly added files in a folder you've converted before — run it manually
|
|
68
|
+
whenever you want to sync, or wire it into a cron job / scheduled task on whatever interval
|
|
69
|
+
suits you (just point it at the tool's full path, e.g. `~/.local/bin/convert-docs --last`,
|
|
70
|
+
since scheduled jobs don't load your shell profile).
|
|
71
|
+
|
|
49
72
|
## Update
|
|
50
73
|
|
|
51
74
|
```bash
|
|
@@ -6,11 +6,23 @@ structure into a destination folder.
|
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
+
Requires [uv](https://docs.astral.sh/uv/). If you don't have it yet:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# macOS / Linux
|
|
13
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
14
|
+
|
|
15
|
+
# Windows (PowerShell)
|
|
16
|
+
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then install the tool:
|
|
20
|
+
|
|
9
21
|
```bash
|
|
10
22
|
uv tool install convert-docs
|
|
11
23
|
```
|
|
12
24
|
|
|
13
|
-
|
|
25
|
+
No `uv`? Plain `pip` also works, as long as it's Python 3.10+: `pip install convert-docs`.
|
|
14
26
|
|
|
15
27
|
## Usage
|
|
16
28
|
|
|
@@ -26,6 +38,7 @@ Or pass flags to skip the prompts:
|
|
|
26
38
|
convert-docs -s ~/Documents/Reports -o ~/Documents/Reports_MD
|
|
27
39
|
convert-docs -e pdf,docx # restrict which extensions get converted
|
|
28
40
|
convert-docs -f # force re-conversion, ignoring the up-to-date skip
|
|
41
|
+
convert-docs --last # re-run with the same source/destination/extensions as last time
|
|
29
42
|
```
|
|
30
43
|
|
|
31
44
|
| Flag | Description |
|
|
@@ -34,10 +47,20 @@ convert-docs -f # force re-conversion, ignoring the up-to-date s
|
|
|
34
47
|
| `-o, --output` | Output directory, mirrors source structure (skips the interactive prompt) |
|
|
35
48
|
| `-e, --ext` | Comma-separated extensions to convert |
|
|
36
49
|
| `-f, --force` | Force re-conversion even if output `.md` already exists and is up to date |
|
|
50
|
+
| `-l, --last` | Reuse the source, destination, and extensions from the last run (skips prompts; cannot be combined with `-s`/`-o`) |
|
|
37
51
|
|
|
38
52
|
Files with unsupported extensions are listed at the end instead of being silently
|
|
39
53
|
skipped, and any conversion failures are reported with the underlying error.
|
|
40
54
|
|
|
55
|
+
### Re-running on new files
|
|
56
|
+
|
|
57
|
+
Every run saves its source/destination/extensions to `~/.config/convert-docs/last_run.json`.
|
|
58
|
+
Since conversion already skips files whose output is up to date, `convert-docs --last` is a
|
|
59
|
+
cheap way to pick up newly added files in a folder you've converted before — run it manually
|
|
60
|
+
whenever you want to sync, or wire it into a cron job / scheduled task on whatever interval
|
|
61
|
+
suits you (just point it at the tool's full path, e.g. `~/.local/bin/convert-docs --last`,
|
|
62
|
+
since scheduled jobs don't load your shell profile).
|
|
63
|
+
|
|
41
64
|
## Update
|
|
42
65
|
|
|
43
66
|
```bash
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "convert-docs"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Recursively convert documents (pdf, docx, pptx, xlsx, ...) to Markdown via markitdown, mirroring the source folder structure into a destination folder."
|
|
5
5
|
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"markitdown[pdf,docx,pptx,xlsx,xls,outlook]>=0.1.5",
|
|
9
9
|
]
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Recursively convert documents to Markdown via markitdown, mirroring folder structure."""
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
4
6
|
import sys
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
|
|
@@ -12,6 +14,45 @@ DEFAULT_EXTENSIONS = [
|
|
|
12
14
|
SEPARATOR = "-" * 40
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
def config_dir() -> Path:
|
|
18
|
+
base = os.environ.get("XDG_CONFIG_HOME")
|
|
19
|
+
return (Path(base) if base else Path.home() / ".config") / "convert-docs"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def last_run_path() -> Path:
|
|
23
|
+
return config_dir() / "last_run.json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def save_last_run(src_dir: Path, dest_dir: Path, extensions: list) -> None:
|
|
27
|
+
path = last_run_path()
|
|
28
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
path.write_text(json.dumps({
|
|
30
|
+
"source": str(src_dir),
|
|
31
|
+
"destination": str(dest_dir),
|
|
32
|
+
"extensions": extensions,
|
|
33
|
+
}, indent=2))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_last_run() -> dict:
|
|
37
|
+
path = last_run_path()
|
|
38
|
+
if not path.exists():
|
|
39
|
+
print(
|
|
40
|
+
"Error: no previous run found. Run convert-docs normally first "
|
|
41
|
+
"(with -s/-o or interactively) before using --last.",
|
|
42
|
+
file=sys.stderr,
|
|
43
|
+
)
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
try:
|
|
46
|
+
return json.loads(path.read_text())
|
|
47
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
48
|
+
print(f"Error: could not read saved config at {path}: {exc}", file=sys.stderr)
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_extensions(raw: str) -> list:
|
|
53
|
+
return [e.strip().lower().lstrip(".") for e in raw.split(",") if e.strip()]
|
|
54
|
+
|
|
55
|
+
|
|
15
56
|
def strip_quotes(text: str) -> str:
|
|
16
57
|
if len(text) >= 2 and text[0] == text[-1] and text[0] in ("'", '"'):
|
|
17
58
|
return text[1:-1]
|
|
@@ -51,14 +92,20 @@ def parse_args(argv=None) -> argparse.Namespace:
|
|
|
51
92
|
)
|
|
52
93
|
parser.add_argument(
|
|
53
94
|
"-e", "--ext",
|
|
54
|
-
default=
|
|
55
|
-
help="Comma-separated extensions to convert (default:
|
|
95
|
+
default=None,
|
|
96
|
+
help=f"Comma-separated extensions to convert (default: {','.join(DEFAULT_EXTENSIONS)})",
|
|
56
97
|
)
|
|
57
98
|
parser.add_argument(
|
|
58
99
|
"-f", "--force",
|
|
59
100
|
action="store_true",
|
|
60
101
|
help="Force re-conversion even if output .md already exists and is up to date",
|
|
61
102
|
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"-l", "--last",
|
|
105
|
+
action="store_true",
|
|
106
|
+
help="Reuse the source, destination, and extensions from the last run "
|
|
107
|
+
"(skips prompts; cannot be combined with -s/-o)",
|
|
108
|
+
)
|
|
62
109
|
return parser.parse_args(argv)
|
|
63
110
|
|
|
64
111
|
|
|
@@ -124,16 +171,32 @@ def convert_files(target_files: list, src_dir: Path, dest_dir: Path, force: bool
|
|
|
124
171
|
|
|
125
172
|
def main(argv=None) -> int:
|
|
126
173
|
args = parse_args(argv)
|
|
127
|
-
extensions = [e.strip().lower().lstrip(".") for e in args.ext.split(",") if e.strip()]
|
|
128
174
|
|
|
129
|
-
|
|
130
|
-
|
|
175
|
+
if args.last and (args.source or args.output):
|
|
176
|
+
print("Error: --last cannot be combined with -s/--source or -o/--output.", file=sys.stderr)
|
|
177
|
+
return 1
|
|
178
|
+
|
|
179
|
+
if args.last:
|
|
180
|
+
last = load_last_run()
|
|
181
|
+
src_dir = Path(last["source"])
|
|
182
|
+
if not src_dir.is_dir():
|
|
183
|
+
print(f"Error: last-used source directory no longer exists: {src_dir}", file=sys.stderr)
|
|
184
|
+
return 1
|
|
185
|
+
src_dir = src_dir.resolve()
|
|
186
|
+
dest_dir = Path(last["destination"])
|
|
187
|
+
extensions = parse_extensions(args.ext) if args.ext else last.get("extensions", DEFAULT_EXTENSIONS)
|
|
188
|
+
else:
|
|
189
|
+
extensions = parse_extensions(args.ext) if args.ext else DEFAULT_EXTENSIONS
|
|
190
|
+
src_dir = resolve_source(args)
|
|
191
|
+
dest_dir = resolve_destination(args, src_dir)
|
|
131
192
|
|
|
132
193
|
if not dest_dir.exists():
|
|
133
194
|
print(f"Destination does not exist, creating: {dest_dir}")
|
|
134
195
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
135
196
|
dest_dir = dest_dir.resolve()
|
|
136
197
|
|
|
198
|
+
save_last_run(src_dir, dest_dir, extensions)
|
|
199
|
+
|
|
137
200
|
print(SEPARATOR)
|
|
138
201
|
print(f"Source folder: {src_dir}")
|
|
139
202
|
print(f"Destination folder: {dest_dir}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|