markdown-to-confluence 0.3.2__py3-none-any.whl → 0.3.4__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.
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/METADATA +32 -8
- markdown_to_confluence-0.3.4.dist-info/RECORD +22 -0
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +40 -14
- md2conf/api.py +135 -50
- md2conf/application.py +89 -160
- md2conf/converter.py +142 -44
- md2conf/emoji.py +3 -1
- md2conf/local.py +132 -0
- md2conf/mermaid.py +1 -1
- md2conf/metadata.py +42 -0
- md2conf/processor.py +159 -88
- md2conf/properties.py +40 -16
- markdown_to_confluence-0.3.2.dist-info/RECORD +0 -20
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/zip-safe +0 -0
md2conf/processor.py
CHANGED
|
@@ -6,98 +6,96 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import hashlib
|
|
10
9
|
import logging
|
|
11
10
|
import os
|
|
11
|
+
from abc import abstractmethod
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Optional
|
|
14
14
|
|
|
15
|
-
from .converter import
|
|
16
|
-
ConfluenceDocument,
|
|
17
|
-
ConfluenceDocumentOptions,
|
|
18
|
-
ConfluencePageMetadata,
|
|
19
|
-
ConfluenceQualifiedID,
|
|
20
|
-
extract_qualified_id,
|
|
21
|
-
)
|
|
15
|
+
from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID
|
|
22
16
|
from .matcher import Matcher, MatcherOptions
|
|
23
|
-
from .
|
|
17
|
+
from .metadata import ConfluencePageMetadata, ConfluenceSiteMetadata
|
|
18
|
+
from .properties import ArgumentError
|
|
24
19
|
|
|
25
20
|
LOGGER = logging.getLogger(__name__)
|
|
26
21
|
|
|
27
22
|
|
|
28
23
|
class Processor:
|
|
24
|
+
"""
|
|
25
|
+
Processes a single Markdown page or a directory of Markdown pages.
|
|
26
|
+
"""
|
|
27
|
+
|
|
29
28
|
options: ConfluenceDocumentOptions
|
|
30
|
-
|
|
29
|
+
site: ConfluenceSiteMetadata
|
|
30
|
+
root_dir: Path
|
|
31
|
+
|
|
32
|
+
page_metadata: dict[Path, ConfluencePageMetadata]
|
|
31
33
|
|
|
32
34
|
def __init__(
|
|
33
|
-
self,
|
|
35
|
+
self,
|
|
36
|
+
options: ConfluenceDocumentOptions,
|
|
37
|
+
site: ConfluenceSiteMetadata,
|
|
38
|
+
root_dir: Path,
|
|
34
39
|
) -> None:
|
|
35
40
|
self.options = options
|
|
36
|
-
self.
|
|
37
|
-
|
|
38
|
-
def process(self, path: Path) -> None:
|
|
39
|
-
"Processes a single Markdown file or a directory of Markdown files."
|
|
41
|
+
self.site = site
|
|
42
|
+
self.root_dir = root_dir
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
if path.is_dir():
|
|
43
|
-
self.process_directory(path)
|
|
44
|
-
elif path.is_file():
|
|
45
|
-
self.process_page(path)
|
|
46
|
-
else:
|
|
47
|
-
raise ValueError(f"expected: valid file or directory path; got: {path}")
|
|
44
|
+
self.page_metadata = {}
|
|
48
45
|
|
|
49
|
-
def process_directory(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
46
|
+
def process_directory(self, local_dir: Path) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Recursively scans a directory hierarchy for Markdown files, and processes each, resolving cross-references.
|
|
49
|
+
"""
|
|
53
50
|
|
|
54
51
|
local_dir = local_dir.resolve(True)
|
|
55
|
-
|
|
56
|
-
root_dir = local_dir
|
|
57
|
-
else:
|
|
58
|
-
root_dir = root_dir.resolve(True)
|
|
59
|
-
|
|
60
|
-
LOGGER.info("Synchronizing directory: %s", local_dir)
|
|
52
|
+
LOGGER.info("Processing directory: %s", local_dir)
|
|
61
53
|
|
|
62
54
|
# Step 1: build index of all page metadata
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
LOGGER.info("Indexed %d page(s)", len(page_metadata))
|
|
55
|
+
self._index_directory(local_dir, self.options.root_page_id)
|
|
56
|
+
LOGGER.info("Indexed %d page(s)", len(self.page_metadata))
|
|
66
57
|
|
|
67
58
|
# Step 2: convert each page
|
|
68
|
-
for page_path in page_metadata.keys():
|
|
69
|
-
self._process_page(page_path
|
|
59
|
+
for page_path in self.page_metadata.keys():
|
|
60
|
+
self._process_page(page_path)
|
|
70
61
|
|
|
71
|
-
def process_page(self, path: Path
|
|
72
|
-
"
|
|
62
|
+
def process_page(self, path: Path) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Processes a single Markdown file.
|
|
65
|
+
"""
|
|
73
66
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
else:
|
|
78
|
-
root_dir = root_dir.resolve(True)
|
|
67
|
+
LOGGER.info("Processing page: %s", path)
|
|
68
|
+
self._index_page(path, self.options.root_page_id)
|
|
69
|
+
self._process_page(path)
|
|
79
70
|
|
|
80
|
-
|
|
71
|
+
def _process_page(self, path: Path) -> None:
|
|
72
|
+
document = ConfluenceDocument.create(
|
|
73
|
+
path, self.options, self.root_dir, self.site, self.page_metadata
|
|
74
|
+
)
|
|
75
|
+
self._save_document(document, path)
|
|
81
76
|
|
|
82
|
-
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def _get_or_create_page(
|
|
83
79
|
self,
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
80
|
+
absolute_path: Path,
|
|
81
|
+
parent_id: Optional[ConfluencePageID],
|
|
82
|
+
*,
|
|
83
|
+
title: Optional[str] = None,
|
|
84
|
+
) -> ConfluencePageMetadata:
|
|
85
|
+
"""
|
|
86
|
+
Creates a new Confluence page if no page is linked in the Markdown document.
|
|
87
|
+
"""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def _save_document(self, document: ConfluenceDocument, path: Path) -> None: ...
|
|
94
92
|
|
|
95
93
|
def _index_directory(
|
|
96
|
-
self,
|
|
97
|
-
local_dir: Path,
|
|
98
|
-
page_metadata: dict[Path, ConfluencePageMetadata],
|
|
94
|
+
self, local_dir: Path, parent_id: Optional[ConfluencePageID]
|
|
99
95
|
) -> None:
|
|
100
|
-
"
|
|
96
|
+
"""
|
|
97
|
+
Indexes Markdown files in a directory hierarchy recursively.
|
|
98
|
+
"""
|
|
101
99
|
|
|
102
100
|
LOGGER.info("Indexing directory: %s", local_dir)
|
|
103
101
|
|
|
@@ -114,34 +112,107 @@ class Processor:
|
|
|
114
112
|
elif entry.is_dir():
|
|
115
113
|
directories.append(Path(local_dir) / entry.name)
|
|
116
114
|
|
|
115
|
+
# make page act as parent node
|
|
116
|
+
parent_doc: Optional[Path] = None
|
|
117
|
+
if (Path(local_dir) / "index.md") in files:
|
|
118
|
+
parent_doc = Path(local_dir) / "index.md"
|
|
119
|
+
elif (Path(local_dir) / "README.md") in files:
|
|
120
|
+
parent_doc = Path(local_dir) / "README.md"
|
|
121
|
+
elif (Path(local_dir) / f"{local_dir.name}.md") in files:
|
|
122
|
+
parent_doc = Path(local_dir) / f"{local_dir.name}.md"
|
|
123
|
+
|
|
124
|
+
if parent_doc is None and self.options.keep_hierarchy:
|
|
125
|
+
parent_doc = Path(local_dir) / "index.md"
|
|
126
|
+
|
|
127
|
+
# create a blank page for directory entry
|
|
128
|
+
with open(parent_doc, "w"):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
if parent_doc is not None:
|
|
132
|
+
if parent_doc in files:
|
|
133
|
+
files.remove(parent_doc)
|
|
134
|
+
|
|
135
|
+
# use latest parent as parent for index page
|
|
136
|
+
metadata = self._get_or_create_page(parent_doc, parent_id)
|
|
137
|
+
LOGGER.debug("Indexed parent %s with metadata: %s", parent_doc, metadata)
|
|
138
|
+
self.page_metadata[parent_doc] = metadata
|
|
139
|
+
|
|
140
|
+
# assign new index page as new parent
|
|
141
|
+
parent_id = ConfluencePageID(metadata.page_id)
|
|
142
|
+
|
|
117
143
|
for doc in files:
|
|
118
|
-
|
|
119
|
-
LOGGER.debug("Indexed %s with metadata: %s", doc, metadata)
|
|
120
|
-
page_metadata[doc] = metadata
|
|
144
|
+
self._index_page(doc, parent_id)
|
|
121
145
|
|
|
122
146
|
for directory in directories:
|
|
123
|
-
self._index_directory(directory,
|
|
124
|
-
|
|
125
|
-
def
|
|
126
|
-
"
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
self._index_directory(directory, parent_id)
|
|
148
|
+
|
|
149
|
+
def _index_page(self, path: Path, parent_id: Optional[ConfluencePageID]) -> None:
|
|
150
|
+
"""
|
|
151
|
+
Indexes a single Markdown file.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
metadata = self._get_or_create_page(path, parent_id)
|
|
155
|
+
LOGGER.debug("Indexed %s with metadata: %s", path, metadata)
|
|
156
|
+
self.page_metadata[path] = metadata
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ProcessorFactory:
|
|
160
|
+
options: ConfluenceDocumentOptions
|
|
161
|
+
site: ConfluenceSiteMetadata
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self, options: ConfluenceDocumentOptions, site: ConfluenceSiteMetadata
|
|
165
|
+
) -> None:
|
|
166
|
+
self.options = options
|
|
167
|
+
self.site = site
|
|
168
|
+
|
|
169
|
+
@abstractmethod
|
|
170
|
+
def create(self, root_dir: Path) -> Processor: ...
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class Converter:
|
|
174
|
+
factory: ProcessorFactory
|
|
175
|
+
|
|
176
|
+
def __init__(self, factory: ProcessorFactory) -> None:
|
|
177
|
+
self.factory = factory
|
|
178
|
+
|
|
179
|
+
def process(self, path: Path) -> None:
|
|
180
|
+
"""
|
|
181
|
+
Processes a single Markdown file or a directory of Markdown files.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
path = path.resolve(True)
|
|
185
|
+
if path.is_dir():
|
|
186
|
+
self.process_directory(path)
|
|
187
|
+
elif path.is_file():
|
|
188
|
+
self.process_page(path)
|
|
189
|
+
else:
|
|
190
|
+
raise ArgumentError(f"expected: valid file or directory path; got: {path}")
|
|
191
|
+
|
|
192
|
+
def process_directory(
|
|
193
|
+
self, local_dir: Path, root_dir: Optional[Path] = None
|
|
194
|
+
) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Recursively scans a directory hierarchy for Markdown files, and processes each, resolving cross-references.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
local_dir = local_dir.resolve(True)
|
|
200
|
+
if root_dir is None:
|
|
201
|
+
root_dir = local_dir
|
|
202
|
+
else:
|
|
203
|
+
root_dir = root_dir.resolve(True)
|
|
204
|
+
|
|
205
|
+
self.factory.create(root_dir).process_directory(local_dir)
|
|
206
|
+
|
|
207
|
+
def process_page(self, path: Path, root_dir: Optional[Path] = None) -> None:
|
|
208
|
+
"""
|
|
209
|
+
Processes a single Markdown file.
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
path = path.resolve(True)
|
|
213
|
+
if root_dir is None:
|
|
214
|
+
root_dir = path.parent
|
|
215
|
+
else:
|
|
216
|
+
root_dir = root_dir.resolve(True)
|
|
217
|
+
|
|
218
|
+
self.factory.create(root_dir).process_page(path)
|
md2conf/properties.py
CHANGED
|
@@ -10,50 +10,74 @@ import os
|
|
|
10
10
|
from typing import Optional
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
class ArgumentError(ValueError):
|
|
14
|
+
"Raised when wrong arguments are passed to a function call."
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PageError(ValueError):
|
|
18
|
+
"Raised in case there is an issue with a Confluence page."
|
|
19
|
+
|
|
20
|
+
|
|
13
21
|
class ConfluenceError(RuntimeError):
|
|
14
|
-
|
|
22
|
+
"Raised when a Confluence API call fails."
|
|
15
23
|
|
|
16
24
|
|
|
17
|
-
class
|
|
25
|
+
class ConfluenceSiteProperties:
|
|
18
26
|
domain: str
|
|
19
27
|
base_path: str
|
|
20
28
|
space_key: Optional[str]
|
|
21
|
-
user_name: Optional[str]
|
|
22
|
-
api_key: str
|
|
23
|
-
headers: Optional[dict[str, str]]
|
|
24
29
|
|
|
25
30
|
def __init__(
|
|
26
31
|
self,
|
|
27
32
|
domain: Optional[str] = None,
|
|
28
33
|
base_path: Optional[str] = None,
|
|
29
|
-
user_name: Optional[str] = None,
|
|
30
|
-
api_key: Optional[str] = None,
|
|
31
34
|
space_key: Optional[str] = None,
|
|
32
|
-
headers: Optional[dict[str, str]] = None,
|
|
33
35
|
) -> None:
|
|
34
36
|
opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
|
|
35
37
|
opt_base_path = base_path or os.getenv("CONFLUENCE_PATH")
|
|
36
|
-
opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
|
|
37
|
-
opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
|
|
38
38
|
opt_space_key = space_key or os.getenv("CONFLUENCE_SPACE_KEY")
|
|
39
39
|
|
|
40
40
|
if not opt_domain:
|
|
41
|
-
raise
|
|
41
|
+
raise ArgumentError("Confluence domain not specified")
|
|
42
42
|
if not opt_base_path:
|
|
43
43
|
opt_base_path = "/wiki/"
|
|
44
|
-
if not opt_api_key:
|
|
45
|
-
raise ConfluenceError("Confluence API key not specified")
|
|
46
44
|
|
|
47
45
|
if opt_domain.startswith(("http://", "https://")) or opt_domain.endswith("/"):
|
|
48
|
-
raise
|
|
46
|
+
raise ArgumentError(
|
|
49
47
|
"Confluence domain looks like a URL; only host name required"
|
|
50
48
|
)
|
|
51
49
|
if not opt_base_path.startswith("/") or not opt_base_path.endswith("/"):
|
|
52
|
-
raise
|
|
50
|
+
raise ArgumentError("Confluence base path must start and end with a '/'")
|
|
53
51
|
|
|
54
52
|
self.domain = opt_domain
|
|
55
53
|
self.base_path = opt_base_path
|
|
54
|
+
self.space_key = opt_space_key
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ConfluenceConnectionProperties(ConfluenceSiteProperties):
|
|
58
|
+
"Properties related to connecting to Confluence."
|
|
59
|
+
|
|
60
|
+
user_name: Optional[str]
|
|
61
|
+
api_key: str
|
|
62
|
+
headers: Optional[dict[str, str]]
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
domain: Optional[str] = None,
|
|
67
|
+
base_path: Optional[str] = None,
|
|
68
|
+
user_name: Optional[str] = None,
|
|
69
|
+
api_key: Optional[str] = None,
|
|
70
|
+
space_key: Optional[str] = None,
|
|
71
|
+
headers: Optional[dict[str, str]] = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
super().__init__(domain, base_path, space_key)
|
|
74
|
+
|
|
75
|
+
opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
|
|
76
|
+
opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
|
|
77
|
+
|
|
78
|
+
if not opt_api_key:
|
|
79
|
+
raise ArgumentError("Confluence API key not specified")
|
|
80
|
+
|
|
56
81
|
self.user_name = opt_user_name
|
|
57
82
|
self.api_key = opt_api_key
|
|
58
|
-
self.space_key = opt_space_key
|
|
59
83
|
self.headers = headers
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
markdown_to_confluence-0.3.2.dist-info/licenses/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
2
|
-
md2conf/__init__.py,sha256=IyztAgNkEXTxDlsTrxZOAkeNIzLSSzjNV0ZHg2SvDDE,402
|
|
3
|
-
md2conf/__main__.py,sha256=ypjV_5mE0smlIRBFrpikgzXq18as2hY43HJxMLpzGp4,7145
|
|
4
|
-
md2conf/api.py,sha256=-bneMbfpspfat4J63Z7bY0kdlW8yJguRRNbx6CaOkGY,20101
|
|
5
|
-
md2conf/application.py,sha256=5K-nCPHJZfIahjubrLtXTwI-zsTiD140fdYXDnh3GSk,9161
|
|
6
|
-
md2conf/converter.py,sha256=MoGbXqh5rE4qkdxxY8RHcnoZ5mz0aEuFz9nmUnt0WdM,36397
|
|
7
|
-
md2conf/emoji.py,sha256=IZeguWqcboeOyJkGLTVONDMO4ZXfYXPgfkp56PTI-hE,1924
|
|
8
|
-
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
9
|
-
md2conf/matcher.py,sha256=FgMFPvGiOqGezCs8OyerfsVo-iIHFoI6LRMzdcjM5UY,3693
|
|
10
|
-
md2conf/mermaid.py,sha256=Alzkv0BY-lju4ojtBdW2qtCLZ59MO9kaS2RpQO6Kyfk,2304
|
|
11
|
-
md2conf/processor.py,sha256=G-MIh1jGq9jjgogHnlnRUSrNgiV6_xO6Fy7ct9alqgM,4769
|
|
12
|
-
md2conf/properties.py,sha256=WaVVOYSck7drVQfcBJmBMa7Mb0KVOZl9UZHvLS1Du8U,1892
|
|
13
|
-
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
14
|
-
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
markdown_to_confluence-0.3.2.dist-info/METADATA,sha256=2exvsuRCcD3L9CN7fl0gqki1gsqQgHDxGyjnKhDeYmw,14958
|
|
16
|
-
markdown_to_confluence-0.3.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
17
|
-
markdown_to_confluence-0.3.2.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
18
|
-
markdown_to_confluence-0.3.2.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
19
|
-
markdown_to_confluence-0.3.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
20
|
-
markdown_to_confluence-0.3.2.dist-info/RECORD,,
|
{markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|