wikinator 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ # Publish on any tag starting with a `v`, e.g. v1.2.3
7
+ - v*
8
+
9
+ jobs:
10
+ pypi:
11
+ name: Publish to PyPI
12
+ runs-on: ubuntu-latest
13
+ # Environment and permissions trusted publishing.
14
+ environment:
15
+ # Create this environment in the GitHub repository under Settings -> Environments
16
+ name: release
17
+ permissions:
18
+ id-token: write
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: astral-sh/setup-uv@v3
22
+ - run: uv build
23
+ # Check that basic features work and we didn't miss to include crucial files
24
+ - name: Smoke test (wheel)
25
+ run: uv run --isolated --no-project -p 3.13 --with dist/*.whl tests/smoke_test.py
26
+ - name: Smoke test (source distribution)
27
+ run: uv run --isolated --no-project -p 3.13 --with dist/*.tar.gz tests/smoke_test.py
28
+ - run: uv publish --trusted-publishing always
@@ -0,0 +1,20 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ .python-version
9
+
10
+ # Virtual environments
11
+ .venv
12
+
13
+ # env and secure files
14
+ .env
15
+ .envrc
16
+ token.json
17
+ credentials.json
18
+
19
+ # Apple dingles
20
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Paul Philion
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,36 @@
1
+ .PHONY: all run clean
2
+
3
+ # Simple makefile to help me remember uv tasks
4
+ # Targets are:
5
+ # - lint : run ruff linter
6
+ # - fix : ... with fixes
7
+ # - test : run test suite
8
+ # - build : build
9
+ # - publish : publish
10
+ # - dist : clean, build, publish
11
+ # - clean : remove anything built
12
+
13
+
14
+ lint:
15
+ uvx ruff check
16
+
17
+ fix:
18
+ uvx ruff check --fix
19
+
20
+ test:
21
+ uv run pytest
22
+
23
+ build:
24
+ uv build
25
+
26
+ dist:
27
+ rm -fr dist/
28
+ uv build
29
+ uv publish
30
+
31
+ clean:
32
+ rm -rf __pycache__
33
+ rm -fr .ruff_cache/
34
+ rm -fr dist/
35
+ rm -f dpytest_*.dat
36
+ find . -type f -name ‘*.pyc’ -delete
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: wikinator
3
+ Version: 0.1.0
4
+ Summary: Convert Google docs to markdown
5
+ Project-URL: Documentation, https://github.com/philion/wikinator#readme
6
+ Project-URL: Issues, https://github.com/philion/wikinator/issues
7
+ Project-URL: Source, https://github.com/philion/wikinator
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.13
10
+ Requires-Dist: dotenv>=0.9.9
11
+ Requires-Dist: google-api-python-client>=2.175.0
12
+ Requires-Dist: google-auth-httplib2>=0.2.0
13
+ Requires-Dist: google-auth-oauthlib>=1.2.2
14
+ Requires-Dist: gql[all]>=3.5.3
15
+ Requires-Dist: markitdown[all]>=0.1.2
16
+ Requires-Dist: typer-slim>=0.16.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # wikinator
20
+
21
+ Convert a Google drive download into a markdown-based wiki.
22
+
23
+ **Note**: This is a work in progress, and not all features will be supported or working properly.
24
+
25
+ ## tl;dr
26
+ ```
27
+ uvx wikinator some/dir another_dir
28
+ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql-auth-token'
29
+ ```
30
+
31
+ Given a directory, convert supported file types into markdown-based files while maintaining names and directory structure. This can then be uploaded into various wiki systems.
32
+
33
+ ### Supported File Types
34
+ - DOCX files (default for GDocs) are converted to markdown
35
+ - images are extracted, uploaded and embedded in the markdown
36
+ - `text` and code file types are wrapped in markdown code blocks
37
+ - CSV and XSLT are converted to markdown tables
38
+ - for any document that is converted to markdown, a copy of the original is uploaded and attached
39
+
40
+ ### Supported Wiki Import
41
+ - wiki.js (and other GraphQL-based wikis)
42
+ - Obsidian
43
+
44
+ The development log will be kept here until the 1.0 release.
45
+
46
+ ## Usage
47
+ ```
48
+ uvx wikinator some/dir another_dir
49
+ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql-auth-token'
50
+ ```
51
+
52
+ ## Build & Test
53
+ 1. Clone
54
+ ```
55
+ git clone ...
56
+ cd wikinator
57
+ ```
58
+ 2. Run, with uv
59
+ ```
60
+ uv run -m wikinator [options]
61
+ ```
62
+ 3. Test, with pytest
63
+ ```
64
+ uv run pytest
65
+ ```
66
+
67
+ ## Development Log
68
+
69
+ ### 2025-07-04
70
+ Let's make a project! Today's goals:
71
+ - [x] clean up code and README
72
+ - [x] add CLI options, using type (not all implemented)
73
+ - [x] initial commit to github
74
+ - [ ] add image handling
75
+ - [ ] upload to pypi and confirm uvx commands
76
+
77
+ Cruft removed. README updated. (author waves, breaking 4th wall)
78
+
79
+ Moving on the main() cleanup and adding support for https://github.com/fastapi/typer
80
+
81
+ Added simple CLI options for src and dest. Got end-to-end tree processing.
82
+
83
+ Let's go for git and call it a day!
84
+
85
+ ### 2025-07-03
86
+ Next steps are testing different document converters and accessing google drive via API.
87
+
88
+ #### Markdown conversion libraries
89
+ - pandoc, see https://docs.asciidoctor.org/asciidoctor/latest/migrate/ms-word/
90
+ - markitdown, https://github.com/microsoft/markitdown
91
+ - docx2markdown, https://github.com/haesleinhuepf/docx2markdown
92
+ - docx2md, https://github.com/mattn/docx2md
93
+
94
+ Reference:
95
+ - https://www.docstomarkdown.pro/convert-word-or-docs-to-markdown-using-pandoc/
96
+
97
+ #### Google Drive API
98
+ Starting with https://developers.google.com/workspace/drive/api/quickstart/python
99
+
100
+ > Note: Follow those Google directions for setting up everything. It's complicated compared to simply generating a service token. Your intrepid author made different tokens in different accounts and couldn't access anything! And get permissions right! Document specific needs in intstall docs.
101
+
102
+ Further aside: There are two versions of the tool: file-based and google-takeout. The google related stuff will always be a bear to setup.
103
+
104
+ Made suffienct progress to feel like there a seperate CLI tool here. Set aside for now, and focus on:
105
+ 1. Build file-based output
106
+ 2. Generate and link images
107
+ 3. Clean up for initial 0.1 version
108
+
109
+ ### 2025-07-02
110
+ Initial time-boxed work started to examine what would be required to migrate our existing GoogleDocs-based info repo into a wiki, with wiki.js being targeted.
111
+
112
+ Initial proof-of-concept goals:
113
+ - Convert a docx page to md or asciidoc
114
+ - Upload test pages to wiki.js
115
+
116
+ I was able to get this working in sample code in a few hours.
@@ -0,0 +1,98 @@
1
+ # wikinator
2
+
3
+ Convert a Google drive download into a markdown-based wiki.
4
+
5
+ **Note**: This is a work in progress, and not all features will be supported or working properly.
6
+
7
+ ## tl;dr
8
+ ```
9
+ uvx wikinator some/dir another_dir
10
+ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql-auth-token'
11
+ ```
12
+
13
+ Given a directory, convert supported file types into markdown-based files while maintaining names and directory structure. This can then be uploaded into various wiki systems.
14
+
15
+ ### Supported File Types
16
+ - DOCX files (default for GDocs) are converted to markdown
17
+ - images are extracted, uploaded and embedded in the markdown
18
+ - `text` and code file types are wrapped in markdown code blocks
19
+ - CSV and XSLT are converted to markdown tables
20
+ - for any document that is converted to markdown, a copy of the original is uploaded and attached
21
+
22
+ ### Supported Wiki Import
23
+ - wiki.js (and other GraphQL-based wikis)
24
+ - Obsidian
25
+
26
+ The development log will be kept here until the 1.0 release.
27
+
28
+ ## Usage
29
+ ```
30
+ uvx wikinator some/dir another_dir
31
+ uvx wikinator some/dir -graphql https://wiki.example.com/graphql -token 'graphql-auth-token'
32
+ ```
33
+
34
+ ## Build & Test
35
+ 1. Clone
36
+ ```
37
+ git clone ...
38
+ cd wikinator
39
+ ```
40
+ 2. Run, with uv
41
+ ```
42
+ uv run -m wikinator [options]
43
+ ```
44
+ 3. Test, with pytest
45
+ ```
46
+ uv run pytest
47
+ ```
48
+
49
+ ## Development Log
50
+
51
+ ### 2025-07-04
52
+ Let's make a project! Today's goals:
53
+ - [x] clean up code and README
54
+ - [x] add CLI options, using type (not all implemented)
55
+ - [x] initial commit to github
56
+ - [ ] add image handling
57
+ - [ ] upload to pypi and confirm uvx commands
58
+
59
+ Cruft removed. README updated. (author waves, breaking 4th wall)
60
+
61
+ Moving on the main() cleanup and adding support for https://github.com/fastapi/typer
62
+
63
+ Added simple CLI options for src and dest. Got end-to-end tree processing.
64
+
65
+ Let's go for git and call it a day!
66
+
67
+ ### 2025-07-03
68
+ Next steps are testing different document converters and accessing google drive via API.
69
+
70
+ #### Markdown conversion libraries
71
+ - pandoc, see https://docs.asciidoctor.org/asciidoctor/latest/migrate/ms-word/
72
+ - markitdown, https://github.com/microsoft/markitdown
73
+ - docx2markdown, https://github.com/haesleinhuepf/docx2markdown
74
+ - docx2md, https://github.com/mattn/docx2md
75
+
76
+ Reference:
77
+ - https://www.docstomarkdown.pro/convert-word-or-docs-to-markdown-using-pandoc/
78
+
79
+ #### Google Drive API
80
+ Starting with https://developers.google.com/workspace/drive/api/quickstart/python
81
+
82
+ > Note: Follow those Google directions for setting up everything. It's complicated compared to simply generating a service token. Your intrepid author made different tokens in different accounts and couldn't access anything! And get permissions right! Document specific needs in intstall docs.
83
+
84
+ Further aside: There are two versions of the tool: file-based and google-takeout. The google related stuff will always be a bear to setup.
85
+
86
+ Made suffienct progress to feel like there a seperate CLI tool here. Set aside for now, and focus on:
87
+ 1. Build file-based output
88
+ 2. Generate and link images
89
+ 3. Clean up for initial 0.1 version
90
+
91
+ ### 2025-07-02
92
+ Initial time-boxed work started to examine what would be required to migrate our existing GoogleDocs-based info repo into a wiki, with wiki.js being targeted.
93
+
94
+ Initial proof-of-concept goals:
95
+ - Convert a docx page to md or asciidoc
96
+ - Upload test pages to wiki.js
97
+
98
+ I was able to get this working in sample code in a few hours.
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "wikinator"
3
+ version = "0.1.0"
4
+ description = "Convert Google docs to markdown"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "dotenv>=0.9.9",
9
+ "google-api-python-client>=2.175.0",
10
+ "google-auth-httplib2>=0.2.0",
11
+ "google-auth-oauthlib>=1.2.2",
12
+ "gql[all]>=3.5.3",
13
+ "markitdown[all]>=0.1.2",
14
+ "typer-slim>=0.16.0",
15
+ ]
16
+
17
+ [project.urls]
18
+ Documentation = "https://github.com/philion/wikinator#readme"
19
+ Issues = "https://github.com/philion/wikinator/issues"
20
+ Source = "https://github.com/philion/wikinator"
21
+
22
+ [tool.coverage.run]
23
+ source_pkgs = ["wikinator", "tests"]
24
+ branch = true
25
+ parallel = true
26
+
27
+ [tool.coverage.paths]
28
+ clw = ["src/wikinator", "*/wikinator/src/wikinator"]
29
+ tests = ["tests", "*/wikinator/tests"]
30
+
31
+ [tool.coverage.report]
32
+ exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
33
+
34
+ [tool.hatch]
35
+
36
+ [tool.hatch.version]
37
+ path = "src/wikinator/__about__.py"
38
+
39
+ [build-system]
40
+ requires = ["hatchling"]
41
+ build-backend = "hatchling.build"
42
+
43
+ [tool.hatch.build.targets.wheel]
44
+ packages = ["src/wikinator"]
45
+
46
+ [project.scripts]
47
+ wikinator = "wikinator.app:main"
@@ -0,0 +1,5 @@
1
+ # SPDX-FileCopyrightText: 2025-present Paul Philion <philion@acmerocket.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """project version"""
5
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python
2
+
3
+ from wikinator.app import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,151 @@
1
+ import os
2
+ from pathlib import Path, PurePath
3
+ import logging
4
+ import json
5
+
6
+ from markitdown import MarkItDown
7
+
8
+ import typer
9
+
10
+ class Page:
11
+ """
12
+ Specific class for page, to help with validation, as graphql is strict about params
13
+ """
14
+ content: str
15
+ editor: str
16
+ isPublished: bool
17
+ isPrivate: bool
18
+ locale: str
19
+ path: str
20
+ tags: list[str]
21
+ title: str
22
+ description: str
23
+
24
+ def __init__(self, content: str, editor: str, isPublished: bool, isPrivate: bool,
25
+ locale: str, path: str, tags: list[str], title: str, description: str):
26
+ self.content = content
27
+ self.editor = editor
28
+ self.isPublished = isPublished
29
+ self.isPrivate = isPrivate
30
+ self.locale = locale
31
+ self.path = path
32
+ self.tags = tags
33
+ self.title = title
34
+ self.description = description
35
+
36
+ @classmethod
37
+ def load(cls, params: dict[str,any]):
38
+ return cls(
39
+ content = params["content"],
40
+ editor = params["editor"],
41
+ isPublished = params["isPublished"],
42
+ isPrivate = params["isPrivate"],
43
+ locale = params["locale"],
44
+ path = params["path"],
45
+ tags = params["tags"],
46
+ title = params["title"],
47
+ description = params["description"],
48
+ )
49
+
50
+ @classmethod
51
+ def load_json(cls, json_str:str):
52
+ return cls.load(cls, json.loads(json_str))
53
+
54
+ def write(self, root:str) -> None:
55
+ """
56
+ Output the converted document to the specified directory `root`.
57
+ Use the stored path to output relative to the provided root.
58
+ """
59
+ filename = self.path + '.md'
60
+ target = Path(root, filename)
61
+ # assure required dirs exist
62
+ target.parent.mkdir(parents=True, exist_ok=True)
63
+ # write the content
64
+ with open(target, 'w') as output_file:
65
+ # TODO write yaml-based meta data?
66
+ output_file.write(self.content)
67
+
68
+
69
+ class Converter:
70
+ root: Path # Root for file walk, and to resolve rol paths
71
+
72
+ def _convert(self, infile:Path) -> Page:
73
+ raise NotImplementedError
74
+
75
+ def convert(self, infile:Path, outroot:Path) -> Page:
76
+ # translate with subclass
77
+ new_doc = self._convert(infile)
78
+
79
+ #print("converted", infile, new_doc.path)
80
+
81
+ # write!
82
+ new_doc.write(outroot)
83
+
84
+ return new_doc
85
+
86
+ def convert_directory(self, inpath:str, outroot:str):
87
+ # load queues
88
+ #docx_queue: list[Path] = []
89
+ self.root = Path(inpath)
90
+
91
+ for root, dirs, files in os.walk(self.root):
92
+ for file in files:
93
+ full_path = Path(root, file)
94
+ ext = full_path.suffix.lower() # TODO strip first char, '.'
95
+
96
+ # TODO: generic mapping to queue based on ext.
97
+ if ext == ".docx":
98
+ #docx_queue.append(full_path) # TODO async!
99
+ self.convert(full_path, outroot)
100
+ else:
101
+ logging.debug(f"No processor for {ext}, skipping {full_path}")
102
+
103
+
104
+ class MarkitdownConverter(Converter):
105
+ def _convert(self, filepath:Path) -> Page:
106
+ """
107
+ Converts a markdown file into a struct to add to the wiki
108
+ """
109
+ md = MarkItDown(enable_plugins=False)
110
+ result = md.convert(filepath)
111
+
112
+ # determine the relative file path
113
+ # this *includes* the top-level directly name
114
+ rel_path = PurePath(os.path.relpath(filepath, self.root.parent)) # remove .parent to remove top dirname
115
+ rel_file = PurePath(rel_path.parent, filepath.stem)
116
+
117
+ return Page(
118
+ title = result.title if result.title else filepath.stem,
119
+ path = str(rel_file),
120
+ content = result.text_content,
121
+ editor = "markdown",
122
+ locale = "en",
123
+ tags = None,
124
+ description = f"generated from: {filepath}",
125
+ isPublished = False,
126
+ isPrivate = True,
127
+ )
128
+
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)
140
+
141
+
142
+ def do_convert(src: str, dest: str) -> None:
143
+ MarkitdownConverter().convert_directory(src, dest)
144
+
145
+
146
+ def main() -> None:
147
+ typer.run(do_convert)
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()
@@ -0,0 +1,66 @@
1
+ import os.path
2
+
3
+ from google.auth.transport.requests import Request
4
+ from google.oauth2.credentials import Credentials
5
+ from google_auth_oauthlib.flow import InstalledAppFlow
6
+ from googleapiclient.discovery import build
7
+ from googleapiclient.errors import HttpError
8
+
9
+ # If modifying these scopes, delete the file token.json.
10
+ SCOPES = ["https://www.googleapis.com/auth/drive.metadata.readonly"]
11
+
12
+
13
+ def main():
14
+ """Shows basic usage of the Drive v3 API.
15
+ Prints the names and ids of the first 10 files the user has access to.
16
+ """
17
+ creds = None
18
+ # The file token.json stores the user's access and refresh tokens, and is
19
+ # created automatically when the authorization flow completes for the first
20
+ # time.
21
+ if os.path.exists("token.json"):
22
+ creds = Credentials.from_authorized_user_file("token.json", SCOPES)
23
+ # If there are no (valid) credentials available, let the user log in.
24
+ if not creds or not creds.valid:
25
+ if creds and creds.expired and creds.refresh_token:
26
+ creds.refresh(Request())
27
+ else:
28
+ flow = InstalledAppFlow.from_client_secrets_file(
29
+ "credentials.json", SCOPES
30
+ )
31
+ creds = flow.run_local_server(port=0)
32
+ # Save the credentials for the next run
33
+ with open("token.json", "w") as token:
34
+ token.write(creds.to_json())
35
+
36
+ try:
37
+ service = build("drive", "v3", credentials=creds)
38
+
39
+ # Call the Drive v3 API
40
+ files = service.files()
41
+ request = files.list(pageSize=1, fields="nextPageToken, files(*)")
42
+
43
+ #while request is not None:
44
+ results = request.execute()
45
+
46
+ items = results.get("files", [])
47
+ if not items:
48
+ print("No files found.")
49
+ return
50
+ for item in items:
51
+ #print(json.dumps(item, indent=4))
52
+ print(item['name'], item['mimeType'])
53
+
54
+ # download
55
+ md_doc = files.export(fileId=item['id'], mimeType="text/markdown").execute()
56
+ print(md_doc)
57
+
58
+ # request = files.list_next(request, results)
59
+
60
+ except HttpError as error:
61
+ # TODO(developer) - Handle errors from drive API.
62
+ print(f"An error occurred: {error}")
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()