mdengine 0.1.0__tar.gz → 0.2.2__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.
- {mdengine-0.1.0 → mdengine-0.2.2}/PKG-INFO +42 -6
- {mdengine-0.1.0 → mdengine-0.2.2}/README.md +13 -3
- {mdengine-0.1.0 → mdengine-0.2.2}/pyproject.toml +40 -4
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/__init__.py +1 -1
- mdengine-0.2.2/src/md_generator/url/__init__.py +24 -0
- mdengine-0.2.2/src/md_generator/url/api/__init__.py +1 -0
- mdengine-0.2.2/src/md_generator/url/api/convert_runner.py +34 -0
- mdengine-0.2.2/src/md_generator/url/api/jobs.py +92 -0
- mdengine-0.2.2/src/md_generator/url/api/main.py +377 -0
- mdengine-0.2.2/src/md_generator/url/api/mcp_server.py +37 -0
- mdengine-0.2.2/src/md_generator/url/api/mcp_setup.py +54 -0
- mdengine-0.2.2/src/md_generator/url/api/query_options.py +61 -0
- mdengine-0.2.2/src/md_generator/url/api/settings.py +25 -0
- mdengine-0.2.2/src/md_generator/url/assets.py +174 -0
- mdengine-0.2.2/src/md_generator/url/convert_impl.py +128 -0
- mdengine-0.2.2/src/md_generator/url/converter.py +200 -0
- mdengine-0.2.2/src/md_generator/url/crawl.py +223 -0
- mdengine-0.2.2/src/md_generator/url/extract.py +70 -0
- mdengine-0.2.2/src/md_generator/url/fetch.py +129 -0
- mdengine-0.2.2/src/md_generator/url/html_to_md.py +64 -0
- mdengine-0.2.2/src/md_generator/url/options.py +66 -0
- mdengine-0.2.2/src/md_generator/url/page_convert.py +102 -0
- mdengine-0.2.2/src/md_generator/url/post_convert_assets.py +387 -0
- mdengine-0.2.2/src/md_generator/url/robots_util.py +56 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/mdengine.egg-info/PKG-INFO +42 -6
- {mdengine-0.1.0 → mdengine-0.2.2}/src/mdengine.egg-info/SOURCES.txt +20 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/mdengine.egg-info/entry_points.txt +1 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/mdengine.egg-info/requires.txt +28 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/LICENSE +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/setup.cfg +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/convert_runner.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/jobs.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/main.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/mcp_server.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/mcp_setup.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/query_options.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/settings.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/convert_impl.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/converter.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/options.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/main.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/mcp_server.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/query_options.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/settings.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/staging.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/zip_bundle.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/backends/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/backends/base.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/backends/easy.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/backends/paddle.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/backends/tesseract.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/convert_impl.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/converter.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/emit.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/io_util.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/utils.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/api/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/api/main.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/api/mcp_server.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/api/settings.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/api/zip_bundle.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/converter.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/md_emit.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/pdf_extract.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/utils.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/convert_runner.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/jobs.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/main.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/mcp_server.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/mcp_setup.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/query_options.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/settings.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/convert_impl.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/converter.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/embedded_extract.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/ooxml_media.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/options.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/post_assets.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/text_formatting.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/vendor_pdf_md/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/vendor_pdf_md/convert.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/vendor_word_md/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/vendor_word_md/convert.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/zip_deep.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/convert_runner.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/jobs.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/main.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/mcp_server.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/mcp_setup.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/query_options.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/settings.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/convert_impl.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/converter.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/format_detect.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/md_emit_json.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/md_emit_txt.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/md_emit_xml.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/options.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/api/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/api/convert_util.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/api/jobs.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/api/main.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/api/mcp_server.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/artifact.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/converter.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/settings.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/api/__init__.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/api/app.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/convert_config.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/converter.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/converter_core.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/excel_reader.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/markdown_emitter.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/mcp_server.py +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/mdengine.egg-info/dependency_links.txt +0 -0
- {mdengine-0.1.0 → mdengine-0.2.2}/src/mdengine.egg-info/top_level.txt +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mdengine
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Convert PDF, Office, images, text/JSON/XML,
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Convert PDF, Office, images, text/JSON/XML, ZIP archives, and web URLs to Markdown.
|
|
5
5
|
Author: mdengine contributors
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/vishal7090/md-generator
|
|
8
8
|
Project-URL: Repository, https://github.com/vishal7090/md-generator
|
|
9
9
|
Project-URL: Issues, https://github.com/vishal7090/md-generator/issues
|
|
10
|
-
Keywords: markdown,pdf,docx,pptx,xlsx,ocr,zip
|
|
10
|
+
Keywords: markdown,pdf,docx,pptx,xlsx,ocr,zip,url,html
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
13
|
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
@@ -46,6 +46,28 @@ Provides-Extra: text
|
|
|
46
46
|
Provides-Extra: archive
|
|
47
47
|
Requires-Dist: Pillow>=10.0.0; extra == "archive"
|
|
48
48
|
Requires-Dist: pytesseract>=0.3.10; extra == "archive"
|
|
49
|
+
Provides-Extra: url
|
|
50
|
+
Requires-Dist: httpx>=0.27.0; extra == "url"
|
|
51
|
+
Requires-Dist: beautifulsoup4>=4.12.0; extra == "url"
|
|
52
|
+
Requires-Dist: lxml>=5.0.0; extra == "url"
|
|
53
|
+
Requires-Dist: lxml_html_clean>=0.4.0; extra == "url"
|
|
54
|
+
Requires-Dist: markdownify>=0.14.0; extra == "url"
|
|
55
|
+
Requires-Dist: readability-lxml>=0.8.1; extra == "url"
|
|
56
|
+
Provides-Extra: url-full
|
|
57
|
+
Requires-Dist: httpx>=0.27.0; extra == "url-full"
|
|
58
|
+
Requires-Dist: beautifulsoup4>=4.12.0; extra == "url-full"
|
|
59
|
+
Requires-Dist: lxml>=5.0.0; extra == "url-full"
|
|
60
|
+
Requires-Dist: lxml_html_clean>=0.4.0; extra == "url-full"
|
|
61
|
+
Requires-Dist: markdownify>=0.14.0; extra == "url-full"
|
|
62
|
+
Requires-Dist: readability-lxml>=0.8.1; extra == "url-full"
|
|
63
|
+
Requires-Dist: pymupdf>=1.24.0; extra == "url-full"
|
|
64
|
+
Requires-Dist: pdfplumber>=0.11.0; extra == "url-full"
|
|
65
|
+
Requires-Dist: mammoth>=1.8.0; extra == "url-full"
|
|
66
|
+
Requires-Dist: python-pptx>=1.0.0; extra == "url-full"
|
|
67
|
+
Requires-Dist: Pillow>=10.0.0; extra == "url-full"
|
|
68
|
+
Requires-Dist: olefile>=0.47; extra == "url-full"
|
|
69
|
+
Requires-Dist: openpyxl>=3.1.0; extra == "url-full"
|
|
70
|
+
Requires-Dist: pytesseract>=0.3.10; extra == "url-full"
|
|
49
71
|
Provides-Extra: api
|
|
50
72
|
Requires-Dist: fastapi>=0.115.0; extra == "api"
|
|
51
73
|
Requires-Dist: uvicorn[standard]>=0.32.0; extra == "api"
|
|
@@ -60,6 +82,7 @@ Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
|
60
82
|
Requires-Dist: httpx>=0.27.0; extra == "dev"
|
|
61
83
|
Requires-Dist: mdengine[api]; extra == "dev"
|
|
62
84
|
Requires-Dist: mdengine[mcp]; extra == "dev"
|
|
85
|
+
Requires-Dist: mdengine[url]; extra == "dev"
|
|
63
86
|
Provides-Extra: all
|
|
64
87
|
Requires-Dist: pymupdf>=1.24.0; extra == "all"
|
|
65
88
|
Requires-Dist: pdfplumber>=0.11.0; extra == "all"
|
|
@@ -82,6 +105,9 @@ Requires-Dist: httpx>=0.27.0; extra == "all"
|
|
|
82
105
|
Requires-Dist: pydantic-settings>=2.0.0; extra == "all"
|
|
83
106
|
Requires-Dist: mcp>=1.2.0; extra == "all"
|
|
84
107
|
Requires-Dist: fastmcp>=2.3.0; extra == "all"
|
|
108
|
+
Requires-Dist: beautifulsoup4>=4.12.0; extra == "all"
|
|
109
|
+
Requires-Dist: lxml_html_clean>=0.4.0; extra == "all"
|
|
110
|
+
Requires-Dist: readability-lxml>=0.8.1; extra == "all"
|
|
85
111
|
Dynamic: license-file
|
|
86
112
|
|
|
87
113
|
# mdengine
|
|
@@ -158,6 +184,8 @@ pip install "mdengine[all]"
|
|
|
158
184
|
| `image-ocr` | Heavy OCR backends (pytesseract, paddle, easyocr, …) |
|
|
159
185
|
| `text` | TXT / JSON / XML converter (stdlib-oriented; marker extra) |
|
|
160
186
|
| `archive` | ZIP → Markdown layout (Pillow; optional tesseract for inline image OCR) |
|
|
187
|
+
| `url` | HTTP(S) HTML → Markdown (httpx, readability-lxml, markdownify, BeautifulSoup, lxml) |
|
|
188
|
+
| `url-full` | `url` plus PDF/Word/PPTX/XLSX/archive stack for **post-converting** downloaded linked files to Markdown |
|
|
161
189
|
| `api` | FastAPI, uvicorn, httpx, pydantic-settings |
|
|
162
190
|
| `mcp` | MCP servers (`mcp`, `fastmcp` where used) |
|
|
163
191
|
| `dev` | pytest + API/MCP test helpers |
|
|
@@ -199,6 +227,7 @@ If the shell reports “command not found”, ensure the Python **Scripts** dire
|
|
|
199
227
|
| `md-image` | `md_generator.image.converter:main` | `md-image ./scans page.md` |
|
|
200
228
|
| `md-text` | `md_generator.text.converter:main` | `md-text config.xml out.md` |
|
|
201
229
|
| `md-zip` | `md_generator.archive.converter:main` | `md-zip bundle.zip ./zip-out` |
|
|
230
|
+
| `md-url` | `md_generator.url.converter:main` | `md-url https://example.com/doc ./web-out --artifact-layout` |
|
|
202
231
|
|
|
203
232
|
Every command accepts **`-h` / `--help`** for full flags (artifact layout, OCR, ZIP options, etc.).
|
|
204
233
|
|
|
@@ -215,6 +244,7 @@ md-xlsx -i export.csv -o ./csv-out
|
|
|
215
244
|
md-image ./photos ocr.md --engines tess --strategy best
|
|
216
245
|
md-text data.json data.md
|
|
217
246
|
md-zip archive.zip ./unzipped-md
|
|
247
|
+
md-url https://example.com/page ./page-bundle --artifact-layout
|
|
218
248
|
```
|
|
219
249
|
|
|
220
250
|
**Windows PowerShell** (same commands; use backslashes for paths if you prefer)
|
|
@@ -222,6 +252,7 @@ md-zip archive.zip ./unzipped-md
|
|
|
222
252
|
```powershell
|
|
223
253
|
md-pdf .\manual.pdf .\out\doc.md
|
|
224
254
|
md-zip .\archive.zip .\zip-out
|
|
255
|
+
md-url https://example.com/page .\page-bundle --artifact-layout
|
|
225
256
|
```
|
|
226
257
|
|
|
227
258
|
**Windows CMD**
|
|
@@ -229,11 +260,12 @@ md-zip .\archive.zip .\zip-out
|
|
|
229
260
|
```cmd
|
|
230
261
|
md-pdf manual.pdf out\doc.md
|
|
231
262
|
md-zip archive.zip zip-out
|
|
263
|
+
md-url https://example.com/page page-bundle --artifact-layout
|
|
232
264
|
```
|
|
233
265
|
|
|
234
266
|
### 5. Run without `pip install` (repo clone + `PYTHONPATH`)
|
|
235
267
|
|
|
236
|
-
The folders `pdf-to-md/`, `word-to-md/`, … contain a thin `converter.py` that calls the same code as `md-pdf`, `md-word`, etc. From the **repository root**, point Python at `src` so `md_generator` imports, then run the shim:
|
|
268
|
+
The folders `pdf-to-md/`, `word-to-md/`, `url-to-md/`, … contain a thin `converter.py` that calls the same code as `md-pdf`, `md-word`, etc. From the **repository root**, point Python at `src` so `md_generator` imports, then run the shim:
|
|
237
269
|
|
|
238
270
|
**PowerShell**
|
|
239
271
|
|
|
@@ -394,12 +426,12 @@ convert_zip(
|
|
|
394
426
|
|
|
395
427
|
All format APIs follow a similar pattern:
|
|
396
428
|
|
|
397
|
-
- **`POST /convert/sync`** — upload a file; response is often a **ZIP** (artifact bundle) for larger formats.
|
|
429
|
+
- **`POST /convert/sync`** — upload a file (most converters) **or** send JSON (`url-to-md`); response is often a **ZIP** (artifact bundle) for larger formats.
|
|
398
430
|
- **`POST /convert/jobs`** — async job; returns `job_id`.
|
|
399
431
|
- **`GET /convert/jobs/{job_id}`** — status.
|
|
400
432
|
- **`GET /convert/jobs/{job_id}/download`** — download result when ready.
|
|
401
433
|
|
|
402
|
-
Upload field name is **`file`** (multipart form). Use `httpx` or `curl -F "file=@path/to/file"`.
|
|
434
|
+
Upload field name is **`file`** (multipart form) for file-based converters. Use `httpx` or `curl -F "file=@path/to/file"`. **URL** conversion uses a **JSON** body (`url` or `urls`); see [url-to-md/README.md](url-to-md/README.md).
|
|
403
435
|
|
|
404
436
|
### Run with Uvicorn
|
|
405
437
|
|
|
@@ -414,6 +446,7 @@ Install `mdengine[api]` plus the format extra(s), then run the **`app`** object
|
|
|
414
446
|
| Image | `md_generator.image.api.main:app` | `image`, `api`, `mcp` |
|
|
415
447
|
| Text/JSON/XML | `md_generator.text.api.main:app` | `text`, `api`, `mcp` |
|
|
416
448
|
| ZIP | `md_generator.archive.api.main:app` | `archive`, `api`, `mcp` (+ extras for nested office/PDF) |
|
|
449
|
+
| URL / HTML | `md_generator.url.api.main:app` | `url`, `api`, `mcp` |
|
|
417
450
|
|
|
418
451
|
Examples:
|
|
419
452
|
|
|
@@ -421,6 +454,7 @@ Examples:
|
|
|
421
454
|
uvicorn md_generator.pdf.api.main:app --host 127.0.0.1 --port 8001
|
|
422
455
|
uvicorn md_generator.word.api.main:app --host 127.0.0.1 --port 8002
|
|
423
456
|
uvicorn md_generator.archive.api.main:app --host 127.0.0.1 --port 8010
|
|
457
|
+
uvicorn md_generator.url.api.main:app --host 127.0.0.1 --port 8011
|
|
424
458
|
```
|
|
425
459
|
|
|
426
460
|
### MCP over HTTP on the same server
|
|
@@ -439,6 +473,7 @@ Prefixes differ per service (often read from a `.env` file next to the process):
|
|
|
439
473
|
| PPTX | `PPT_TO_MD_` | `PPT_TO_MD_MAX_UPLOAD_MB`, … |
|
|
440
474
|
| Text | `TXT_JSON_XML_TO_MD_` | same pattern |
|
|
441
475
|
| XLSX | `XLSX_TO_MD_` | `XLSX_TO_MD_TEMP_DIR`, `XLSX_TO_MD_CORS_ORIGINS`, etc. (see `md_generator.xlsx.api.app`) |
|
|
476
|
+
| URL | `URL_TO_MD_` | `URL_TO_MD_MAX_SYNC_URLS`, `URL_TO_MD_MAX_SYNC_CRAWL_PAGES`, `URL_TO_MD_MAX_JOB_URLS`, `URL_TO_MD_JOB_TTL_SECONDS`, `URL_TO_MD_TEMP_DIR`, `URL_TO_MD_CORS_ORIGINS` |
|
|
442
477
|
|
|
443
478
|
Exact variable names match the `ApiSettings` / helper functions in each `api/settings` or `api/app` module.
|
|
444
479
|
|
|
@@ -461,6 +496,7 @@ Two usage patterns:
|
|
|
461
496
|
| PDF (FastMCP) | `python -m md_generator.pdf.api.mcp_server` / `--transport stdio` / `sse` / `streamable-http` |
|
|
462
497
|
| PPTX | `python -m md_generator.ppt.api.mcp_server` (see module docstring for flags) |
|
|
463
498
|
| Image | `python -m md_generator.image.api.mcp_server` (see module for CLI) |
|
|
499
|
+
| URL / HTML | `python -m md_generator.url.api.mcp_server` / `--transport sse` / `--transport streamable-http` |
|
|
464
500
|
|
|
465
501
|
**Word** and **XLSX** also ship a small runner script in the repo:
|
|
466
502
|
|
|
@@ -72,6 +72,8 @@ pip install "mdengine[all]"
|
|
|
72
72
|
| `image-ocr` | Heavy OCR backends (pytesseract, paddle, easyocr, …) |
|
|
73
73
|
| `text` | TXT / JSON / XML converter (stdlib-oriented; marker extra) |
|
|
74
74
|
| `archive` | ZIP → Markdown layout (Pillow; optional tesseract for inline image OCR) |
|
|
75
|
+
| `url` | HTTP(S) HTML → Markdown (httpx, readability-lxml, markdownify, BeautifulSoup, lxml) |
|
|
76
|
+
| `url-full` | `url` plus PDF/Word/PPTX/XLSX/archive stack for **post-converting** downloaded linked files to Markdown |
|
|
75
77
|
| `api` | FastAPI, uvicorn, httpx, pydantic-settings |
|
|
76
78
|
| `mcp` | MCP servers (`mcp`, `fastmcp` where used) |
|
|
77
79
|
| `dev` | pytest + API/MCP test helpers |
|
|
@@ -113,6 +115,7 @@ If the shell reports “command not found”, ensure the Python **Scripts** dire
|
|
|
113
115
|
| `md-image` | `md_generator.image.converter:main` | `md-image ./scans page.md` |
|
|
114
116
|
| `md-text` | `md_generator.text.converter:main` | `md-text config.xml out.md` |
|
|
115
117
|
| `md-zip` | `md_generator.archive.converter:main` | `md-zip bundle.zip ./zip-out` |
|
|
118
|
+
| `md-url` | `md_generator.url.converter:main` | `md-url https://example.com/doc ./web-out --artifact-layout` |
|
|
116
119
|
|
|
117
120
|
Every command accepts **`-h` / `--help`** for full flags (artifact layout, OCR, ZIP options, etc.).
|
|
118
121
|
|
|
@@ -129,6 +132,7 @@ md-xlsx -i export.csv -o ./csv-out
|
|
|
129
132
|
md-image ./photos ocr.md --engines tess --strategy best
|
|
130
133
|
md-text data.json data.md
|
|
131
134
|
md-zip archive.zip ./unzipped-md
|
|
135
|
+
md-url https://example.com/page ./page-bundle --artifact-layout
|
|
132
136
|
```
|
|
133
137
|
|
|
134
138
|
**Windows PowerShell** (same commands; use backslashes for paths if you prefer)
|
|
@@ -136,6 +140,7 @@ md-zip archive.zip ./unzipped-md
|
|
|
136
140
|
```powershell
|
|
137
141
|
md-pdf .\manual.pdf .\out\doc.md
|
|
138
142
|
md-zip .\archive.zip .\zip-out
|
|
143
|
+
md-url https://example.com/page .\page-bundle --artifact-layout
|
|
139
144
|
```
|
|
140
145
|
|
|
141
146
|
**Windows CMD**
|
|
@@ -143,11 +148,12 @@ md-zip .\archive.zip .\zip-out
|
|
|
143
148
|
```cmd
|
|
144
149
|
md-pdf manual.pdf out\doc.md
|
|
145
150
|
md-zip archive.zip zip-out
|
|
151
|
+
md-url https://example.com/page page-bundle --artifact-layout
|
|
146
152
|
```
|
|
147
153
|
|
|
148
154
|
### 5. Run without `pip install` (repo clone + `PYTHONPATH`)
|
|
149
155
|
|
|
150
|
-
The folders `pdf-to-md/`, `word-to-md/`, … contain a thin `converter.py` that calls the same code as `md-pdf`, `md-word`, etc. From the **repository root**, point Python at `src` so `md_generator` imports, then run the shim:
|
|
156
|
+
The folders `pdf-to-md/`, `word-to-md/`, `url-to-md/`, … contain a thin `converter.py` that calls the same code as `md-pdf`, `md-word`, etc. From the **repository root**, point Python at `src` so `md_generator` imports, then run the shim:
|
|
151
157
|
|
|
152
158
|
**PowerShell**
|
|
153
159
|
|
|
@@ -308,12 +314,12 @@ convert_zip(
|
|
|
308
314
|
|
|
309
315
|
All format APIs follow a similar pattern:
|
|
310
316
|
|
|
311
|
-
- **`POST /convert/sync`** — upload a file; response is often a **ZIP** (artifact bundle) for larger formats.
|
|
317
|
+
- **`POST /convert/sync`** — upload a file (most converters) **or** send JSON (`url-to-md`); response is often a **ZIP** (artifact bundle) for larger formats.
|
|
312
318
|
- **`POST /convert/jobs`** — async job; returns `job_id`.
|
|
313
319
|
- **`GET /convert/jobs/{job_id}`** — status.
|
|
314
320
|
- **`GET /convert/jobs/{job_id}/download`** — download result when ready.
|
|
315
321
|
|
|
316
|
-
Upload field name is **`file`** (multipart form). Use `httpx` or `curl -F "file=@path/to/file"`.
|
|
322
|
+
Upload field name is **`file`** (multipart form) for file-based converters. Use `httpx` or `curl -F "file=@path/to/file"`. **URL** conversion uses a **JSON** body (`url` or `urls`); see [url-to-md/README.md](url-to-md/README.md).
|
|
317
323
|
|
|
318
324
|
### Run with Uvicorn
|
|
319
325
|
|
|
@@ -328,6 +334,7 @@ Install `mdengine[api]` plus the format extra(s), then run the **`app`** object
|
|
|
328
334
|
| Image | `md_generator.image.api.main:app` | `image`, `api`, `mcp` |
|
|
329
335
|
| Text/JSON/XML | `md_generator.text.api.main:app` | `text`, `api`, `mcp` |
|
|
330
336
|
| ZIP | `md_generator.archive.api.main:app` | `archive`, `api`, `mcp` (+ extras for nested office/PDF) |
|
|
337
|
+
| URL / HTML | `md_generator.url.api.main:app` | `url`, `api`, `mcp` |
|
|
331
338
|
|
|
332
339
|
Examples:
|
|
333
340
|
|
|
@@ -335,6 +342,7 @@ Examples:
|
|
|
335
342
|
uvicorn md_generator.pdf.api.main:app --host 127.0.0.1 --port 8001
|
|
336
343
|
uvicorn md_generator.word.api.main:app --host 127.0.0.1 --port 8002
|
|
337
344
|
uvicorn md_generator.archive.api.main:app --host 127.0.0.1 --port 8010
|
|
345
|
+
uvicorn md_generator.url.api.main:app --host 127.0.0.1 --port 8011
|
|
338
346
|
```
|
|
339
347
|
|
|
340
348
|
### MCP over HTTP on the same server
|
|
@@ -353,6 +361,7 @@ Prefixes differ per service (often read from a `.env` file next to the process):
|
|
|
353
361
|
| PPTX | `PPT_TO_MD_` | `PPT_TO_MD_MAX_UPLOAD_MB`, … |
|
|
354
362
|
| Text | `TXT_JSON_XML_TO_MD_` | same pattern |
|
|
355
363
|
| XLSX | `XLSX_TO_MD_` | `XLSX_TO_MD_TEMP_DIR`, `XLSX_TO_MD_CORS_ORIGINS`, etc. (see `md_generator.xlsx.api.app`) |
|
|
364
|
+
| URL | `URL_TO_MD_` | `URL_TO_MD_MAX_SYNC_URLS`, `URL_TO_MD_MAX_SYNC_CRAWL_PAGES`, `URL_TO_MD_MAX_JOB_URLS`, `URL_TO_MD_JOB_TTL_SECONDS`, `URL_TO_MD_TEMP_DIR`, `URL_TO_MD_CORS_ORIGINS` |
|
|
356
365
|
|
|
357
366
|
Exact variable names match the `ApiSettings` / helper functions in each `api/settings` or `api/app` module.
|
|
358
367
|
|
|
@@ -375,6 +384,7 @@ Two usage patterns:
|
|
|
375
384
|
| PDF (FastMCP) | `python -m md_generator.pdf.api.mcp_server` / `--transport stdio` / `sse` / `streamable-http` |
|
|
376
385
|
| PPTX | `python -m md_generator.ppt.api.mcp_server` (see module docstring for flags) |
|
|
377
386
|
| Image | `python -m md_generator.image.api.mcp_server` (see module for CLI) |
|
|
387
|
+
| URL / HTML | `python -m md_generator.url.api.mcp_server` / `--transport sse` / `--transport streamable-http` |
|
|
378
388
|
|
|
379
389
|
**Word** and **XLSX** also ship a small runner script in the repo:
|
|
380
390
|
|
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mdengine"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Convert PDF, Office, images, text/JSON/XML,
|
|
7
|
+
version = "0.2.2"
|
|
8
|
+
description = "Convert PDF, Office, images, text/JSON/XML, ZIP archives, and web URLs to Markdown."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "MIT"
|
|
12
12
|
license-files = ["LICENSE"]
|
|
13
13
|
authors = [{ name = "mdengine contributors" }]
|
|
14
|
-
keywords = ["markdown", "pdf", "docx", "pptx", "xlsx", "ocr", "zip"]
|
|
14
|
+
keywords = ["markdown", "pdf", "docx", "pptx", "xlsx", "ocr", "zip", "url", "html"]
|
|
15
15
|
classifiers = [
|
|
16
16
|
"Programming Language :: Python :: 3",
|
|
17
17
|
"Programming Language :: Python :: 3 :: Only",
|
|
@@ -47,6 +47,31 @@ image-ocr = [
|
|
|
47
47
|
]
|
|
48
48
|
text = []
|
|
49
49
|
archive = ["Pillow>=10.0.0", "pytesseract>=0.3.10"]
|
|
50
|
+
url = [
|
|
51
|
+
"httpx>=0.27.0",
|
|
52
|
+
"beautifulsoup4>=4.12.0",
|
|
53
|
+
"lxml>=5.0.0",
|
|
54
|
+
"lxml_html_clean>=0.4.0",
|
|
55
|
+
"markdownify>=0.14.0",
|
|
56
|
+
"readability-lxml>=0.8.1",
|
|
57
|
+
]
|
|
58
|
+
# Convenience extra for URL fetch + post-convert of downloaded office/PDF/ZIP/etc.
|
|
59
|
+
url-full = [
|
|
60
|
+
"httpx>=0.27.0",
|
|
61
|
+
"beautifulsoup4>=4.12.0",
|
|
62
|
+
"lxml>=5.0.0",
|
|
63
|
+
"lxml_html_clean>=0.4.0",
|
|
64
|
+
"markdownify>=0.14.0",
|
|
65
|
+
"readability-lxml>=0.8.1",
|
|
66
|
+
"pymupdf>=1.24.0",
|
|
67
|
+
"pdfplumber>=0.11.0",
|
|
68
|
+
"mammoth>=1.8.0",
|
|
69
|
+
"python-pptx>=1.0.0",
|
|
70
|
+
"Pillow>=10.0.0",
|
|
71
|
+
"olefile>=0.47",
|
|
72
|
+
"openpyxl>=3.1.0",
|
|
73
|
+
"pytesseract>=0.3.10",
|
|
74
|
+
]
|
|
50
75
|
api = [
|
|
51
76
|
"fastapi>=0.115.0",
|
|
52
77
|
"uvicorn[standard]>=0.32.0",
|
|
@@ -55,7 +80,13 @@ api = [
|
|
|
55
80
|
"pydantic-settings>=2.0.0",
|
|
56
81
|
]
|
|
57
82
|
mcp = ["mcp>=1.2.0", "fastmcp>=2.3.0"]
|
|
58
|
-
dev = [
|
|
83
|
+
dev = [
|
|
84
|
+
"pytest>=8.0.0",
|
|
85
|
+
"httpx>=0.27.0",
|
|
86
|
+
"mdengine[api]",
|
|
87
|
+
"mdengine[mcp]",
|
|
88
|
+
"mdengine[url]",
|
|
89
|
+
]
|
|
59
90
|
all = [
|
|
60
91
|
"pymupdf>=1.24.0",
|
|
61
92
|
"pdfplumber>=0.11.0",
|
|
@@ -78,6 +109,9 @@ all = [
|
|
|
78
109
|
"pydantic-settings>=2.0.0",
|
|
79
110
|
"mcp>=1.2.0",
|
|
80
111
|
"fastmcp>=2.3.0",
|
|
112
|
+
"beautifulsoup4>=4.12.0",
|
|
113
|
+
"lxml_html_clean>=0.4.0",
|
|
114
|
+
"readability-lxml>=0.8.1",
|
|
81
115
|
]
|
|
82
116
|
|
|
83
117
|
[project.urls]
|
|
@@ -93,6 +127,7 @@ md-xlsx = "md_generator.xlsx.converter:main"
|
|
|
93
127
|
md-image = "md_generator.image.converter:main"
|
|
94
128
|
md-text = "md_generator.text.converter:main"
|
|
95
129
|
md-zip = "md_generator.archive.converter:main"
|
|
130
|
+
md-url = "md_generator.url.converter:main"
|
|
96
131
|
|
|
97
132
|
[tool.setuptools.packages.find]
|
|
98
133
|
where = ["src"]
|
|
@@ -107,5 +142,6 @@ testpaths = [
|
|
|
107
142
|
"image-to-md/tests",
|
|
108
143
|
"txt-json-xml-to-md/tests",
|
|
109
144
|
"zip-to-md/tests",
|
|
145
|
+
"url-to-md/tests",
|
|
110
146
|
]
|
|
111
147
|
markers = ["integration: optional dependency or slower checks"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Convert public HTTP(S) HTML pages to Markdown with downloaded assets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from md_generator.url.options import ConvertOptions
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
__all__ = ["ConvertOptions", "convert_url", "convert_urls_from_list"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __getattr__(name: str) -> object:
|
|
16
|
+
if name == "convert_url":
|
|
17
|
+
from md_generator.url.convert_impl import convert_url as v
|
|
18
|
+
|
|
19
|
+
return v
|
|
20
|
+
if name == "convert_urls_from_list":
|
|
21
|
+
from md_generator.url.convert_impl import convert_urls_from_list as v
|
|
22
|
+
|
|
23
|
+
return v
|
|
24
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# FastAPI + MCP for url-to-md
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import zipfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from md_generator.url.convert_impl import convert_url_job
|
|
8
|
+
from md_generator.url.options import ConvertOptions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def zip_directory(root: Path) -> bytes:
|
|
12
|
+
buf = io.BytesIO()
|
|
13
|
+
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
14
|
+
for p in sorted(root.rglob("*")):
|
|
15
|
+
if p.is_file():
|
|
16
|
+
arc = p.relative_to(root)
|
|
17
|
+
zf.write(p, arc.as_posix())
|
|
18
|
+
return buf.getvalue()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_artifact_zip_bytes(
|
|
22
|
+
*,
|
|
23
|
+
url: str | None,
|
|
24
|
+
urls: list[str] | None,
|
|
25
|
+
options: ConvertOptions,
|
|
26
|
+
) -> bytes:
|
|
27
|
+
"""Run conversion into a temp dir; return ZIP bytes (document.md + assets/ + pages/ when applicable)."""
|
|
28
|
+
import tempfile
|
|
29
|
+
|
|
30
|
+
with tempfile.TemporaryDirectory() as td:
|
|
31
|
+
out = Path(td) / "artifact"
|
|
32
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
convert_url_job(url, urls, out, options)
|
|
34
|
+
return zip_directory(out)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Job:
|
|
14
|
+
job_id: str
|
|
15
|
+
workspace: Path
|
|
16
|
+
status: str = "queued"
|
|
17
|
+
error: str | None = None
|
|
18
|
+
created_at: float = field(default_factory=time.time)
|
|
19
|
+
zip_path: Path | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JobStore:
|
|
23
|
+
def __init__(self, base_temp: Path | None, ttl_seconds: int) -> None:
|
|
24
|
+
self._base = base_temp
|
|
25
|
+
self._ttl = ttl_seconds
|
|
26
|
+
self._jobs: dict[str, Job] = {}
|
|
27
|
+
self._lock = threading.Lock()
|
|
28
|
+
self._sweeper_started = False
|
|
29
|
+
|
|
30
|
+
def _root(self) -> Path:
|
|
31
|
+
import tempfile
|
|
32
|
+
|
|
33
|
+
if self._base:
|
|
34
|
+
p = Path(self._base)
|
|
35
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
return p
|
|
37
|
+
return Path(tempfile.gettempdir()) / "url-to-md-jobs"
|
|
38
|
+
|
|
39
|
+
def start_sweeper(self) -> None:
|
|
40
|
+
if self._sweeper_started:
|
|
41
|
+
return
|
|
42
|
+
self._sweeper_started = True
|
|
43
|
+
|
|
44
|
+
def loop() -> None:
|
|
45
|
+
while True:
|
|
46
|
+
time.sleep(60)
|
|
47
|
+
self.sweep()
|
|
48
|
+
|
|
49
|
+
threading.Thread(target=loop, daemon=True).start()
|
|
50
|
+
|
|
51
|
+
def sweep(self) -> None:
|
|
52
|
+
now = time.time()
|
|
53
|
+
with self._lock:
|
|
54
|
+
dead: list[str] = []
|
|
55
|
+
for jid, job in self._jobs.items():
|
|
56
|
+
if job.status in ("done", "failed") and now - job.created_at > self._ttl:
|
|
57
|
+
dead.append(jid)
|
|
58
|
+
for jid in dead:
|
|
59
|
+
job = self._jobs.pop(jid, None)
|
|
60
|
+
if job and job.workspace.exists():
|
|
61
|
+
shutil.rmtree(job.workspace, ignore_errors=True)
|
|
62
|
+
|
|
63
|
+
def create_job(self) -> Job:
|
|
64
|
+
jid = str(uuid.uuid4())
|
|
65
|
+
ws = self._root() / jid
|
|
66
|
+
ws.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
job = Job(job_id=jid, workspace=ws)
|
|
68
|
+
with self._lock:
|
|
69
|
+
self._jobs[jid] = job
|
|
70
|
+
return job
|
|
71
|
+
|
|
72
|
+
def get(self, job_id: str) -> Job | None:
|
|
73
|
+
with self._lock:
|
|
74
|
+
return self._jobs.get(job_id)
|
|
75
|
+
|
|
76
|
+
def run_async(self, job: Job, fn: Callable[[], None]) -> None:
|
|
77
|
+
def target() -> None:
|
|
78
|
+
try:
|
|
79
|
+
job.status = "running"
|
|
80
|
+
fn()
|
|
81
|
+
job.status = "done"
|
|
82
|
+
except Exception as e:
|
|
83
|
+
job.status = "failed"
|
|
84
|
+
job.error = str(e)
|
|
85
|
+
|
|
86
|
+
threading.Thread(target=target, daemon=True).start()
|
|
87
|
+
|
|
88
|
+
def remove_after_download(self, job: Job) -> None:
|
|
89
|
+
with self._lock:
|
|
90
|
+
self._jobs.pop(job.job_id, None)
|
|
91
|
+
if job.workspace.exists():
|
|
92
|
+
shutil.rmtree(job.workspace, ignore_errors=True)
|