wikinator 0.1.0__py3-none-any.whl
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.
- wikinator/__about__.py +5 -0
- wikinator/__init__.py +0 -0
- wikinator/__main__.py +6 -0
- wikinator/app.py +151 -0
- wikinator/quickstart.py +66 -0
- wikinator/wiki.py +281 -0
- wikinator-0.1.0.dist-info/METADATA +116 -0
- wikinator-0.1.0.dist-info/RECORD +11 -0
- wikinator-0.1.0.dist-info/WHEEL +4 -0
- wikinator-0.1.0.dist-info/entry_points.txt +2 -0
- wikinator-0.1.0.dist-info/licenses/LICENSE +21 -0
wikinator/__about__.py
ADDED
wikinator/__init__.py
ADDED
|
File without changes
|
wikinator/__main__.py
ADDED
wikinator/app.py
ADDED
|
@@ -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()
|
wikinator/quickstart.py
ADDED
|
@@ -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()
|
wikinator/wiki.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from gql import Client, gql
|
|
7
|
+
from gql.transport.aiohttp import AIOHTTPTransport
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def init_client() -> Client:
|
|
12
|
+
"""
|
|
13
|
+
Initialize the GraphQL client with the credentials found in the system ENV:
|
|
14
|
+
- GRAPH_DB : The full URL for requests to the graph DB
|
|
15
|
+
- AUTH_TOKEN : Security token to authorize session
|
|
16
|
+
"""
|
|
17
|
+
load_dotenv()
|
|
18
|
+
db_url = os.getenv("GRAPH_DB")
|
|
19
|
+
token = os.getenv("AUTH_TOKEN")
|
|
20
|
+
transport = AIOHTTPTransport(url=db_url, headers={'Authorization': f'Bearer {token}'}, ssl=True)
|
|
21
|
+
return Client(transport=transport)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def store_page(client: Client, params: dict):
|
|
25
|
+
# store in file?
|
|
26
|
+
query = gql(
|
|
27
|
+
'''
|
|
28
|
+
mutation Page (
|
|
29
|
+
$content: String!,
|
|
30
|
+
$description: String!,
|
|
31
|
+
$editor:String!,
|
|
32
|
+
$isPublished:Boolean!,
|
|
33
|
+
$isPrivate:Boolean!,
|
|
34
|
+
$locale:String!,
|
|
35
|
+
$path:String!,
|
|
36
|
+
$tags:[String]!,
|
|
37
|
+
$title:String!) {
|
|
38
|
+
pages {
|
|
39
|
+
create (
|
|
40
|
+
content:$content,
|
|
41
|
+
description:$description,
|
|
42
|
+
editor: $editor,
|
|
43
|
+
isPublished: $isPublished,
|
|
44
|
+
isPrivate: $isPrivate,
|
|
45
|
+
locale: $locale,
|
|
46
|
+
path:$path,
|
|
47
|
+
tags: $tags,
|
|
48
|
+
title:$title
|
|
49
|
+
) {
|
|
50
|
+
responseResult {
|
|
51
|
+
succeeded
|
|
52
|
+
errorCode
|
|
53
|
+
slug
|
|
54
|
+
message
|
|
55
|
+
}
|
|
56
|
+
page {
|
|
57
|
+
id
|
|
58
|
+
path
|
|
59
|
+
title
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
'''
|
|
65
|
+
)
|
|
66
|
+
print('---', params['path'])
|
|
67
|
+
result = client.execute(query, variable_values=params)
|
|
68
|
+
print(result)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
# EXTS = {
|
|
72
|
+
# ".docx":
|
|
73
|
+
# 32
|
|
74
|
+
# .pdf 87
|
|
75
|
+
# .xlsx 15
|
|
76
|
+
# .rsc 4
|
|
77
|
+
# .drawio 4
|
|
78
|
+
# .deb 8
|
|
79
|
+
# .bin 1
|
|
80
|
+
# .mp4 28
|
|
81
|
+
# .png 49
|
|
82
|
+
# .PDF 7
|
|
83
|
+
# .pptx 1
|
|
84
|
+
# .apk 10
|
|
85
|
+
# .jpg 5
|
|
86
|
+
# .HEIC 21
|
|
87
|
+
# .tgz 1
|
|
88
|
+
# .php 2
|
|
89
|
+
# .yaml 2
|
|
90
|
+
# .gz 1
|
|
91
|
+
# .toml 1
|
|
92
|
+
# .service 1
|
|
93
|
+
# .jpeg 2
|
|
94
|
+
# .txt 1
|
|
95
|
+
# .csv 1
|
|
96
|
+
# }
|
|
97
|
+
|
|
98
|
+
### Design Notes
|
|
99
|
+
# multiprocessing.JoinableQueue for queues, using both get() and task_done()
|
|
100
|
+
|
|
101
|
+
#running = False
|
|
102
|
+
#docx_queue = multiprocessing.JoinableQueue()
|
|
103
|
+
|
|
104
|
+
# def docx_worker(client: Client):
|
|
105
|
+
# while running:
|
|
106
|
+
# try:
|
|
107
|
+
# item = docx_queue.get(timeout=0.01)
|
|
108
|
+
# if item is None:
|
|
109
|
+
# continue
|
|
110
|
+
|
|
111
|
+
# try:
|
|
112
|
+
# process_docx(client, item)
|
|
113
|
+
# finally:
|
|
114
|
+
# docx_queue.task_done()
|
|
115
|
+
|
|
116
|
+
# except queue.Empty:
|
|
117
|
+
# pass
|
|
118
|
+
# except:
|
|
119
|
+
# logging.exception('error while processing item')
|
|
120
|
+
|
|
121
|
+
CLIENT: Client = init_client()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
#def main():
|
|
126
|
+
#CLIENT = init_client()
|
|
127
|
+
#process_directory("./data")
|
|
128
|
+
|
|
129
|
+
# client = init_client()
|
|
130
|
+
|
|
131
|
+
# with open("sample.adoc", 'r') as file:
|
|
132
|
+
# file_content = file.read()
|
|
133
|
+
|
|
134
|
+
# params = {
|
|
135
|
+
# "title": "This is a test",
|
|
136
|
+
# "path": "test/test-43",
|
|
137
|
+
# "content": file_content,
|
|
138
|
+
# "editor": "asciidoc",
|
|
139
|
+
# "locale": "en",
|
|
140
|
+
# "tags": "",
|
|
141
|
+
# "description": "generated from googledoc/...",
|
|
142
|
+
# "isPublished": False,
|
|
143
|
+
# "isPrivate": True,
|
|
144
|
+
# }
|
|
145
|
+
|
|
146
|
+
# page = create_page(client, params)
|
|
147
|
+
# print(json.dumps(page, indent=4))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
### other queries. remove when stable
|
|
156
|
+
# query = gql(
|
|
157
|
+
# """
|
|
158
|
+
# {
|
|
159
|
+
# pages {
|
|
160
|
+
# list (orderBy: TITLE) {
|
|
161
|
+
# id
|
|
162
|
+
# path
|
|
163
|
+
# title
|
|
164
|
+
# contentType
|
|
165
|
+
# createdAt
|
|
166
|
+
# description
|
|
167
|
+
# isPrivate
|
|
168
|
+
# isPublished
|
|
169
|
+
# updatedAt
|
|
170
|
+
# }
|
|
171
|
+
# }
|
|
172
|
+
# }
|
|
173
|
+
# """
|
|
174
|
+
# )
|
|
175
|
+
|
|
176
|
+
# query = gql(
|
|
177
|
+
# """
|
|
178
|
+
# {
|
|
179
|
+
# users {
|
|
180
|
+
# list {
|
|
181
|
+
# id
|
|
182
|
+
# name
|
|
183
|
+
# email
|
|
184
|
+
# }
|
|
185
|
+
# }
|
|
186
|
+
# }
|
|
187
|
+
# """
|
|
188
|
+
# )
|
|
189
|
+
|
|
190
|
+
# insert into "pages" (
|
|
191
|
+
# "authorId",
|
|
192
|
+
# "content",
|
|
193
|
+
# "contentType",
|
|
194
|
+
# "createdAt",
|
|
195
|
+
# "creatorId",
|
|
196
|
+
# "description",
|
|
197
|
+
# "editorKey",
|
|
198
|
+
# "extra",
|
|
199
|
+
# "hash",
|
|
200
|
+
# "isPrivate",
|
|
201
|
+
# "isPublished",
|
|
202
|
+
# "localeCode",
|
|
203
|
+
# "path",
|
|
204
|
+
# "publishEndDate",
|
|
205
|
+
# "publishStartDate",
|
|
206
|
+
# "title",
|
|
207
|
+
# "toc",
|
|
208
|
+
# "updatedAt")
|
|
209
|
+
|
|
210
|
+
# query = gql(
|
|
211
|
+
# """
|
|
212
|
+
# mutation Page ($content: String!, $description: String!, $editor:String!, $isPublished:Boolean!, $isPrivate:Boolean!, $locale:String!, $path:String!,$tags:[String]!, $title:String!) {
|
|
213
|
+
# pages {
|
|
214
|
+
# create (content:$content, description:$description, editor: $editor, isPublished: $isPublished, isPrivate: $isPrivate, locale: $locale, path:$path, tags: $tags, title:$title) {
|
|
215
|
+
# responseResult {
|
|
216
|
+
# succeeded,
|
|
217
|
+
# errorCode,
|
|
218
|
+
# slug,
|
|
219
|
+
# message
|
|
220
|
+
# },
|
|
221
|
+
# page {
|
|
222
|
+
# id,
|
|
223
|
+
# path,
|
|
224
|
+
# title
|
|
225
|
+
# }
|
|
226
|
+
# }
|
|
227
|
+
# }
|
|
228
|
+
# }
|
|
229
|
+
# """
|
|
230
|
+
# )
|
|
231
|
+
|
|
232
|
+
# query = gql(
|
|
233
|
+
# '''
|
|
234
|
+
# mutation Page (
|
|
235
|
+
# $content: String!,
|
|
236
|
+
# $description: String!,
|
|
237
|
+
# $editor:String!,
|
|
238
|
+
# $isPublished:Boolean!,
|
|
239
|
+
# $isPrivate:Boolean!,
|
|
240
|
+
# $locale:String!,
|
|
241
|
+
# $path:String!,
|
|
242
|
+
# $tags:[String]!,
|
|
243
|
+
# $title:String!) {
|
|
244
|
+
# pages {
|
|
245
|
+
# create (
|
|
246
|
+
# content:$content,
|
|
247
|
+
# description:$description,
|
|
248
|
+
# editor: $editor,
|
|
249
|
+
# isPublished: $isPublished,
|
|
250
|
+
# isPrivate: $isPrivate,
|
|
251
|
+
# locale: $locale,
|
|
252
|
+
# path:$path,
|
|
253
|
+
# tags: $tags,
|
|
254
|
+
# title:$title
|
|
255
|
+
# ) {
|
|
256
|
+
# responseResult {
|
|
257
|
+
# succeeded
|
|
258
|
+
# errorCode
|
|
259
|
+
# slug
|
|
260
|
+
# message
|
|
261
|
+
# }
|
|
262
|
+
# page {
|
|
263
|
+
# id
|
|
264
|
+
# path
|
|
265
|
+
# title
|
|
266
|
+
# }
|
|
267
|
+
# }
|
|
268
|
+
# }
|
|
269
|
+
# }
|
|
270
|
+
# '''
|
|
271
|
+
# )
|
|
272
|
+
|
|
273
|
+
# query = gql(
|
|
274
|
+
# """
|
|
275
|
+
# query getContinentName ($code: ID!) {
|
|
276
|
+
# continent (code: $code) {
|
|
277
|
+
# name
|
|
278
|
+
# }
|
|
279
|
+
# }
|
|
280
|
+
# """
|
|
281
|
+
# )
|
|
@@ -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,11 @@
|
|
|
1
|
+
wikinator/__about__.py,sha256=IdVXYY7Z3ydcf0n-hgfyqAau2-a22qBsSNMM5FZtHes,154
|
|
2
|
+
wikinator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
wikinator/__main__.py,sha256=TRhJB-hjf9CD9mFn4TlXG2qPRpVNZ_wArjpvm6jiZdQ,93
|
|
4
|
+
wikinator/app.py,sha256=JzHYmgLLWoTJCBsw-u71qLzJZTS7nTZo8FjcfwvSnCU,4628
|
|
5
|
+
wikinator/quickstart.py,sha256=FHx_b1mPA8H2-5Pw0FbnOP6UCmARAs0DsxHJ6y3BmFc,2255
|
|
6
|
+
wikinator/wiki.py,sha256=bdM9nGtylfywlWkGD1dAqTdVnPEQPwR20Y62wM6yPVM,6905
|
|
7
|
+
wikinator-0.1.0.dist-info/METADATA,sha256=xrVcgY4sH8-8gs2LbUBqy9TyxsQcMm_RaB9be40_0Ng,4059
|
|
8
|
+
wikinator-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
wikinator-0.1.0.dist-info/entry_points.txt,sha256=6NGOQQ5sXSESmFtd4j0Dy_Yv6fcNwE-NUxPp10xG4cI,49
|
|
10
|
+
wikinator-0.1.0.dist-info/licenses/LICENSE,sha256=Mmlntf1TLxyV5T4YFPW_x1oKKyb9-OC2pT4dJHC4ebQ,1069
|
|
11
|
+
wikinator-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|