markdown-to-confluence 0.3.4__tar.gz → 0.3.5__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.4 → markdown_to_confluence-0.3.5}/PKG-INFO +15 -9
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/README.md +8 -2
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/markdown_to_confluence.egg-info/PKG-INFO +15 -9
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/markdown_to_confluence.egg-info/SOURCES.txt +3 -1
- markdown_to_confluence-0.3.5/markdown_to_confluence.egg-info/requires.txt +9 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/__init__.py +1 -1
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/api.py +142 -23
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/application.py +38 -33
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/converter.py +34 -102
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/local.py +16 -23
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/matcher.py +54 -13
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/mermaid.py +10 -4
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/metadata.py +1 -1
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/processor.py +7 -9
- markdown_to_confluence-0.3.5/md2conf/scanner.py +117 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/setup.cfg +6 -6
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/tests/test_conversion.py +8 -8
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/tests/test_matcher.py +6 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/tests/test_processor.py +1 -0
- markdown_to_confluence-0.3.5/tests/test_scanner.py +46 -0
- markdown_to_confluence-0.3.4/markdown_to_confluence.egg-info/requires.txt +0 -9
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/LICENSE +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/markdown_to_confluence.egg-info/dependency_links.txt +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/markdown_to_confluence.egg-info/entry_points.txt +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/markdown_to_confluence.egg-info/top_level.txt +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/markdown_to_confluence.egg-info/zip-safe +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/__main__.py +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/emoji.py +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/entities.dtd +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/properties.py +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/puppeteer-config.json +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/md2conf/py.typed +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/pyproject.toml +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/setup.py +0 -0
- {markdown_to_confluence-0.3.4 → markdown_to_confluence-0.3.5}/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.5
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
5
|
Home-page: https://github.com/hunyadi/md2conf
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -21,12 +21,12 @@ Classifier: Typing :: Typed
|
|
|
21
21
|
Requires-Python: >=3.9
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: lxml>=5.
|
|
25
|
-
Requires-Dist: types-lxml>=
|
|
26
|
-
Requires-Dist: markdown>=3.
|
|
27
|
-
Requires-Dist: types-markdown>=3.
|
|
28
|
-
Requires-Dist: pymdown-extensions>=10.
|
|
29
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: lxml>=5.4
|
|
25
|
+
Requires-Dist: types-lxml>=2025.3.30
|
|
26
|
+
Requires-Dist: markdown>=3.8
|
|
27
|
+
Requires-Dist: types-markdown>=3.8
|
|
28
|
+
Requires-Dist: pymdown-extensions>=10.15
|
|
29
|
+
Requires-Dist: PyYAML>=6.0
|
|
30
30
|
Requires-Dist: types-PyYAML>=6.0
|
|
31
31
|
Requires-Dist: requests>=2.32
|
|
32
32
|
Requires-Dist: types-requests>=2.32
|
|
@@ -198,20 +198,26 @@ root
|
|
|
198
198
|
└── Mean vs. median
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
+
### Lists and tables
|
|
202
|
+
|
|
203
|
+
If your Markdown lists or tables don't appear in Confluence as expected, verify that the list or table is delimited by a blank line both before and after, as per strict Markdown syntax. While some previewers accept a more lenient syntax (e.g. an itemized list immediately following a paragraph), *md2conf* uses [Python-Markdown](https://python-markdown.github.io/) internally to convert Markdown into XHTML, which expects the Markdown document to adhere to the stricter syntax.
|
|
204
|
+
|
|
201
205
|
### Publishing images
|
|
202
206
|
|
|
203
207
|
Local images referenced in a Markdown file are automatically published to Confluence as attachments to the page.
|
|
204
208
|
|
|
205
|
-
Unfortunately, Confluence struggles with SVG images, e.g. they may only show in *edit* mode, display in a wrong size or text labels in the image may be truncated. In order to mitigate the issue, whenever *md2conf* encounters a reference to an SVG image in a Markdown file, it checks whether a corresponding PNG image also exists in the same directory, and if a PNG image is found, it is published instead.
|
|
209
|
+
Unfortunately, Confluence struggles with SVG images, e.g. they may only show in *edit* mode, display in a wrong size or text labels in the image may be truncated. (This seems to be a known issue in Confluence.) In order to mitigate the issue, whenever *md2conf* encounters a reference to an SVG image in a Markdown file, it checks whether a corresponding PNG image also exists in the same directory, and if a PNG image is found, it is published instead.
|
|
206
210
|
|
|
207
211
|
External images referenced with an absolute URL retain the original URL.
|
|
208
212
|
|
|
209
213
|
### Ignoring files
|
|
210
214
|
|
|
211
|
-
Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
|
|
215
|
+
Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax (and constraints) of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
|
|
212
216
|
|
|
213
217
|
Files that don't have the extension `*.md` are skipped automatically. Hidden directories (whose name starts with `.`) are not recursed into.
|
|
214
218
|
|
|
219
|
+
Relative paths to items in a nested directory are not supported. You must put `.mdignore` in the same directory where the items to be skipped reside.
|
|
220
|
+
|
|
215
221
|
### Page title
|
|
216
222
|
|
|
217
223
|
*md2conf* makes a best-effort attempt at setting the Confluence wiki page title when it publishes a Markdown document the first time. The following are probed in this order:
|
|
@@ -164,20 +164,26 @@ root
|
|
|
164
164
|
└── Mean vs. median
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
+
### Lists and tables
|
|
168
|
+
|
|
169
|
+
If your Markdown lists or tables don't appear in Confluence as expected, verify that the list or table is delimited by a blank line both before and after, as per strict Markdown syntax. While some previewers accept a more lenient syntax (e.g. an itemized list immediately following a paragraph), *md2conf* uses [Python-Markdown](https://python-markdown.github.io/) internally to convert Markdown into XHTML, which expects the Markdown document to adhere to the stricter syntax.
|
|
170
|
+
|
|
167
171
|
### Publishing images
|
|
168
172
|
|
|
169
173
|
Local images referenced in a Markdown file are automatically published to Confluence as attachments to the page.
|
|
170
174
|
|
|
171
|
-
Unfortunately, Confluence struggles with SVG images, e.g. they may only show in *edit* mode, display in a wrong size or text labels in the image may be truncated. In order to mitigate the issue, whenever *md2conf* encounters a reference to an SVG image in a Markdown file, it checks whether a corresponding PNG image also exists in the same directory, and if a PNG image is found, it is published instead.
|
|
175
|
+
Unfortunately, Confluence struggles with SVG images, e.g. they may only show in *edit* mode, display in a wrong size or text labels in the image may be truncated. (This seems to be a known issue in Confluence.) In order to mitigate the issue, whenever *md2conf* encounters a reference to an SVG image in a Markdown file, it checks whether a corresponding PNG image also exists in the same directory, and if a PNG image is found, it is published instead.
|
|
172
176
|
|
|
173
177
|
External images referenced with an absolute URL retain the original URL.
|
|
174
178
|
|
|
175
179
|
### Ignoring files
|
|
176
180
|
|
|
177
|
-
Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
|
|
181
|
+
Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax (and constraints) of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
|
|
178
182
|
|
|
179
183
|
Files that don't have the extension `*.md` are skipped automatically. Hidden directories (whose name starts with `.`) are not recursed into.
|
|
180
184
|
|
|
185
|
+
Relative paths to items in a nested directory are not supported. You must put `.mdignore` in the same directory where the items to be skipped reside.
|
|
186
|
+
|
|
181
187
|
### Page title
|
|
182
188
|
|
|
183
189
|
*md2conf* makes a best-effort attempt at setting the Confluence wiki page title when it publishes a Markdown document the first time. The following are probed in this order:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Publish Markdown files to Confluence wiki
|
|
5
5
|
Home-page: https://github.com/hunyadi/md2conf
|
|
6
6
|
Author: Levente Hunyadi
|
|
@@ -21,12 +21,12 @@ Classifier: Typing :: Typed
|
|
|
21
21
|
Requires-Python: >=3.9
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
License-File: LICENSE
|
|
24
|
-
Requires-Dist: lxml>=5.
|
|
25
|
-
Requires-Dist: types-lxml>=
|
|
26
|
-
Requires-Dist: markdown>=3.
|
|
27
|
-
Requires-Dist: types-markdown>=3.
|
|
28
|
-
Requires-Dist: pymdown-extensions>=10.
|
|
29
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: lxml>=5.4
|
|
25
|
+
Requires-Dist: types-lxml>=2025.3.30
|
|
26
|
+
Requires-Dist: markdown>=3.8
|
|
27
|
+
Requires-Dist: types-markdown>=3.8
|
|
28
|
+
Requires-Dist: pymdown-extensions>=10.15
|
|
29
|
+
Requires-Dist: PyYAML>=6.0
|
|
30
30
|
Requires-Dist: types-PyYAML>=6.0
|
|
31
31
|
Requires-Dist: requests>=2.32
|
|
32
32
|
Requires-Dist: types-requests>=2.32
|
|
@@ -198,20 +198,26 @@ root
|
|
|
198
198
|
└── Mean vs. median
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
+
### Lists and tables
|
|
202
|
+
|
|
203
|
+
If your Markdown lists or tables don't appear in Confluence as expected, verify that the list or table is delimited by a blank line both before and after, as per strict Markdown syntax. While some previewers accept a more lenient syntax (e.g. an itemized list immediately following a paragraph), *md2conf* uses [Python-Markdown](https://python-markdown.github.io/) internally to convert Markdown into XHTML, which expects the Markdown document to adhere to the stricter syntax.
|
|
204
|
+
|
|
201
205
|
### Publishing images
|
|
202
206
|
|
|
203
207
|
Local images referenced in a Markdown file are automatically published to Confluence as attachments to the page.
|
|
204
208
|
|
|
205
|
-
Unfortunately, Confluence struggles with SVG images, e.g. they may only show in *edit* mode, display in a wrong size or text labels in the image may be truncated. In order to mitigate the issue, whenever *md2conf* encounters a reference to an SVG image in a Markdown file, it checks whether a corresponding PNG image also exists in the same directory, and if a PNG image is found, it is published instead.
|
|
209
|
+
Unfortunately, Confluence struggles with SVG images, e.g. they may only show in *edit* mode, display in a wrong size or text labels in the image may be truncated. (This seems to be a known issue in Confluence.) In order to mitigate the issue, whenever *md2conf* encounters a reference to an SVG image in a Markdown file, it checks whether a corresponding PNG image also exists in the same directory, and if a PNG image is found, it is published instead.
|
|
206
210
|
|
|
207
211
|
External images referenced with an absolute URL retain the original URL.
|
|
208
212
|
|
|
209
213
|
### Ignoring files
|
|
210
214
|
|
|
211
|
-
Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
|
|
215
|
+
Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax (and constraints) of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
|
|
212
216
|
|
|
213
217
|
Files that don't have the extension `*.md` are skipped automatically. Hidden directories (whose name starts with `.`) are not recursed into.
|
|
214
218
|
|
|
219
|
+
Relative paths to items in a nested directory are not supported. You must put `.mdignore` in the same directory where the items to be skipped reside.
|
|
220
|
+
|
|
215
221
|
### Page title
|
|
216
222
|
|
|
217
223
|
*md2conf* makes a best-effort attempt at setting the Confluence wiki page title when it publishes a Markdown document the first time. The following are probed in this order:
|
|
@@ -25,7 +25,9 @@ md2conf/processor.py
|
|
|
25
25
|
md2conf/properties.py
|
|
26
26
|
md2conf/puppeteer-config.json
|
|
27
27
|
md2conf/py.typed
|
|
28
|
+
md2conf/scanner.py
|
|
28
29
|
tests/test_conversion.py
|
|
29
30
|
tests/test_matcher.py
|
|
30
31
|
tests/test_mermaid.py
|
|
31
|
-
tests/test_processor.py
|
|
32
|
+
tests/test_processor.py
|
|
33
|
+
tests/test_scanner.py
|
|
@@ -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.5"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
10
|
__copyright__ = "Copyright 2022-2025, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
|
@@ -43,13 +43,23 @@ JsonType = Union[
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class ConfluenceVersion(enum.Enum):
|
|
46
|
+
"""
|
|
47
|
+
Confluence REST API version an HTTP request corresponds to.
|
|
48
|
+
|
|
49
|
+
For some operations, Confluence Cloud supports v2 endpoints exclusively. However, for other operations, only v1 endpoints are available via REST API.
|
|
50
|
+
Some versions of Confluence Server and Data Center, unfortunately, don't support v2 endpoints at all.
|
|
51
|
+
|
|
52
|
+
The principal use case for *md2conf* is Confluence Cloud. As such, *md2conf* uses v2 endpoints when available, and resorts to v1 endpoints only when
|
|
53
|
+
necessary.
|
|
54
|
+
"""
|
|
55
|
+
|
|
46
56
|
VERSION_1 = "rest/api"
|
|
47
57
|
VERSION_2 = "api/v2"
|
|
48
58
|
|
|
49
59
|
|
|
50
60
|
class ConfluencePageParentContentType(enum.Enum):
|
|
51
61
|
"""
|
|
52
|
-
Content types that can be a parent to a Confluence page
|
|
62
|
+
Content types that can be a parent to a Confluence page.
|
|
53
63
|
"""
|
|
54
64
|
|
|
55
65
|
PAGE = "page"
|
|
@@ -80,6 +90,15 @@ LOGGER = logging.getLogger(__name__)
|
|
|
80
90
|
|
|
81
91
|
@dataclass(frozen=True)
|
|
82
92
|
class ConfluenceAttachment:
|
|
93
|
+
"""
|
|
94
|
+
Holds data for an object uploaded to Confluence as a page attachment.
|
|
95
|
+
|
|
96
|
+
:param id: Unique ID for the attachment.
|
|
97
|
+
:param media_type: MIME type for the attachment.
|
|
98
|
+
:param file_size: Size in bytes.
|
|
99
|
+
:param comment: Description for the attachment.
|
|
100
|
+
"""
|
|
101
|
+
|
|
83
102
|
id: str
|
|
84
103
|
media_type: str
|
|
85
104
|
file_size: int
|
|
@@ -87,7 +106,18 @@ class ConfluenceAttachment:
|
|
|
87
106
|
|
|
88
107
|
|
|
89
108
|
@dataclass(frozen=True)
|
|
90
|
-
class
|
|
109
|
+
class ConfluencePageProperties:
|
|
110
|
+
"""
|
|
111
|
+
Holds Confluence page properties used for page synchronization.
|
|
112
|
+
|
|
113
|
+
:param id: Confluence page ID.
|
|
114
|
+
:param space_id: Confluence space ID.
|
|
115
|
+
:param parent_id: Confluence page ID of the immediate parent.
|
|
116
|
+
:param parent_type: Identifies the content type of the parent.
|
|
117
|
+
:param title: Page title.
|
|
118
|
+
:param version: Page version. Incremented when the page is updated.
|
|
119
|
+
"""
|
|
120
|
+
|
|
91
121
|
id: str
|
|
92
122
|
space_id: str
|
|
93
123
|
parent_id: str
|
|
@@ -97,11 +127,36 @@ class ConfluencePageMetadata:
|
|
|
97
127
|
|
|
98
128
|
|
|
99
129
|
@dataclass(frozen=True)
|
|
100
|
-
class ConfluencePage(
|
|
130
|
+
class ConfluencePage(ConfluencePageProperties):
|
|
131
|
+
"""
|
|
132
|
+
Holds Confluence page data used for page synchronization.
|
|
133
|
+
|
|
134
|
+
:param content: Page content in Confluence Storage Format.
|
|
135
|
+
"""
|
|
136
|
+
|
|
101
137
|
content: str
|
|
102
138
|
|
|
103
139
|
|
|
140
|
+
@dataclass(frozen=True)
|
|
141
|
+
class ConfluenceLabel:
|
|
142
|
+
"""
|
|
143
|
+
Holds information about a single label.
|
|
144
|
+
|
|
145
|
+
:param id: ID of the label.
|
|
146
|
+
:param name: Name of the label.
|
|
147
|
+
:param prefix: Prefix of the label.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
id: str
|
|
151
|
+
name: str
|
|
152
|
+
prefix: str
|
|
153
|
+
|
|
154
|
+
|
|
104
155
|
class ConfluenceAPI:
|
|
156
|
+
"""
|
|
157
|
+
Represents an active connection to a Confluence server.
|
|
158
|
+
"""
|
|
159
|
+
|
|
105
160
|
properties: ConfluenceConnectionProperties
|
|
106
161
|
session: Optional["ConfluenceSession"] = None
|
|
107
162
|
|
|
@@ -195,7 +250,7 @@ class ConfluenceSession:
|
|
|
195
250
|
path: str,
|
|
196
251
|
query: Optional[dict[str, str]] = None,
|
|
197
252
|
) -> JsonType:
|
|
198
|
-
"
|
|
253
|
+
"Executes an HTTP request via Confluence API."
|
|
199
254
|
|
|
200
255
|
url = self._build_url(version, path, query)
|
|
201
256
|
response = self.session.get(url)
|
|
@@ -204,7 +259,33 @@ class ConfluenceSession:
|
|
|
204
259
|
response.raise_for_status()
|
|
205
260
|
return response.json()
|
|
206
261
|
|
|
207
|
-
def
|
|
262
|
+
def _fetch(
|
|
263
|
+
self, path: str, query: Optional[dict[str, str]] = None
|
|
264
|
+
) -> list[JsonType]:
|
|
265
|
+
"Retrieves all results of a REST API v2 paginated result-set."
|
|
266
|
+
|
|
267
|
+
items: list[JsonType] = []
|
|
268
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path, query)
|
|
269
|
+
while True:
|
|
270
|
+
response = self.session.get(url)
|
|
271
|
+
response.raise_for_status()
|
|
272
|
+
|
|
273
|
+
payload = typing.cast(dict[str, JsonType], response.json())
|
|
274
|
+
results = typing.cast(list[JsonType], payload["results"])
|
|
275
|
+
items.extend(results)
|
|
276
|
+
|
|
277
|
+
links = typing.cast(dict[str, JsonType], payload.get("_links", {}))
|
|
278
|
+
link = typing.cast(str, links.get("next", ""))
|
|
279
|
+
if link:
|
|
280
|
+
url = f"https://{self.site.domain}{link}"
|
|
281
|
+
else:
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
return items
|
|
285
|
+
|
|
286
|
+
def _save(self, version: ConfluenceVersion, path: str, data: JsonType) -> None:
|
|
287
|
+
"Persists data via Confluence REST API."
|
|
288
|
+
|
|
208
289
|
url = self._build_url(version, path)
|
|
209
290
|
response = self.session.put(
|
|
210
291
|
url,
|
|
@@ -263,7 +344,7 @@ class ConfluenceSession:
|
|
|
263
344
|
self, *, space_id: Optional[str] = None, space_key: Optional[str] = None
|
|
264
345
|
) -> Optional[str]:
|
|
265
346
|
"""
|
|
266
|
-
|
|
347
|
+
Coalesces a space ID or space key into a space ID, accounting for site default.
|
|
267
348
|
|
|
268
349
|
:param space_id: A Confluence space ID.
|
|
269
350
|
:param space_key: A Confluence space key.
|
|
@@ -285,6 +366,10 @@ class ConfluenceSession:
|
|
|
285
366
|
def get_attachment_by_name(
|
|
286
367
|
self, page_id: str, filename: str
|
|
287
368
|
) -> ConfluenceAttachment:
|
|
369
|
+
"""
|
|
370
|
+
Retrieves a Confluence page attachment by an unprefixed file name.
|
|
371
|
+
"""
|
|
372
|
+
|
|
288
373
|
path = f"/pages/{page_id}/attachments"
|
|
289
374
|
query = {"filename": filename}
|
|
290
375
|
data = typing.cast(
|
|
@@ -313,6 +398,18 @@ class ConfluenceSession:
|
|
|
313
398
|
comment: Optional[str] = None,
|
|
314
399
|
force: bool = False,
|
|
315
400
|
) -> None:
|
|
401
|
+
"""
|
|
402
|
+
Uploads a new attachment to a Confluence page.
|
|
403
|
+
|
|
404
|
+
:param page_id: Confluence page ID.
|
|
405
|
+
:param attachment_name: Unprefixed name unique to the page.
|
|
406
|
+
:param attachment_path: Path to the file to upload as an attachment.
|
|
407
|
+
:param raw_data: Raw data to upload as an attachment.
|
|
408
|
+
:param content_type: Attachment MIME type.
|
|
409
|
+
:param comment: Attachment description.
|
|
410
|
+
:param force: Overwrite an existing attachment even if there seem to be no changes.
|
|
411
|
+
"""
|
|
412
|
+
|
|
316
413
|
if attachment_path is None and raw_data is None:
|
|
317
414
|
raise ArgumentError("required: `attachment_path` or `raw_data`")
|
|
318
415
|
|
|
@@ -409,7 +506,7 @@ class ConfluenceSession:
|
|
|
409
506
|
) -> None:
|
|
410
507
|
id = attachment_id.removeprefix("att")
|
|
411
508
|
path = f"/content/{page_id}/child/attachment/{id}"
|
|
412
|
-
data = {
|
|
509
|
+
data: JsonType = {
|
|
413
510
|
"id": attachment_id,
|
|
414
511
|
"type": "attachment",
|
|
415
512
|
"status": "current",
|
|
@@ -428,10 +525,11 @@ class ConfluenceSession:
|
|
|
428
525
|
space_key: Optional[str] = None,
|
|
429
526
|
) -> str:
|
|
430
527
|
"""
|
|
431
|
-
|
|
528
|
+
Looks up a Confluence wiki page ID by title.
|
|
432
529
|
|
|
433
530
|
:param title: The page title.
|
|
434
|
-
:param
|
|
531
|
+
:param space_id: The Confluence space ID (unless the default space is to be used). Exclusive with space key.
|
|
532
|
+
:param space_key: The Confluence space key (unless the default space is to be used). Exclusive with space ID.
|
|
435
533
|
:returns: Confluence page ID.
|
|
436
534
|
"""
|
|
437
535
|
|
|
@@ -457,7 +555,7 @@ class ConfluenceSession:
|
|
|
457
555
|
|
|
458
556
|
def get_page(self, page_id: str) -> ConfluencePage:
|
|
459
557
|
"""
|
|
460
|
-
|
|
558
|
+
Retrieves Confluence wiki page details and content.
|
|
461
559
|
|
|
462
560
|
:param page_id: The Confluence page ID.
|
|
463
561
|
:returns: Confluence page info and content.
|
|
@@ -486,9 +584,9 @@ class ConfluenceSession:
|
|
|
486
584
|
)
|
|
487
585
|
|
|
488
586
|
@functools.cache
|
|
489
|
-
def
|
|
587
|
+
def get_page_properties(self, page_id: str) -> ConfluencePageProperties:
|
|
490
588
|
"""
|
|
491
|
-
|
|
589
|
+
Retrieves Confluence wiki page details.
|
|
492
590
|
|
|
493
591
|
:param page_id: The Confluence page ID.
|
|
494
592
|
:returns: Confluence page info.
|
|
@@ -499,7 +597,7 @@ class ConfluenceSession:
|
|
|
499
597
|
data = typing.cast(dict[str, JsonType], payload)
|
|
500
598
|
version = typing.cast(dict[str, JsonType], data["version"])
|
|
501
599
|
|
|
502
|
-
return
|
|
600
|
+
return ConfluencePageProperties(
|
|
503
601
|
id=page_id,
|
|
504
602
|
space_id=typing.cast(str, data["spaceId"]),
|
|
505
603
|
parent_id=typing.cast(str, data["parentId"]),
|
|
@@ -514,7 +612,7 @@ class ConfluenceSession:
|
|
|
514
612
|
|
|
515
613
|
def get_page_version(self, page_id: str) -> int:
|
|
516
614
|
"""
|
|
517
|
-
|
|
615
|
+
Retrieves a Confluence wiki page version.
|
|
518
616
|
|
|
519
617
|
:param page_id: The Confluence page ID.
|
|
520
618
|
:returns: Confluence page version.
|
|
@@ -534,7 +632,7 @@ class ConfluenceSession:
|
|
|
534
632
|
title: Optional[str] = None,
|
|
535
633
|
) -> None:
|
|
536
634
|
"""
|
|
537
|
-
|
|
635
|
+
Updates a page via the Confluence API.
|
|
538
636
|
|
|
539
637
|
:param page_id: The Confluence page ID.
|
|
540
638
|
:param new_content: Confluence Storage Format XHTML.
|
|
@@ -553,7 +651,7 @@ class ConfluenceSession:
|
|
|
553
651
|
LOGGER.warning(exc)
|
|
554
652
|
|
|
555
653
|
path = f"/pages/{page_id}"
|
|
556
|
-
data = {
|
|
654
|
+
data: JsonType = {
|
|
557
655
|
"id": page_id,
|
|
558
656
|
"status": "current",
|
|
559
657
|
"title": new_title,
|
|
@@ -571,10 +669,10 @@ class ConfluenceSession:
|
|
|
571
669
|
new_content: str,
|
|
572
670
|
) -> ConfluencePage:
|
|
573
671
|
"""
|
|
574
|
-
|
|
672
|
+
Creates a new page via Confluence API.
|
|
575
673
|
"""
|
|
576
674
|
|
|
577
|
-
parent_page = self.
|
|
675
|
+
parent_page = self.get_page_properties(parent_id)
|
|
578
676
|
path = "/pages/"
|
|
579
677
|
query = {
|
|
580
678
|
"spaceId": parent_page.space_id,
|
|
@@ -615,10 +713,10 @@ class ConfluenceSession:
|
|
|
615
713
|
|
|
616
714
|
def delete_page(self, page_id: str, *, purge: bool = False) -> None:
|
|
617
715
|
"""
|
|
618
|
-
|
|
716
|
+
Deletes a page via Confluence API.
|
|
619
717
|
|
|
620
718
|
:param page_id: The Confluence page ID.
|
|
621
|
-
:param purge: True to completely purge the page, False to move to trash only.
|
|
719
|
+
:param purge: `True` to completely purge the page, `False` to move to trash only.
|
|
622
720
|
"""
|
|
623
721
|
|
|
624
722
|
path = f"/pages/{page_id}"
|
|
@@ -645,10 +743,12 @@ class ConfluenceSession:
|
|
|
645
743
|
space_key: Optional[str] = None,
|
|
646
744
|
) -> Optional[str]:
|
|
647
745
|
"""
|
|
648
|
-
|
|
746
|
+
Checks if a Confluence page exists with the given title.
|
|
649
747
|
|
|
650
748
|
:param title: Page title. Pages in the same Confluence space must have a unique title.
|
|
651
749
|
:param space_key: Identifies the Confluence space.
|
|
750
|
+
|
|
751
|
+
:returns: Confluence page ID of a matching page (if found), or `None`.
|
|
652
752
|
"""
|
|
653
753
|
|
|
654
754
|
space_id = self.get_space_id(space_id=space_id, space_key=space_key)
|
|
@@ -676,13 +776,13 @@ class ConfluenceSession:
|
|
|
676
776
|
|
|
677
777
|
def get_or_create_page(self, title: str, parent_id: str) -> ConfluencePage:
|
|
678
778
|
"""
|
|
679
|
-
|
|
779
|
+
Finds a page with the given title, or creates a new page if no such page exists.
|
|
680
780
|
|
|
681
781
|
:param title: Page title. Pages in the same Confluence space must have a unique title.
|
|
682
782
|
:param parent_id: Identifies the parent page for a new child page.
|
|
683
783
|
"""
|
|
684
784
|
|
|
685
|
-
parent_page = self.
|
|
785
|
+
parent_page = self.get_page_properties(parent_id)
|
|
686
786
|
page_id = self.page_exists(title, space_id=parent_page.space_id)
|
|
687
787
|
|
|
688
788
|
if page_id is not None:
|
|
@@ -691,3 +791,22 @@ class ConfluenceSession:
|
|
|
691
791
|
else:
|
|
692
792
|
LOGGER.debug("Creating new page with title: %s", title)
|
|
693
793
|
return self.create_page(parent_id, title, "")
|
|
794
|
+
|
|
795
|
+
def get_labels(self, page_id: str) -> list[ConfluenceLabel]:
|
|
796
|
+
"""
|
|
797
|
+
Retrieves labels for a Confluence page.
|
|
798
|
+
|
|
799
|
+
:param page_id: The Confluence page ID.
|
|
800
|
+
:returns: A list of page labels.
|
|
801
|
+
"""
|
|
802
|
+
|
|
803
|
+
items: list[ConfluenceLabel] = []
|
|
804
|
+
path = f"/pages/{page_id}/labels"
|
|
805
|
+
results = self._fetch(path)
|
|
806
|
+
for r in results:
|
|
807
|
+
result = typing.cast(dict[str, JsonType], r)
|
|
808
|
+
id = typing.cast(str, result["id"])
|
|
809
|
+
name = typing.cast(str, result["name"])
|
|
810
|
+
prefix = typing.cast(str, result["prefix"])
|
|
811
|
+
items.append(ConfluenceLabel(id, name, prefix))
|
|
812
|
+
return items
|
|
@@ -17,12 +17,11 @@ from .converter import (
|
|
|
17
17
|
ConfluenceDocumentOptions,
|
|
18
18
|
ConfluencePageID,
|
|
19
19
|
attachment_name,
|
|
20
|
-
extract_frontmatter_title,
|
|
21
|
-
extract_qualified_id,
|
|
22
20
|
)
|
|
23
21
|
from .metadata import ConfluencePageMetadata
|
|
24
22
|
from .processor import Converter, Processor, ProcessorFactory
|
|
25
23
|
from .properties import PageError
|
|
24
|
+
from .scanner import Scanner
|
|
26
25
|
|
|
27
26
|
LOGGER = logging.getLogger(__name__)
|
|
28
27
|
|
|
@@ -49,56 +48,43 @@ class SynchronizingProcessor(Processor):
|
|
|
49
48
|
self.api = api
|
|
50
49
|
|
|
51
50
|
def _get_or_create_page(
|
|
52
|
-
self,
|
|
53
|
-
absolute_path: Path,
|
|
54
|
-
parent_id: Optional[ConfluencePageID],
|
|
55
|
-
*,
|
|
56
|
-
title: Optional[str] = None,
|
|
51
|
+
self, absolute_path: Path, parent_id: Optional[ConfluencePageID]
|
|
57
52
|
) -> ConfluencePageMetadata:
|
|
58
53
|
"""
|
|
59
54
|
Creates a new Confluence page if no page is linked in the Markdown document.
|
|
60
55
|
"""
|
|
61
56
|
|
|
62
57
|
# parse file
|
|
63
|
-
|
|
64
|
-
text = f.read()
|
|
65
|
-
|
|
66
|
-
qualified_id, text = extract_qualified_id(text)
|
|
58
|
+
document = Scanner().read(absolute_path)
|
|
67
59
|
|
|
68
60
|
overwrite = False
|
|
69
|
-
if
|
|
61
|
+
if document.page_id is None:
|
|
70
62
|
# create new Confluence page
|
|
71
63
|
if parent_id is None:
|
|
72
64
|
raise PageError(
|
|
73
65
|
f"expected: parent page ID for Markdown file with no linked Confluence page: {absolute_path}"
|
|
74
66
|
)
|
|
75
67
|
|
|
76
|
-
# assign title from front-matter if present
|
|
77
|
-
if title is None:
|
|
78
|
-
title, _ = extract_frontmatter_title(text)
|
|
79
|
-
|
|
80
68
|
# use file name (without extension) and path hash if no title is supplied
|
|
81
|
-
if title is None:
|
|
69
|
+
if document.title is not None:
|
|
70
|
+
title = document.title
|
|
71
|
+
else:
|
|
82
72
|
overwrite = True
|
|
83
73
|
relative_path = absolute_path.relative_to(self.root_dir)
|
|
84
74
|
hash = hashlib.md5(relative_path.as_posix().encode("utf-8"))
|
|
85
75
|
digest = "".join(f"{c:x}" for c in hash.digest())
|
|
86
76
|
title = f"{absolute_path.stem} [{digest}]"
|
|
87
77
|
|
|
88
|
-
confluence_page = self._create_page(
|
|
78
|
+
confluence_page = self._create_page(
|
|
79
|
+
absolute_path, document.text, title, parent_id
|
|
80
|
+
)
|
|
89
81
|
else:
|
|
90
82
|
# look up existing Confluence page
|
|
91
|
-
confluence_page = self.api.get_page(
|
|
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
|
-
)
|
|
83
|
+
confluence_page = self.api.get_page(document.page_id)
|
|
98
84
|
|
|
99
85
|
return ConfluencePageMetadata(
|
|
100
86
|
page_id=confluence_page.id,
|
|
101
|
-
space_key=
|
|
87
|
+
space_key=self.api.space_id_to_key(confluence_page.space_id),
|
|
102
88
|
title=confluence_page.title,
|
|
103
89
|
overwrite=overwrite,
|
|
104
90
|
)
|
|
@@ -123,7 +109,9 @@ class SynchronizingProcessor(Processor):
|
|
|
123
109
|
)
|
|
124
110
|
return confluence_page
|
|
125
111
|
|
|
126
|
-
def _save_document(
|
|
112
|
+
def _save_document(
|
|
113
|
+
self, page_id: ConfluencePageID, document: ConfluenceDocument, path: Path
|
|
114
|
+
) -> None:
|
|
127
115
|
"""
|
|
128
116
|
Saves a new version of a Confluence document.
|
|
129
117
|
|
|
@@ -133,25 +121,40 @@ class SynchronizingProcessor(Processor):
|
|
|
133
121
|
base_path = path.parent
|
|
134
122
|
for image in document.images:
|
|
135
123
|
self.api.upload_attachment(
|
|
136
|
-
|
|
124
|
+
page_id.page_id,
|
|
137
125
|
attachment_name(image),
|
|
138
126
|
attachment_path=base_path / image,
|
|
139
127
|
)
|
|
140
128
|
|
|
141
129
|
for name, data in document.embedded_images.items():
|
|
142
130
|
self.api.upload_attachment(
|
|
143
|
-
|
|
131
|
+
page_id.page_id,
|
|
144
132
|
name,
|
|
145
133
|
raw_data=data,
|
|
146
134
|
)
|
|
147
135
|
|
|
148
136
|
content = document.xhtml()
|
|
137
|
+
LOGGER.debug("Generated Confluence Storage Format document:\n%s", content)
|
|
149
138
|
|
|
150
|
-
|
|
151
|
-
|
|
139
|
+
title = None
|
|
140
|
+
if document.title is not None:
|
|
141
|
+
meta = self.page_metadata[path]
|
|
152
142
|
|
|
153
|
-
|
|
154
|
-
|
|
143
|
+
# update title only for pages with randomly assigned title
|
|
144
|
+
if meta.overwrite:
|
|
145
|
+
conflicting_page_id = self.api.page_exists(
|
|
146
|
+
document.title, space_id=self.api.space_key_to_id(meta.space_key)
|
|
147
|
+
)
|
|
148
|
+
if conflicting_page_id is None:
|
|
149
|
+
title = document.title
|
|
150
|
+
else:
|
|
151
|
+
LOGGER.info(
|
|
152
|
+
"Document title of %s conflicts with Confluence page title of %s",
|
|
153
|
+
path,
|
|
154
|
+
conflicting_page_id,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
self.api.update_page(page_id.page_id, content, title=title)
|
|
155
158
|
|
|
156
159
|
def _update_markdown(
|
|
157
160
|
self,
|
|
@@ -200,6 +203,8 @@ class SynchronizingProcessorFactory(ProcessorFactory):
|
|
|
200
203
|
class Application(Converter):
|
|
201
204
|
"""
|
|
202
205
|
The entry point for Markdown to Confluence conversion.
|
|
206
|
+
|
|
207
|
+
This is the class instantiated by the command-line application.
|
|
203
208
|
"""
|
|
204
209
|
|
|
205
210
|
def __init__(
|