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.
Files changed (127) hide show
  1. {mdengine-0.1.0 → mdengine-0.2.2}/PKG-INFO +42 -6
  2. {mdengine-0.1.0 → mdengine-0.2.2}/README.md +13 -3
  3. {mdengine-0.1.0 → mdengine-0.2.2}/pyproject.toml +40 -4
  4. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/__init__.py +1 -1
  5. mdengine-0.2.2/src/md_generator/url/__init__.py +24 -0
  6. mdengine-0.2.2/src/md_generator/url/api/__init__.py +1 -0
  7. mdengine-0.2.2/src/md_generator/url/api/convert_runner.py +34 -0
  8. mdengine-0.2.2/src/md_generator/url/api/jobs.py +92 -0
  9. mdengine-0.2.2/src/md_generator/url/api/main.py +377 -0
  10. mdengine-0.2.2/src/md_generator/url/api/mcp_server.py +37 -0
  11. mdengine-0.2.2/src/md_generator/url/api/mcp_setup.py +54 -0
  12. mdengine-0.2.2/src/md_generator/url/api/query_options.py +61 -0
  13. mdengine-0.2.2/src/md_generator/url/api/settings.py +25 -0
  14. mdengine-0.2.2/src/md_generator/url/assets.py +174 -0
  15. mdengine-0.2.2/src/md_generator/url/convert_impl.py +128 -0
  16. mdengine-0.2.2/src/md_generator/url/converter.py +200 -0
  17. mdengine-0.2.2/src/md_generator/url/crawl.py +223 -0
  18. mdengine-0.2.2/src/md_generator/url/extract.py +70 -0
  19. mdengine-0.2.2/src/md_generator/url/fetch.py +129 -0
  20. mdengine-0.2.2/src/md_generator/url/html_to_md.py +64 -0
  21. mdengine-0.2.2/src/md_generator/url/options.py +66 -0
  22. mdengine-0.2.2/src/md_generator/url/page_convert.py +102 -0
  23. mdengine-0.2.2/src/md_generator/url/post_convert_assets.py +387 -0
  24. mdengine-0.2.2/src/md_generator/url/robots_util.py +56 -0
  25. {mdengine-0.1.0 → mdengine-0.2.2}/src/mdengine.egg-info/PKG-INFO +42 -6
  26. {mdengine-0.1.0 → mdengine-0.2.2}/src/mdengine.egg-info/SOURCES.txt +20 -0
  27. {mdengine-0.1.0 → mdengine-0.2.2}/src/mdengine.egg-info/entry_points.txt +1 -0
  28. {mdengine-0.1.0 → mdengine-0.2.2}/src/mdengine.egg-info/requires.txt +28 -0
  29. {mdengine-0.1.0 → mdengine-0.2.2}/LICENSE +0 -0
  30. {mdengine-0.1.0 → mdengine-0.2.2}/setup.cfg +0 -0
  31. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/__init__.py +0 -0
  32. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/__init__.py +0 -0
  33. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/convert_runner.py +0 -0
  34. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/jobs.py +0 -0
  35. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/main.py +0 -0
  36. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/mcp_server.py +0 -0
  37. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/mcp_setup.py +0 -0
  38. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/query_options.py +0 -0
  39. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/api/settings.py +0 -0
  40. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/convert_impl.py +0 -0
  41. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/converter.py +0 -0
  42. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/archive/options.py +0 -0
  43. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/__init__.py +0 -0
  44. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/__init__.py +0 -0
  45. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/main.py +0 -0
  46. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/mcp_server.py +0 -0
  47. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/query_options.py +0 -0
  48. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/settings.py +0 -0
  49. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/staging.py +0 -0
  50. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/api/zip_bundle.py +0 -0
  51. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/backends/__init__.py +0 -0
  52. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/backends/base.py +0 -0
  53. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/backends/easy.py +0 -0
  54. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/backends/paddle.py +0 -0
  55. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/backends/tesseract.py +0 -0
  56. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/convert_impl.py +0 -0
  57. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/converter.py +0 -0
  58. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/emit.py +0 -0
  59. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/io_util.py +0 -0
  60. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/image/utils.py +0 -0
  61. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/__init__.py +0 -0
  62. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/api/__init__.py +0 -0
  63. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/api/main.py +0 -0
  64. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/api/mcp_server.py +0 -0
  65. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/api/settings.py +0 -0
  66. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/api/zip_bundle.py +0 -0
  67. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/converter.py +0 -0
  68. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/md_emit.py +0 -0
  69. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/pdf_extract.py +0 -0
  70. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/pdf/utils.py +0 -0
  71. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/__init__.py +0 -0
  72. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/__init__.py +0 -0
  73. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/convert_runner.py +0 -0
  74. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/jobs.py +0 -0
  75. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/main.py +0 -0
  76. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/mcp_server.py +0 -0
  77. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/mcp_setup.py +0 -0
  78. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/query_options.py +0 -0
  79. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/api/settings.py +0 -0
  80. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/convert_impl.py +0 -0
  81. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/converter.py +0 -0
  82. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/embedded_extract.py +0 -0
  83. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/ooxml_media.py +0 -0
  84. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/options.py +0 -0
  85. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/post_assets.py +0 -0
  86. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/text_formatting.py +0 -0
  87. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/vendor_pdf_md/__init__.py +0 -0
  88. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/vendor_pdf_md/convert.py +0 -0
  89. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/vendor_word_md/__init__.py +0 -0
  90. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/vendor_word_md/convert.py +0 -0
  91. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/ppt/zip_deep.py +0 -0
  92. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/__init__.py +0 -0
  93. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/__init__.py +0 -0
  94. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/convert_runner.py +0 -0
  95. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/jobs.py +0 -0
  96. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/main.py +0 -0
  97. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/mcp_server.py +0 -0
  98. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/mcp_setup.py +0 -0
  99. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/query_options.py +0 -0
  100. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/api/settings.py +0 -0
  101. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/convert_impl.py +0 -0
  102. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/converter.py +0 -0
  103. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/format_detect.py +0 -0
  104. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/md_emit_json.py +0 -0
  105. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/md_emit_txt.py +0 -0
  106. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/md_emit_xml.py +0 -0
  107. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/text/options.py +0 -0
  108. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/__init__.py +0 -0
  109. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/api/__init__.py +0 -0
  110. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/api/convert_util.py +0 -0
  111. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/api/jobs.py +0 -0
  112. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/api/main.py +0 -0
  113. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/api/mcp_server.py +0 -0
  114. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/artifact.py +0 -0
  115. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/converter.py +0 -0
  116. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/word/settings.py +0 -0
  117. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/__init__.py +0 -0
  118. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/api/__init__.py +0 -0
  119. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/api/app.py +0 -0
  120. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/convert_config.py +0 -0
  121. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/converter.py +0 -0
  122. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/converter_core.py +0 -0
  123. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/excel_reader.py +0 -0
  124. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/markdown_emitter.py +0 -0
  125. {mdengine-0.1.0 → mdengine-0.2.2}/src/md_generator/xlsx/mcp_server.py +0 -0
  126. {mdengine-0.1.0 → mdengine-0.2.2}/src/mdengine.egg-info/dependency_links.txt +0 -0
  127. {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.1.0
4
- Summary: Convert PDF, Office, images, text/JSON/XML, and ZIP archives to Markdown.
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.1.0"
8
- description = "Convert PDF, Office, images, text/JSON/XML, and ZIP archives to Markdown."
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 = ["pytest>=8.0.0", "httpx>=0.27.0", "mdengine[api]", "mdengine[mcp]"]
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"]
@@ -1,5 +1,5 @@
1
1
  """md_generator: PDF, Office, image, text, and ZIP to Markdown conversion."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.2"
4
4
 
5
5
  __all__ = ["__version__"]
@@ -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)