wikinator 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.
@@ -29,8 +29,10 @@ dist:
29
29
  uv publish
30
30
 
31
31
  clean:
32
- rm -rf __pycache__
33
32
  rm -fr .ruff_cache/
34
33
  rm -fr dist/
34
+ rm -fr .venv/
35
35
  rm -f dpytest_*.dat
36
- find . -type f -name ‘*.pyc’ -delete
36
+ rm -fr .pytest_cache/
37
+ find . -type f -name ‘*.pyc’ -delete
38
+ find . -name __pycache__ | xargs rm -rf
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wikinator
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Convert Google docs to markdown
5
5
  Project-URL: Documentation, https://github.com/philion/wikinator#readme
6
6
  Project-URL: Issues, https://github.com/philion/wikinator/issues
7
7
  Project-URL: Source, https://github.com/philion/wikinator
8
8
  License-File: LICENSE
9
9
  Requires-Python: >=3.13
10
+ Requires-Dist: docx2markdown>=0.1.1
10
11
  Requires-Dist: dotenv>=0.9.9
11
12
  Requires-Dist: google-api-python-client>=2.175.0
12
13
  Requires-Dist: google-auth-httplib2>=0.2.0
@@ -52,12 +53,12 @@ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql
52
53
  ## Build & Test
53
54
  1. Clone
54
55
  ```
55
- git clone ...
56
+ git clone https://github.com/philion/wikinator.git
56
57
  cd wikinator
57
58
  ```
58
59
  2. Run, with uv
59
60
  ```
60
- uv run -m wikinator [options]
61
+ uv run wikinator [options]
61
62
  ```
62
63
  3. Test, with pytest
63
64
  ```
@@ -66,13 +67,42 @@ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql
66
67
 
67
68
  ## Development Log
68
69
 
70
+ ### 2025-07-05
71
+ Starting work on image preservation.
72
+
73
+ Looking first at https://github.com/haesleinhuepf/docx2markdown for images.
74
+
75
+ Created a `Docx2MarkdownConverter` which almost works: images are put in the wrong path in the MD (`s/images/` instead of just `images`).
76
+ There's probably an easy fix, but lets try a pandoc version.
77
+
78
+ Creating `PandocConverter` to try and compare output.
79
+ ```
80
+ pandoc {indoc} -f docx -t markdown --wrap=none --markdown-headings=atx --extract-media=images -o {outdoc}
81
+ ```
82
+
83
+ Neither produces desired results.
84
+
85
+ Trying a literal hack of docx2markdown, to see how quickly I can fix the little problems I saw.
86
+
87
+ Got it working quickly, removed a little bug, got the images.
88
+
89
+ Now looking over the DOCX XML format to see how much I can scrape out.
90
+
91
+ https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.fontsizecomplexscript?view=openxml-3.0.1
92
+
93
+ Added detection for strikethru and Courier New (as "code font").
94
+
95
+ This is good enough for v0.2!
96
+ ```
97
+
98
+
69
99
  ### 2025-07-04
70
100
  Let's make a project! Today's goals:
71
101
  - [x] clean up code and README
72
102
  - [x] add CLI options, using type (not all implemented)
73
103
  - [x] initial commit to github
74
104
  - [ ] add image handling
75
- - [ ] upload to pypi and confirm uvx commands
105
+ - [x] upload to pypi and confirm uvx commands
76
106
 
77
107
  Cruft removed. README updated. (author waves, breaking 4th wall)
78
108
 
@@ -80,6 +110,10 @@ Moving on the main() cleanup and adding support for https://github.com/fastapi/t
80
110
 
81
111
  Added simple CLI options for src and dest. Got end-to-end tree processing.
82
112
 
113
+ Added Makefile to help with release management. Got PyPI setup: https://pypi.org/project/wikinator/
114
+
115
+ `uvx wikinator` is working.
116
+
83
117
  Let's go for git and call it a day!
84
118
 
85
119
  ### 2025-07-03
@@ -34,12 +34,12 @@ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql
34
34
  ## Build & Test
35
35
  1. Clone
36
36
  ```
37
- git clone ...
37
+ git clone https://github.com/philion/wikinator.git
38
38
  cd wikinator
39
39
  ```
40
40
  2. Run, with uv
41
41
  ```
42
- uv run -m wikinator [options]
42
+ uv run wikinator [options]
43
43
  ```
44
44
  3. Test, with pytest
45
45
  ```
@@ -48,13 +48,42 @@ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql
48
48
 
49
49
  ## Development Log
50
50
 
51
+ ### 2025-07-05
52
+ Starting work on image preservation.
53
+
54
+ Looking first at https://github.com/haesleinhuepf/docx2markdown for images.
55
+
56
+ Created a `Docx2MarkdownConverter` which almost works: images are put in the wrong path in the MD (`s/images/` instead of just `images`).
57
+ There's probably an easy fix, but lets try a pandoc version.
58
+
59
+ Creating `PandocConverter` to try and compare output.
60
+ ```
61
+ pandoc {indoc} -f docx -t markdown --wrap=none --markdown-headings=atx --extract-media=images -o {outdoc}
62
+ ```
63
+
64
+ Neither produces desired results.
65
+
66
+ Trying a literal hack of docx2markdown, to see how quickly I can fix the little problems I saw.
67
+
68
+ Got it working quickly, removed a little bug, got the images.
69
+
70
+ Now looking over the DOCX XML format to see how much I can scrape out.
71
+
72
+ https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.fontsizecomplexscript?view=openxml-3.0.1
73
+
74
+ Added detection for strikethru and Courier New (as "code font").
75
+
76
+ This is good enough for v0.2!
77
+ ```
78
+
79
+
51
80
  ### 2025-07-04
52
81
  Let's make a project! Today's goals:
53
82
  - [x] clean up code and README
54
83
  - [x] add CLI options, using type (not all implemented)
55
84
  - [x] initial commit to github
56
85
  - [ ] add image handling
57
- - [ ] upload to pypi and confirm uvx commands
86
+ - [x] upload to pypi and confirm uvx commands
58
87
 
59
88
  Cruft removed. README updated. (author waves, breaking 4th wall)
60
89
 
@@ -62,6 +91,10 @@ Moving on the main() cleanup and adding support for https://github.com/fastapi/t
62
91
 
63
92
  Added simple CLI options for src and dest. Got end-to-end tree processing.
64
93
 
94
+ Added Makefile to help with release management. Got PyPI setup: https://pypi.org/project/wikinator/
95
+
96
+ `uvx wikinator` is working.
97
+
65
98
  Let's go for git and call it a day!
66
99
 
67
100
  ### 2025-07-03
@@ -1,10 +1,11 @@
1
1
  [project]
2
2
  name = "wikinator"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Convert Google docs to markdown"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
7
7
  dependencies = [
8
+ "docx2markdown>=0.1.1",
8
9
  "dotenv>=0.9.9",
9
10
  "google-api-python-client>=2.175.0",
10
11
  "google-auth-httplib2>=0.2.0",
@@ -2,4 +2,4 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
  """project version"""
5
- __version__ = "0.1.0"
5
+ __version__ = "0.2.0"
@@ -2,11 +2,15 @@ import os
2
2
  from pathlib import Path, PurePath
3
3
  import logging
4
4
  import json
5
+ import subprocess
5
6
 
6
7
  from markitdown import MarkItDown
8
+ import docx2markdown
7
9
 
8
10
  import typer
9
11
 
12
+ from wikinator import docxit
13
+
10
14
  class Page:
11
15
  """
12
16
  Specific class for page, to help with validation, as graphql is strict about params
@@ -101,6 +105,7 @@ class Converter:
101
105
  logging.debug(f"No processor for {ext}, skipping {full_path}")
102
106
 
103
107
 
108
+ # remove
104
109
  class MarkitdownConverter(Converter):
105
110
  def _convert(self, filepath:Path) -> Page:
106
111
  """
@@ -126,21 +131,106 @@ class MarkitdownConverter(Converter):
126
131
  isPrivate = True,
127
132
  )
128
133
 
129
- # trying to make everything async
130
- # skipping for now
131
- # start workers
132
- # with multiprocessing.Pool(3) as worker_pool:
133
- # # create a list the same size for starmap
134
- # args = [(doc, outroot) for doc in docx_queue]
135
- # # run the workers
136
- # results = worker_pool.starmap(convert_docx, args)
137
- # # check result success
138
- # for result in results:
139
- # print(result)
134
+ # remove
135
+ class Docx2MarkdownConverter(Converter):
136
+ def convert(self, infile:Path, outroot:Path) -> Page:
137
+ """
138
+ Converts a docx file into markdown using docx2markdown
139
+ """
140
+ rel_path = PurePath(os.path.relpath(infile, self.root.parent)) # remove .parent to remove top dirname
141
+ rel_file = PurePath(rel_path.parent, infile.stem + ".md")
142
+ out_file = Path(outroot, rel_file)
143
+
144
+ #print(f"in: {infile}, out: {out_file}")
145
+
146
+ # FIXME deconstruct docx_to_markdown to seperate convert from write
147
+ # assure required dirs exist
148
+ out_file.parent.mkdir(parents=True, exist_ok=True)
149
+ docx2markdown.docx_to_markdown(infile, out_file)
150
+
151
+ return Page(
152
+ title = infile.stem,
153
+ path = str(rel_file),
154
+ content = "", ## FIXME
155
+ editor = "markdown",
156
+ locale = "en",
157
+ tags = None,
158
+ description = f"generated from: {infile}",
159
+ isPublished = False,
160
+ isPrivate = True,
161
+ )
162
+
163
+ # remove?
164
+ class PandocConverter(Converter):
165
+ def convert(self, infile:Path, outroot:Path) -> Page:
166
+ """
167
+ Converts a docx file into markdown using docx2markdown
168
+ """
169
+ rel_path = PurePath(os.path.relpath(infile, self.root.parent)) # remove .parent to remove top dirname
170
+ rel_file = PurePath(rel_path.parent, infile.stem + ".md")
171
+ out_file = Path(outroot, rel_file)
172
+
173
+ media_path = Path(outroot, "images")
174
+
175
+ # assure required dirs exist
176
+ out_file.parent.mkdir(parents=True, exist_ok=True)
177
+
178
+ # pandoc {indoc} -f docx -t markdown --wrap=none --markdown-headings=atx --extract-media=images -o {outdoc}
179
+ command = ["pandoc", infile,
180
+ "-f", "docx",
181
+ "-t", "markdown",
182
+ "--wrap=none",
183
+ "--markdown-headings=atx",
184
+ "--extract-media=" + str(media_path),
185
+ "-o", out_file]
186
+ subprocess.run(command, check=True)
187
+
188
+ return Page(
189
+ title = infile.stem,
190
+ path = str(rel_file),
191
+ content = "", ## FIXME
192
+ editor = "markdown",
193
+ locale = "en",
194
+ tags = None,
195
+ description = f"generated from: {infile}",
196
+ isPublished = False,
197
+ isPrivate = True,
198
+ )
199
+
200
+
201
+ class DocxitConverter(Converter):
202
+ def convert(self, infile:Path, outroot:Path) -> Page:
203
+ """
204
+ Converts a docx file into markdown using docxit
205
+ """
206
+ rel_path = PurePath(os.path.relpath(infile, self.root.parent)) # remove .parent to remove top dirname
207
+ rel_file = PurePath(rel_path.parent, infile.stem + ".md")
208
+ out_file = Path(outroot, rel_file)
209
+
210
+ # FIXME deconstruct docx_to_markdown to seperate convert from write
211
+ # assure required dirs exist
212
+ out_file.parent.mkdir(parents=True, exist_ok=True)
213
+ docxit.docx_to_markdown(infile, out_file)
214
+
215
+ return Page(
216
+ title = infile.stem,
217
+ path = str(rel_file),
218
+ content = "", ## FIXME
219
+ editor = "markdown",
220
+ locale = "en",
221
+ tags = None,
222
+ description = f"generated from: {infile}",
223
+ isPublished = False,
224
+ isPrivate = True,
225
+ )
140
226
 
141
227
 
142
228
  def do_convert(src: str, dest: str) -> None:
143
- MarkitdownConverter().convert_directory(src, dest)
229
+ #MarkitdownConverter().convert_directory(src, dest)
230
+ #Docx2MarkdownConverter().convert_directory(src, dest)
231
+ #PandocConverter().convert_directory(src, dest)
232
+ DocxitConverter().convert_directory(src, dest)
233
+
144
234
 
145
235
 
146
236
  def main() -> None:
@@ -0,0 +1,197 @@
1
+ # This file is original taken from https://github.com/haesleinhuepf/docx2markdown
2
+ # - src/docx2markdown/_docx_to_markdown.py
3
+ # It is being altered here to provide results needed for this project.
4
+ # original license: https://github.com/haesleinhuepf/docx2markdown/blob/main/LICENSE
5
+ # included here for completeness
6
+ #---
7
+ # BSD 3-Clause License
8
+ # Copyright (c) 2024, Robert Haase, ScaDS.AI, Uni Leipzig
9
+ # Redistribution and use in source and binary forms, with or without
10
+ # modification, are permitted provided that the following conditions are met:
11
+ # 1. Redistributions of source code must retain the above copyright notice, this
12
+ # list of conditions and the following disclaimer.
13
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
14
+ # this list of conditions and the following disclaimer in the documentation
15
+ # and/or other materials provided with the distribution.
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
+ #---
30
+ # All changes in the this version are Copyright (c) 2025, Paul Philion, Acme Rocket Company
31
+ # under the provided MIT license.
32
+
33
+ import docx
34
+ import os
35
+ from lxml import etree
36
+ from pathlib import Path
37
+
38
+ def docx_to_markdown(docx_file, output_md):
39
+ """Convert a .docx file to a Markdown file and a subfolder of images."""
40
+
41
+ folder = str(Path(output_md).parent)
42
+ image_folder = str(Path(output_md).parent / "images")
43
+
44
+ doc = docx.Document(docx_file)
45
+
46
+ paragraphs = list(doc.paragraphs)
47
+ tables = list(doc.tables)
48
+ markdown = []
49
+
50
+ # save all images
51
+ images = {}
52
+ for rel in doc.part.rels.values():
53
+ if "image" in rel.reltype:
54
+ image_filename = save_image(rel.target_part, image_folder)
55
+ images[rel.rId] = image_filename[len(folder)+1:]
56
+ #print("image file", image_filename, "-->", images[rel.rId])
57
+
58
+ #print("images", images)
59
+
60
+ for block in doc.element.body:
61
+ if block.tag.endswith('p'): # Handle paragraphs
62
+ paragraph = paragraphs.pop(0) # Match current paragraph
63
+ md_paragraph = ""
64
+
65
+ ### switching on paragraph.style.name
66
+
67
+ style_name = paragraph.style.name
68
+
69
+ if "List" in style_name:
70
+ prefix = get_bullet_point_prefix(paragraph)
71
+ md_paragraph = prefix # Markdown syntax for bullet points
72
+ elif "Heading 1" in style_name:
73
+ md_paragraph = "# "
74
+ elif "Heading 2" in style_name:
75
+ md_paragraph = "## "
76
+ elif "Heading 3" in style_name:
77
+ md_paragraph = "### "
78
+ elif "Heading 4" in style_name:
79
+ md_paragraph = "#### "
80
+ elif "Heading 5" in style_name:
81
+ md_paragraph = "##### "
82
+
83
+ elif "Normal" or "normal" in style_name:
84
+ md_paragraph = ""
85
+ else:
86
+ print("Unsupported style:", style_name)
87
+
88
+ md_paragraph += parse_run(paragraph, images)
89
+
90
+ markdown.append(md_paragraph)
91
+
92
+ elif block.tag.endswith('tbl'): # Handle tables (if present)
93
+ table = tables.pop(0) # Match current table
94
+ table_text = ""
95
+ for i, row in enumerate(table.rows):
96
+ table_text += "| " + " | ".join(cell.text.strip() for cell in row.cells) + " |\n"
97
+ if i == 0:
98
+ table_text += "| " + " | ".join("---" for _ in row.cells) + " |\n"
99
+
100
+ markdown.append(table_text)
101
+
102
+ elif block.tag.endswith('sectPr') or block.tag.endswith('sdt'):
103
+ # ignore
104
+ pass
105
+ else:
106
+ print("Unsupported block:", docx_file, block.tag)
107
+
108
+ # Write to Markdown file
109
+ with open(output_md, "w", encoding="utf-8") as md_file:
110
+ md_file.write("\n\n".join(markdown))
111
+
112
+
113
+ def extract_r_embed(xml_string):
114
+ """
115
+ Extract the value of r:embed from the given XML string.
116
+
117
+ :param xml_string: The XML content as a string.
118
+ :return: The value of r:embed or None if not found.
119
+ """
120
+ # Parse the XML
121
+ root = etree.fromstring(xml_string)
122
+
123
+ # Define the namespaces
124
+ namespaces = {
125
+ 'a': "http://schemas.openxmlformats.org/drawingml/2006/main",
126
+ 'r': "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
127
+ 'pic': "http://schemas.openxmlformats.org/drawingml/2006/picture",
128
+ }
129
+
130
+ # Use XPath to find the <a:blip> element with r:embed
131
+ blip = root.find(".//a:blip", namespaces=namespaces)
132
+
133
+ # Extract the r:embed attribute value
134
+ if blip is not None:
135
+ return blip.attrib.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed")
136
+ return None
137
+
138
+ def save_image(image_part, output_folder):
139
+ """Save an image to the output folder and return the filename."""
140
+ os.makedirs(output_folder, exist_ok=True)
141
+ image_filename = os.path.join(output_folder, os.path.basename(image_part.partname))
142
+ with open(image_filename, "wb") as img_file:
143
+ img_file.write(image_part.blob)
144
+ return str(image_filename).replace("\\", "/")
145
+
146
+
147
+ def get_list_level(paragraph):
148
+ """Determine the level of a bullet point or numbered list item."""
149
+ # Access the raw XML of the paragraph
150
+ p = paragraph._element
151
+ numPr = p.find(".//w:numPr", namespaces=p.nsmap)
152
+ if numPr is not None:
153
+ ilvl = numPr.find(".//w:ilvl", namespaces=p.nsmap)
154
+ if ilvl is not None:
155
+ return int(ilvl.get("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val"))
156
+ return 0
157
+
158
+ def get_bullet_point_prefix(paragraph):
159
+ """Determine the Markdown prefix for a bullet point based on its indentation level."""
160
+ level = get_list_level(paragraph)
161
+ return " " * level + "- " # Use Markdown syntax for nested lists
162
+
163
+ def parse_run(run, images):
164
+ """Go through document objects recursively and return markdown."""
165
+ sub_parts = list(run.iter_inner_content())
166
+ text = ""
167
+ for s in sub_parts:
168
+ if isinstance(s, str):
169
+ text += s
170
+ elif isinstance(s, docx.text.run.Run):
171
+ text += parse_run(s, images)
172
+ elif isinstance(s, docx.text.hyperlink.Hyperlink):
173
+ text += f"[{s.text}]({s.address})"
174
+ elif isinstance(s, docx.drawing.Drawing):
175
+ rId = extract_r_embed(s._element.xml)
176
+ image_url = images[rId]
177
+ text += f"![]({image_url})"
178
+ else:
179
+ print("unknown run type", s)
180
+
181
+ if isinstance(run, docx.text.run.Run):
182
+ if run.bold:
183
+ text = f"**{text}**"
184
+ if run.italic:
185
+ text = f"*{text}*"
186
+ if run.underline:
187
+ text = f"__{text}__"
188
+ if run.font.strike:
189
+ text = f"~~{text}~~"
190
+ # check .font for monospacing
191
+ # check style
192
+ if run.font.name == "Courier New":
193
+ print(text)
194
+ text = f"`{text}`"
195
+ #print(run.font.name)
196
+
197
+ return text
@@ -347,6 +347,19 @@ wheels = [
347
347
  { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
348
348
  ]
349
349
 
350
+ [[package]]
351
+ name = "docx2markdown"
352
+ version = "0.1.1"
353
+ source = { registry = "https://pypi.org/simple" }
354
+ dependencies = [
355
+ { name = "lxml" },
356
+ { name = "python-docx" },
357
+ ]
358
+ sdist = { url = "https://files.pythonhosted.org/packages/06/be/692425e45455b1bbff7ad0b9fa2fab31ac3f9da13f210ded06c0c99158f5/docx2markdown-0.1.1.tar.gz", hash = "sha256:177e7ca0c998139b9afb9ffbe9a3be05a27d60664ac602b38de643e7b343593d", size = 8256, upload-time = "2024-11-23T21:11:45.015Z" }
359
+ wheels = [
360
+ { url = "https://files.pythonhosted.org/packages/d2/f5/970f7c2f64331891ff8b8c1863a793779e4fc784dbd210e8ae3712bcfd0e/docx2markdown-0.1.1-py3-none-any.whl", hash = "sha256:a97d413e6a225d4a0932cc8660b92f10f5de28f0436bc18ee12ef3961cd4b2b1", size = 8393, upload-time = "2024-11-23T21:11:43.452Z" },
361
+ ]
362
+
350
363
  [[package]]
351
364
  name = "dotenv"
352
365
  version = "0.9.9"
@@ -1141,6 +1154,19 @@ wheels = [
1141
1154
  { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
1142
1155
  ]
1143
1156
 
1157
+ [[package]]
1158
+ name = "python-docx"
1159
+ version = "1.2.0"
1160
+ source = { registry = "https://pypi.org/simple" }
1161
+ dependencies = [
1162
+ { name = "lxml" },
1163
+ { name = "typing-extensions" },
1164
+ ]
1165
+ sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/eddfe33871520adab45aaa1a71f0402a2252050c14c7e3009446c8f4701c/python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce", size = 5723256, upload-time = "2025-06-16T20:46:27.921Z" }
1166
+ wheels = [
1167
+ { url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" },
1168
+ ]
1169
+
1144
1170
  [[package]]
1145
1171
  name = "python-dotenv"
1146
1172
  version = "1.1.1"
@@ -1364,6 +1390,7 @@ name = "wikinator"
1364
1390
  version = "0.1.0"
1365
1391
  source = { editable = "." }
1366
1392
  dependencies = [
1393
+ { name = "docx2markdown" },
1367
1394
  { name = "dotenv" },
1368
1395
  { name = "google-api-python-client" },
1369
1396
  { name = "google-auth-httplib2" },
@@ -1375,6 +1402,7 @@ dependencies = [
1375
1402
 
1376
1403
  [package.metadata]
1377
1404
  requires-dist = [
1405
+ { name = "docx2markdown", specifier = ">=0.1.1" },
1378
1406
  { name = "dotenv", specifier = ">=0.9.9" },
1379
1407
  { name = "google-api-python-client", specifier = ">=2.175.0" },
1380
1408
  { name = "google-auth-httplib2", specifier = ">=0.2.0" },
File without changes
File without changes
File without changes