markdown-to-confluence 0.3.3__tar.gz → 0.3.4__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.
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/PKG-INFO +10 -3
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/README.md +9 -2
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/markdown_to_confluence.egg-info/PKG-INFO +10 -3
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/markdown_to_confluence.egg-info/SOURCES.txt +2 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/md2conf/__init__.py +1 -1
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/md2conf/__main__.py +6 -5
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/md2conf/api.py +104 -33
- markdown_to_confluence-0.3.4/md2conf/application.py +208 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/md2conf/converter.py +33 -24
- markdown_to_confluence-0.3.4/md2conf/local.py +132 -0
- markdown_to_confluence-0.3.4/md2conf/metadata.py +42 -0
- markdown_to_confluence-0.3.4/md2conf/processor.py +218 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/tests/test_conversion.py +1 -1
- markdown_to_confluence-0.3.4/tests/test_processor.py +104 -0
- markdown_to_confluence-0.3.3/md2conf/application.py +0 -295
- markdown_to_confluence-0.3.3/md2conf/processor.py +0 -148
- markdown_to_confluence-0.3.3/tests/test_processor.py +0 -66
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/LICENSE +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/markdown_to_confluence.egg-info/dependency_links.txt +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/markdown_to_confluence.egg-info/entry_points.txt +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/markdown_to_confluence.egg-info/requires.txt +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/markdown_to_confluence.egg-info/top_level.txt +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/markdown_to_confluence.egg-info/zip-safe +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/md2conf/emoji.py +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/md2conf/entities.dtd +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/md2conf/matcher.py +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/md2conf/mermaid.py +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/md2conf/properties.py +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/md2conf/puppeteer-config.json +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/md2conf/py.typed +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/pyproject.toml +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/setup.cfg +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/setup.py +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/tests/test_matcher.py +0 -0
- {markdown_to_confluence-0.3.3 → markdown_to_confluence-0.3.4}/tests/test_mermaid.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
5
|
Home-page: https://github.com/hunyadi/md2conf
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -62,13 +62,13 @@ Whenever possible, the implementation uses [Confluence REST API v2](https://deve
|
|
|
62
62
|
|
|
63
63
|
## Installation
|
|
64
64
|
|
|
65
|
-
Install the core package from PyPI:
|
|
65
|
+
**Required.** Install the core package from [PyPI](https://pypi.org/project/markdown-to-confluence/):
|
|
66
66
|
|
|
67
67
|
```sh
|
|
68
68
|
pip install markdown-to-confluence
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Converting code blocks of Mermaid diagrams into Confluence image attachments requires [mermaid-cli](https://github.com/mermaid-js/mermaid-cli):
|
|
71
|
+
**Optional.** Converting code blocks of Mermaid diagrams into Confluence image attachments requires [mermaid-cli](https://github.com/mermaid-js/mermaid-cli):
|
|
72
72
|
|
|
73
73
|
```sh
|
|
74
74
|
npm install -g @mermaid-js/mermaid-cli
|
|
@@ -222,6 +222,13 @@ Files that don't have the extension `*.md` are skipped automatically. Hidden dir
|
|
|
222
222
|
|
|
223
223
|
If a matching Confluence page already exists for a Markdown file, the page title in Confluence is left unchanged.
|
|
224
224
|
|
|
225
|
+
### Converting diagrams
|
|
226
|
+
|
|
227
|
+
You can include [Mermaid diagrams](https://mermaid.js.org/) in your Markdown documents to create visual representations of systems, processes, and relationships. When a Markdown document contains a code block with the language specifier `mermaid`, *md2conf* offers two options to publish the diagram:
|
|
228
|
+
|
|
229
|
+
1. Pre-render into an image. The code block is interpreted by and converted into a PNG or SVG image with the Mermaid diagram utility [mermaid-cli](https://github.com/mermaid-js/mermaid-cli). The generated image is then uploaded to Confluence as an attachment to the page. This is the approach we use and support.
|
|
230
|
+
2. Render on demand. The code block is transformed into a [diagram macro](https://atlasauthority.atlassian.net/wiki/spaces/MARKDOWNCLOUD/pages/2946826241/Diagram+Macro), which is processed by Confluence. You need a [Confluence plugin](https://marketplace.atlassian.com/apps/1211438/markdown-html-plantuml-latex-diagrams-open-api-mermaid) to turn macro definitions into images when a Confluence page is visited. This is a contributed feature. As authors of *md2conf*, we don't endorse or support any particular Confluence plugin.
|
|
231
|
+
|
|
225
232
|
### Running the tool
|
|
226
233
|
|
|
227
234
|
You execute the command-line tool `md2conf` to synchronize the Markdown file with Confluence:
|
|
@@ -28,13 +28,13 @@ Whenever possible, the implementation uses [Confluence REST API v2](https://deve
|
|
|
28
28
|
|
|
29
29
|
## Installation
|
|
30
30
|
|
|
31
|
-
Install the core package from PyPI:
|
|
31
|
+
**Required.** Install the core package from [PyPI](https://pypi.org/project/markdown-to-confluence/):
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
34
|
pip install markdown-to-confluence
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Converting code blocks of Mermaid diagrams into Confluence image attachments requires [mermaid-cli](https://github.com/mermaid-js/mermaid-cli):
|
|
37
|
+
**Optional.** Converting code blocks of Mermaid diagrams into Confluence image attachments requires [mermaid-cli](https://github.com/mermaid-js/mermaid-cli):
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
40
|
npm install -g @mermaid-js/mermaid-cli
|
|
@@ -188,6 +188,13 @@ Files that don't have the extension `*.md` are skipped automatically. Hidden dir
|
|
|
188
188
|
|
|
189
189
|
If a matching Confluence page already exists for a Markdown file, the page title in Confluence is left unchanged.
|
|
190
190
|
|
|
191
|
+
### Converting diagrams
|
|
192
|
+
|
|
193
|
+
You can include [Mermaid diagrams](https://mermaid.js.org/) in your Markdown documents to create visual representations of systems, processes, and relationships. When a Markdown document contains a code block with the language specifier `mermaid`, *md2conf* offers two options to publish the diagram:
|
|
194
|
+
|
|
195
|
+
1. Pre-render into an image. The code block is interpreted by and converted into a PNG or SVG image with the Mermaid diagram utility [mermaid-cli](https://github.com/mermaid-js/mermaid-cli). The generated image is then uploaded to Confluence as an attachment to the page. This is the approach we use and support.
|
|
196
|
+
2. Render on demand. The code block is transformed into a [diagram macro](https://atlasauthority.atlassian.net/wiki/spaces/MARKDOWNCLOUD/pages/2946826241/Diagram+Macro), which is processed by Confluence. You need a [Confluence plugin](https://marketplace.atlassian.com/apps/1211438/markdown-html-plantuml-latex-diagrams-open-api-mermaid) to turn macro definitions into images when a Confluence page is visited. This is a contributed feature. As authors of *md2conf*, we don't endorse or support any particular Confluence plugin.
|
|
197
|
+
|
|
191
198
|
### Running the tool
|
|
192
199
|
|
|
193
200
|
You execute the command-line tool `md2conf` to synchronize the Markdown file with Confluence:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
5
|
Home-page: https://github.com/hunyadi/md2conf
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -62,13 +62,13 @@ Whenever possible, the implementation uses [Confluence REST API v2](https://deve
|
|
|
62
62
|
|
|
63
63
|
## Installation
|
|
64
64
|
|
|
65
|
-
Install the core package from PyPI:
|
|
65
|
+
**Required.** Install the core package from [PyPI](https://pypi.org/project/markdown-to-confluence/):
|
|
66
66
|
|
|
67
67
|
```sh
|
|
68
68
|
pip install markdown-to-confluence
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Converting code blocks of Mermaid diagrams into Confluence image attachments requires [mermaid-cli](https://github.com/mermaid-js/mermaid-cli):
|
|
71
|
+
**Optional.** Converting code blocks of Mermaid diagrams into Confluence image attachments requires [mermaid-cli](https://github.com/mermaid-js/mermaid-cli):
|
|
72
72
|
|
|
73
73
|
```sh
|
|
74
74
|
npm install -g @mermaid-js/mermaid-cli
|
|
@@ -222,6 +222,13 @@ Files that don't have the extension `*.md` are skipped automatically. Hidden dir
|
|
|
222
222
|
|
|
223
223
|
If a matching Confluence page already exists for a Markdown file, the page title in Confluence is left unchanged.
|
|
224
224
|
|
|
225
|
+
### Converting diagrams
|
|
226
|
+
|
|
227
|
+
You can include [Mermaid diagrams](https://mermaid.js.org/) in your Markdown documents to create visual representations of systems, processes, and relationships. When a Markdown document contains a code block with the language specifier `mermaid`, *md2conf* offers two options to publish the diagram:
|
|
228
|
+
|
|
229
|
+
1. Pre-render into an image. The code block is interpreted by and converted into a PNG or SVG image with the Mermaid diagram utility [mermaid-cli](https://github.com/mermaid-js/mermaid-cli). The generated image is then uploaded to Confluence as an attachment to the page. This is the approach we use and support.
|
|
230
|
+
2. Render on demand. The code block is transformed into a [diagram macro](https://atlasauthority.atlassian.net/wiki/spaces/MARKDOWNCLOUD/pages/2946826241/Diagram+Macro), which is processed by Confluence. You need a [Confluence plugin](https://marketplace.atlassian.com/apps/1211438/markdown-html-plantuml-latex-diagrams-open-api-mermaid) to turn macro definitions into images when a Confluence page is visited. This is a contributed feature. As authors of *md2conf*, we don't endorse or support any particular Confluence plugin.
|
|
231
|
+
|
|
225
232
|
### Running the tool
|
|
226
233
|
|
|
227
234
|
You execute the command-line tool `md2conf` to synchronize the Markdown file with Confluence:
|
|
@@ -17,8 +17,10 @@ md2conf/application.py
|
|
|
17
17
|
md2conf/converter.py
|
|
18
18
|
md2conf/emoji.py
|
|
19
19
|
md2conf/entities.dtd
|
|
20
|
+
md2conf/local.py
|
|
20
21
|
md2conf/matcher.py
|
|
21
22
|
md2conf/mermaid.py
|
|
23
|
+
md2conf/metadata.py
|
|
22
24
|
md2conf/processor.py
|
|
23
25
|
md2conf/properties.py
|
|
24
26
|
md2conf/puppeteer-config.json
|
|
@@ -5,7 +5,7 @@ Parses Markdown files, converts Markdown content into the Confluence Storage For
|
|
|
5
5
|
Confluence API endpoints to upload images and content.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "0.3.
|
|
8
|
+
__version__ = "0.3.4"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
10
|
__copyright__ = "Copyright 2022-2025, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
|
@@ -22,8 +22,9 @@ import requests
|
|
|
22
22
|
from . import __version__
|
|
23
23
|
from .api import ConfluenceAPI
|
|
24
24
|
from .application import Application
|
|
25
|
-
from .converter import ConfluenceDocumentOptions,
|
|
26
|
-
from .
|
|
25
|
+
from .converter import ConfluenceDocumentOptions, ConfluencePageID
|
|
26
|
+
from .local import LocalConverter
|
|
27
|
+
from .metadata import ConfluenceSiteMetadata
|
|
27
28
|
from .properties import (
|
|
28
29
|
ArgumentError,
|
|
29
30
|
ConfluenceConnectionProperties,
|
|
@@ -199,7 +200,7 @@ def main() -> None:
|
|
|
199
200
|
heading_anchors=args.heading_anchors,
|
|
200
201
|
ignore_invalid_url=args.ignore_invalid_url,
|
|
201
202
|
generated_by=args.generated_by,
|
|
202
|
-
root_page_id=args.root_page,
|
|
203
|
+
root_page_id=ConfluencePageID(args.root_page) if args.root_page else None,
|
|
203
204
|
keep_hierarchy=args.keep_hierarchy,
|
|
204
205
|
render_mermaid=args.render_mermaid,
|
|
205
206
|
diagram_output_format=args.diagram_output_format,
|
|
@@ -219,7 +220,7 @@ def main() -> None:
|
|
|
219
220
|
base_path=site_properties.base_path,
|
|
220
221
|
space_key=site_properties.space_key,
|
|
221
222
|
)
|
|
222
|
-
|
|
223
|
+
LocalConverter(options, site_metadata).process(args.mdpath)
|
|
223
224
|
else:
|
|
224
225
|
try:
|
|
225
226
|
properties = ConfluenceConnectionProperties(
|
|
@@ -237,7 +238,7 @@ def main() -> None:
|
|
|
237
238
|
Application(
|
|
238
239
|
api,
|
|
239
240
|
options,
|
|
240
|
-
).
|
|
241
|
+
).process(args.mdpath)
|
|
241
242
|
except requests.exceptions.HTTPError as err:
|
|
242
243
|
logging.error(err)
|
|
243
244
|
|
|
@@ -7,6 +7,7 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import enum
|
|
10
|
+
import functools
|
|
10
11
|
import io
|
|
11
12
|
import json
|
|
12
13
|
import logging
|
|
@@ -21,6 +22,7 @@ from urllib.parse import urlencode, urlparse, urlunparse
|
|
|
21
22
|
import requests
|
|
22
23
|
|
|
23
24
|
from .converter import ParseError, sanitize_confluence
|
|
25
|
+
from .metadata import ConfluenceSiteMetadata
|
|
24
26
|
from .properties import (
|
|
25
27
|
ArgumentError,
|
|
26
28
|
ConfluenceConnectionProperties,
|
|
@@ -76,7 +78,7 @@ def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
|
|
|
76
78
|
LOGGER = logging.getLogger(__name__)
|
|
77
79
|
|
|
78
80
|
|
|
79
|
-
@dataclass
|
|
81
|
+
@dataclass(frozen=True)
|
|
80
82
|
class ConfluenceAttachment:
|
|
81
83
|
id: str
|
|
82
84
|
media_type: str
|
|
@@ -84,14 +86,18 @@ class ConfluenceAttachment:
|
|
|
84
86
|
comment: str
|
|
85
87
|
|
|
86
88
|
|
|
87
|
-
@dataclass
|
|
88
|
-
class
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class ConfluencePageMetadata:
|
|
89
91
|
id: str
|
|
90
92
|
space_id: str
|
|
91
93
|
parent_id: str
|
|
92
94
|
parent_type: Optional[ConfluencePageParentContentType]
|
|
93
95
|
title: str
|
|
94
96
|
version: int
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class ConfluencePage(ConfluencePageMetadata):
|
|
95
101
|
content: str
|
|
96
102
|
|
|
97
103
|
|
|
@@ -136,10 +142,12 @@ class ConfluenceAPI:
|
|
|
136
142
|
|
|
137
143
|
|
|
138
144
|
class ConfluenceSession:
|
|
145
|
+
"""
|
|
146
|
+
Information about an open session to a Confluence server.
|
|
147
|
+
"""
|
|
148
|
+
|
|
139
149
|
session: requests.Session
|
|
140
|
-
|
|
141
|
-
base_path: str
|
|
142
|
-
space_key: Optional[str]
|
|
150
|
+
site: ConfluenceSiteMetadata
|
|
143
151
|
|
|
144
152
|
_space_id_to_key: dict[str, str]
|
|
145
153
|
_space_key_to_id: dict[str, str]
|
|
@@ -152,9 +160,7 @@ class ConfluenceSession:
|
|
|
152
160
|
space_key: Optional[str] = None,
|
|
153
161
|
) -> None:
|
|
154
162
|
self.session = session
|
|
155
|
-
self.
|
|
156
|
-
self.base_path = base_path
|
|
157
|
-
self.space_key = space_key
|
|
163
|
+
self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
|
|
158
164
|
|
|
159
165
|
self._space_id_to_key = {}
|
|
160
166
|
self._space_key_to_id = {}
|
|
@@ -178,7 +184,9 @@ class ConfluenceSession:
|
|
|
178
184
|
:returns: A full URL.
|
|
179
185
|
"""
|
|
180
186
|
|
|
181
|
-
base_url =
|
|
187
|
+
base_url = (
|
|
188
|
+
f"https://{self.site.domain}{self.site.base_path}{version.value}{path}"
|
|
189
|
+
)
|
|
182
190
|
return build_url(base_url, query)
|
|
183
191
|
|
|
184
192
|
def _invoke(
|
|
@@ -251,6 +259,29 @@ class ConfluenceSession:
|
|
|
251
259
|
|
|
252
260
|
return id
|
|
253
261
|
|
|
262
|
+
def get_space_id(
|
|
263
|
+
self, *, space_id: Optional[str] = None, space_key: Optional[str] = None
|
|
264
|
+
) -> Optional[str]:
|
|
265
|
+
"""
|
|
266
|
+
Coalesce a space ID or space key into a space ID, accounting for site default.
|
|
267
|
+
|
|
268
|
+
:param space_id: A Confluence space ID.
|
|
269
|
+
:param space_key: A Confluence space key.
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
if space_id is not None and space_key is not None:
|
|
273
|
+
raise ConfluenceError("either space ID or space key is required; not both")
|
|
274
|
+
|
|
275
|
+
if space_id is not None:
|
|
276
|
+
return space_id
|
|
277
|
+
|
|
278
|
+
space_key = space_key or self.site.space_key
|
|
279
|
+
if space_key is not None:
|
|
280
|
+
return self.space_key_to_id(space_key)
|
|
281
|
+
|
|
282
|
+
# space ID and key are unset, and no default space is configured
|
|
283
|
+
return None
|
|
284
|
+
|
|
254
285
|
def get_attachment_by_name(
|
|
255
286
|
self, page_id: str, filename: str
|
|
256
287
|
) -> ConfluenceAttachment:
|
|
@@ -393,6 +424,7 @@ class ConfluenceSession:
|
|
|
393
424
|
self,
|
|
394
425
|
title: str,
|
|
395
426
|
*,
|
|
427
|
+
space_id: Optional[str] = None,
|
|
396
428
|
space_key: Optional[str] = None,
|
|
397
429
|
) -> str:
|
|
398
430
|
"""
|
|
@@ -408,9 +440,9 @@ class ConfluenceSession:
|
|
|
408
440
|
query = {
|
|
409
441
|
"title": title,
|
|
410
442
|
}
|
|
411
|
-
|
|
412
|
-
if
|
|
413
|
-
query["space-id"] =
|
|
443
|
+
space_id = self.get_space_id(space_id=space_id, space_key=space_key)
|
|
444
|
+
if space_id is not None:
|
|
445
|
+
query["space-id"] = space_id
|
|
414
446
|
|
|
415
447
|
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
416
448
|
payload = typing.cast(dict[str, JsonType], payload)
|
|
@@ -425,10 +457,10 @@ class ConfluenceSession:
|
|
|
425
457
|
|
|
426
458
|
def get_page(self, page_id: str) -> ConfluencePage:
|
|
427
459
|
"""
|
|
428
|
-
Retrieve Confluence wiki page details.
|
|
460
|
+
Retrieve Confluence wiki page details and content.
|
|
429
461
|
|
|
430
462
|
:param page_id: The Confluence page ID.
|
|
431
|
-
:returns: Confluence page info.
|
|
463
|
+
:returns: Confluence page info and content.
|
|
432
464
|
"""
|
|
433
465
|
|
|
434
466
|
path = f"/pages/{page_id}"
|
|
@@ -453,6 +485,33 @@ class ConfluenceSession:
|
|
|
453
485
|
content=typing.cast(str, storage["value"]),
|
|
454
486
|
)
|
|
455
487
|
|
|
488
|
+
@functools.cache
|
|
489
|
+
def get_page_metadata(self, page_id: str) -> ConfluencePageMetadata:
|
|
490
|
+
"""
|
|
491
|
+
Retrieve Confluence wiki page details.
|
|
492
|
+
|
|
493
|
+
:param page_id: The Confluence page ID.
|
|
494
|
+
:returns: Confluence page info.
|
|
495
|
+
"""
|
|
496
|
+
|
|
497
|
+
path = f"/pages/{page_id}"
|
|
498
|
+
payload = self._invoke(ConfluenceVersion.VERSION_2, path)
|
|
499
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
500
|
+
version = typing.cast(dict[str, JsonType], data["version"])
|
|
501
|
+
|
|
502
|
+
return ConfluencePageMetadata(
|
|
503
|
+
id=page_id,
|
|
504
|
+
space_id=typing.cast(str, data["spaceId"]),
|
|
505
|
+
parent_id=typing.cast(str, data["parentId"]),
|
|
506
|
+
parent_type=(
|
|
507
|
+
ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
|
|
508
|
+
if data["parentType"] is not None
|
|
509
|
+
else None
|
|
510
|
+
),
|
|
511
|
+
title=typing.cast(str, data["title"]),
|
|
512
|
+
version=typing.cast(int, version["number"]),
|
|
513
|
+
)
|
|
514
|
+
|
|
456
515
|
def get_page_version(self, page_id: str) -> int:
|
|
457
516
|
"""
|
|
458
517
|
Retrieve a Confluence wiki page version.
|
|
@@ -507,26 +566,21 @@ class ConfluenceSession:
|
|
|
507
566
|
|
|
508
567
|
def create_page(
|
|
509
568
|
self,
|
|
510
|
-
|
|
569
|
+
parent_id: str,
|
|
511
570
|
title: str,
|
|
512
571
|
new_content: str,
|
|
513
|
-
*,
|
|
514
|
-
space_key: Optional[str] = None,
|
|
515
572
|
) -> ConfluencePage:
|
|
516
573
|
"""
|
|
517
574
|
Create a new page via Confluence API.
|
|
518
575
|
"""
|
|
519
576
|
|
|
520
|
-
|
|
521
|
-
if coalesced_space_key is None:
|
|
522
|
-
raise ArgumentError("Confluence space key required for creating a new page")
|
|
523
|
-
|
|
577
|
+
parent_page = self.get_page_metadata(parent_id)
|
|
524
578
|
path = "/pages/"
|
|
525
579
|
query = {
|
|
526
|
-
"spaceId":
|
|
580
|
+
"spaceId": parent_page.space_id,
|
|
527
581
|
"status": "current",
|
|
528
582
|
"title": title,
|
|
529
|
-
"parentId":
|
|
583
|
+
"parentId": parent_id,
|
|
530
584
|
"body": {"storage": {"value": new_content, "representation": "storage"}},
|
|
531
585
|
}
|
|
532
586
|
|
|
@@ -584,13 +638,24 @@ class ConfluenceSession:
|
|
|
584
638
|
response.raise_for_status()
|
|
585
639
|
|
|
586
640
|
def page_exists(
|
|
587
|
-
self,
|
|
641
|
+
self,
|
|
642
|
+
title: str,
|
|
643
|
+
*,
|
|
644
|
+
space_id: Optional[str] = None,
|
|
645
|
+
space_key: Optional[str] = None,
|
|
588
646
|
) -> Optional[str]:
|
|
647
|
+
"""
|
|
648
|
+
Check if a Confluence page exists with the given title.
|
|
649
|
+
|
|
650
|
+
:param title: Page title. Pages in the same Confluence space must have a unique title.
|
|
651
|
+
:param space_key: Identifies the Confluence space.
|
|
652
|
+
"""
|
|
653
|
+
|
|
654
|
+
space_id = self.get_space_id(space_id=space_id, space_key=space_key)
|
|
589
655
|
path = "/pages"
|
|
590
|
-
coalesced_space_key = space_key or self.space_key
|
|
591
656
|
query = {"title": title}
|
|
592
|
-
if
|
|
593
|
-
query["space-id"] =
|
|
657
|
+
if space_id is not None:
|
|
658
|
+
query["space-id"] = space_id
|
|
594
659
|
|
|
595
660
|
LOGGER.info("Checking if page exists with title: %s", title)
|
|
596
661
|
|
|
@@ -609,14 +674,20 @@ class ConfluenceSession:
|
|
|
609
674
|
else:
|
|
610
675
|
return None
|
|
611
676
|
|
|
612
|
-
def get_or_create_page(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
677
|
+
def get_or_create_page(self, title: str, parent_id: str) -> ConfluencePage:
|
|
678
|
+
"""
|
|
679
|
+
Find a page with the given title, or create a new page if no such page exists.
|
|
680
|
+
|
|
681
|
+
:param title: Page title. Pages in the same Confluence space must have a unique title.
|
|
682
|
+
:param parent_id: Identifies the parent page for a new child page.
|
|
683
|
+
"""
|
|
684
|
+
|
|
685
|
+
parent_page = self.get_page_metadata(parent_id)
|
|
686
|
+
page_id = self.page_exists(title, space_id=parent_page.space_id)
|
|
616
687
|
|
|
617
688
|
if page_id is not None:
|
|
618
689
|
LOGGER.debug("Retrieving existing page: %s", page_id)
|
|
619
690
|
return self.get_page(page_id)
|
|
620
691
|
else:
|
|
621
692
|
LOGGER.debug("Creating new page with title: %s", title)
|
|
622
|
-
return self.create_page(parent_id, title, ""
|
|
693
|
+
return self.create_page(parent_id, title, "")
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Publish Markdown files to Confluence wiki.
|
|
3
|
+
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/md2conf
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from .api import ConfluencePage, ConfluenceSession
|
|
15
|
+
from .converter import (
|
|
16
|
+
ConfluenceDocument,
|
|
17
|
+
ConfluenceDocumentOptions,
|
|
18
|
+
ConfluencePageID,
|
|
19
|
+
attachment_name,
|
|
20
|
+
extract_frontmatter_title,
|
|
21
|
+
extract_qualified_id,
|
|
22
|
+
)
|
|
23
|
+
from .metadata import ConfluencePageMetadata
|
|
24
|
+
from .processor import Converter, Processor, ProcessorFactory
|
|
25
|
+
from .properties import PageError
|
|
26
|
+
|
|
27
|
+
LOGGER = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SynchronizingProcessor(Processor):
|
|
31
|
+
"""
|
|
32
|
+
Synchronizes a single Markdown page or a directory of Markdown pages with Confluence.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
api: ConfluenceSession
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self, api: ConfluenceSession, options: ConfluenceDocumentOptions, root_dir: Path
|
|
39
|
+
) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Initializes a new processor instance.
|
|
42
|
+
|
|
43
|
+
:param api: Holds information about an open session to a Confluence server.
|
|
44
|
+
:param options: Options that control the generated page content.
|
|
45
|
+
:param root_dir: File system directory that acts as topmost root node.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
super().__init__(options, api.site, root_dir)
|
|
49
|
+
self.api = api
|
|
50
|
+
|
|
51
|
+
def _get_or_create_page(
|
|
52
|
+
self,
|
|
53
|
+
absolute_path: Path,
|
|
54
|
+
parent_id: Optional[ConfluencePageID],
|
|
55
|
+
*,
|
|
56
|
+
title: Optional[str] = None,
|
|
57
|
+
) -> ConfluencePageMetadata:
|
|
58
|
+
"""
|
|
59
|
+
Creates a new Confluence page if no page is linked in the Markdown document.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
# parse file
|
|
63
|
+
with open(absolute_path, "r", encoding="utf-8") as f:
|
|
64
|
+
text = f.read()
|
|
65
|
+
|
|
66
|
+
qualified_id, text = extract_qualified_id(text)
|
|
67
|
+
|
|
68
|
+
overwrite = False
|
|
69
|
+
if qualified_id is None:
|
|
70
|
+
# create new Confluence page
|
|
71
|
+
if parent_id is None:
|
|
72
|
+
raise PageError(
|
|
73
|
+
f"expected: parent page ID for Markdown file with no linked Confluence page: {absolute_path}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# assign title from front-matter if present
|
|
77
|
+
if title is None:
|
|
78
|
+
title, _ = extract_frontmatter_title(text)
|
|
79
|
+
|
|
80
|
+
# use file name (without extension) and path hash if no title is supplied
|
|
81
|
+
if title is None:
|
|
82
|
+
overwrite = True
|
|
83
|
+
relative_path = absolute_path.relative_to(self.root_dir)
|
|
84
|
+
hash = hashlib.md5(relative_path.as_posix().encode("utf-8"))
|
|
85
|
+
digest = "".join(f"{c:x}" for c in hash.digest())
|
|
86
|
+
title = f"{absolute_path.stem} [{digest}]"
|
|
87
|
+
|
|
88
|
+
confluence_page = self._create_page(absolute_path, text, title, parent_id)
|
|
89
|
+
else:
|
|
90
|
+
# look up existing Confluence page
|
|
91
|
+
confluence_page = self.api.get_page(qualified_id.page_id)
|
|
92
|
+
|
|
93
|
+
space_key = (
|
|
94
|
+
self.api.space_id_to_key(confluence_page.space_id)
|
|
95
|
+
if confluence_page.space_id
|
|
96
|
+
else self.site.space_key
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return ConfluencePageMetadata(
|
|
100
|
+
page_id=confluence_page.id,
|
|
101
|
+
space_key=space_key,
|
|
102
|
+
title=confluence_page.title,
|
|
103
|
+
overwrite=overwrite,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _create_page(
|
|
107
|
+
self,
|
|
108
|
+
absolute_path: Path,
|
|
109
|
+
document: str,
|
|
110
|
+
title: str,
|
|
111
|
+
parent_id: ConfluencePageID,
|
|
112
|
+
) -> ConfluencePage:
|
|
113
|
+
"""
|
|
114
|
+
Creates a new Confluence page when Markdown file doesn't have an embedded page ID yet.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
confluence_page = self.api.get_or_create_page(title, parent_id.page_id)
|
|
118
|
+
self._update_markdown(
|
|
119
|
+
absolute_path,
|
|
120
|
+
document,
|
|
121
|
+
confluence_page.id,
|
|
122
|
+
self.api.space_id_to_key(confluence_page.space_id),
|
|
123
|
+
)
|
|
124
|
+
return confluence_page
|
|
125
|
+
|
|
126
|
+
def _save_document(self, document: ConfluenceDocument, path: Path) -> None:
|
|
127
|
+
"""
|
|
128
|
+
Saves a new version of a Confluence document.
|
|
129
|
+
|
|
130
|
+
Invokes Confluence REST API to persist the new version.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
base_path = path.parent
|
|
134
|
+
for image in document.images:
|
|
135
|
+
self.api.upload_attachment(
|
|
136
|
+
document.id.page_id,
|
|
137
|
+
attachment_name(image),
|
|
138
|
+
attachment_path=base_path / image,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
for name, data in document.embedded_images.items():
|
|
142
|
+
self.api.upload_attachment(
|
|
143
|
+
document.id.page_id,
|
|
144
|
+
name,
|
|
145
|
+
raw_data=data,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
content = document.xhtml()
|
|
149
|
+
|
|
150
|
+
# leave title as it is for existing pages, update title for pages with randomly assigned title
|
|
151
|
+
title = document.title if self.page_metadata[path].overwrite else None
|
|
152
|
+
|
|
153
|
+
LOGGER.debug("Generated Confluence Storage Format document:\n%s", content)
|
|
154
|
+
self.api.update_page(document.id.page_id, content, title=title)
|
|
155
|
+
|
|
156
|
+
def _update_markdown(
|
|
157
|
+
self,
|
|
158
|
+
path: Path,
|
|
159
|
+
document: str,
|
|
160
|
+
page_id: str,
|
|
161
|
+
space_key: Optional[str],
|
|
162
|
+
) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Writes the Confluence page ID and space key at the beginning of the Markdown file.
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
content: list[str] = []
|
|
168
|
+
|
|
169
|
+
# check if the file has frontmatter
|
|
170
|
+
index = 0
|
|
171
|
+
if document.startswith("---\n"):
|
|
172
|
+
index = document.find("\n---\n", 4) + 4
|
|
173
|
+
|
|
174
|
+
# insert the Confluence keys after the frontmatter
|
|
175
|
+
content.append(document[:index])
|
|
176
|
+
|
|
177
|
+
content.append(f"<!-- confluence-page-id: {page_id} -->")
|
|
178
|
+
if space_key:
|
|
179
|
+
content.append(f"<!-- confluence-space-key: {space_key} -->")
|
|
180
|
+
|
|
181
|
+
content.append(document[index:])
|
|
182
|
+
|
|
183
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
184
|
+
file.write("\n".join(content))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class SynchronizingProcessorFactory(ProcessorFactory):
|
|
188
|
+
api: ConfluenceSession
|
|
189
|
+
|
|
190
|
+
def __init__(
|
|
191
|
+
self, api: ConfluenceSession, options: ConfluenceDocumentOptions
|
|
192
|
+
) -> None:
|
|
193
|
+
super().__init__(options, api.site)
|
|
194
|
+
self.api = api
|
|
195
|
+
|
|
196
|
+
def create(self, root_dir: Path) -> Processor:
|
|
197
|
+
return SynchronizingProcessor(self.api, self.options, root_dir)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Application(Converter):
|
|
201
|
+
"""
|
|
202
|
+
The entry point for Markdown to Confluence conversion.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def __init__(
|
|
206
|
+
self, api: ConfluenceSession, options: ConfluenceDocumentOptions
|
|
207
|
+
) -> None:
|
|
208
|
+
super().__init__(SynchronizingProcessorFactory(api, options))
|