markdown-to-confluence 0.3.3__py3-none-any.whl → 0.3.5__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.3.dist-info → markdown_to_confluence-0.3.5.dist-info}/METADATA +24 -11
- markdown_to_confluence-0.3.5.dist-info/RECORD +23 -0
- {markdown_to_confluence-0.3.3.dist-info → markdown_to_confluence-0.3.5.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +6 -5
- md2conf/api.py +235 -45
- md2conf/application.py +100 -182
- md2conf/converter.py +53 -112
- md2conf/local.py +125 -0
- md2conf/matcher.py +54 -13
- md2conf/mermaid.py +10 -4
- md2conf/metadata.py +42 -0
- md2conf/processor.py +158 -90
- md2conf/scanner.py +117 -0
- markdown_to_confluence-0.3.3.dist-info/RECORD +0 -20
- {markdown_to_confluence-0.3.3.dist-info → markdown_to_confluence-0.3.5.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.3.3.dist-info → markdown_to_confluence-0.3.5.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.3.3.dist-info → markdown_to_confluence-0.3.5.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.3.3.dist-info → markdown_to_confluence-0.3.5.dist-info}/zip-safe +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
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -222,6 +228,13 @@ Files that don't have the extension `*.md` are skipped automatically. Hidden dir
|
|
|
222
228
|
|
|
223
229
|
If a matching Confluence page already exists for a Markdown file, the page title in Confluence is left unchanged.
|
|
224
230
|
|
|
231
|
+
### Converting diagrams
|
|
232
|
+
|
|
233
|
+
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:
|
|
234
|
+
|
|
235
|
+
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.
|
|
236
|
+
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.
|
|
237
|
+
|
|
225
238
|
### Running the tool
|
|
226
239
|
|
|
227
240
|
You execute the command-line tool `md2conf` to synchronize the Markdown file with Confluence:
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
markdown_to_confluence-0.3.5.dist-info/licenses/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
2
|
+
md2conf/__init__.py,sha256=Uaqb3maQScpYs3FiH8kuM6pUh5JzE4Vy52MgU9pvMTw,402
|
|
3
|
+
md2conf/__main__.py,sha256=bFcfmSnTWeuhmDm7bJ3jJabZ2S8W9biuAP6_R-Cc9As,8034
|
|
4
|
+
md2conf/api.py,sha256=VxrAJ4yCsdGFVAEQQWw5aONwsMz0b6KvN4EMLXCKOwE,26905
|
|
5
|
+
md2conf/application.py,sha256=SIM4yLHaLnvG7wRJLbRvptrkc0q4JMuAhDnanqsuYzA,6697
|
|
6
|
+
md2conf/converter.py,sha256=ASXhs7g79dOU4x1QhfvKL8mtwth508GTGcb3AUHigC4,37286
|
|
7
|
+
md2conf/emoji.py,sha256=48QJtOD0F3Be1laYLvAOwe0GxrJS-vcfjtCdiBsNcAc,1960
|
|
8
|
+
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
9
|
+
md2conf/local.py,sha256=998bBRpDAOywA-L0KD4_VyuL2Xftflv0ler-uNPQZn4,3866
|
|
10
|
+
md2conf/matcher.py,sha256=y5WEZNklTpUoJtMJlulTvfhl_v-UMU6wySJAKit91ig,4940
|
|
11
|
+
md2conf/mermaid.py,sha256=ZETocFDKi_fSYyVR1pJ7fo207YYFSuT44MSYFQ8-cZ0,2562
|
|
12
|
+
md2conf/metadata.py,sha256=Xozg2PjJnis7VQYQT_edIvTb8u0cs_ZizPOAxc1N8vg,1003
|
|
13
|
+
md2conf/processor.py,sha256=jSLFy8hqZJXf3b79jp31Fn9-cm4j9xq4HDChp9pyhP0,6706
|
|
14
|
+
md2conf/properties.py,sha256=TOCXLdTfYkKjRwZaMgvXw0mNCI4opEUwpBXro2Kv2B4,2467
|
|
15
|
+
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
16
|
+
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
md2conf/scanner.py,sha256=iF8NCQAFO6Yut5aAQr7uxfWzVMMt9j3T5ADoVVSJWKQ,3543
|
|
18
|
+
markdown_to_confluence-0.3.5.dist-info/METADATA,sha256=NiXwBXtQ5WhHce_JX7TBUSefQSR5jk5fERe46BL4vwE,18462
|
|
19
|
+
markdown_to_confluence-0.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
+
markdown_to_confluence-0.3.5.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
21
|
+
markdown_to_confluence-0.3.5.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
22
|
+
markdown_to_confluence-0.3.5.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
23
|
+
markdown_to_confluence-0.3.5.dist-info/RECORD,,
|
md2conf/__init__.py
CHANGED
|
@@ -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"
|
md2conf/__main__.py
CHANGED
|
@@ -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
|
|
md2conf/api.py
CHANGED
|
@@ -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,
|
|
@@ -41,13 +43,23 @@ JsonType = Union[
|
|
|
41
43
|
|
|
42
44
|
|
|
43
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
|
+
|
|
44
56
|
VERSION_1 = "rest/api"
|
|
45
57
|
VERSION_2 = "api/v2"
|
|
46
58
|
|
|
47
59
|
|
|
48
60
|
class ConfluencePageParentContentType(enum.Enum):
|
|
49
61
|
"""
|
|
50
|
-
Content types that can be a parent to a Confluence page
|
|
62
|
+
Content types that can be a parent to a Confluence page.
|
|
51
63
|
"""
|
|
52
64
|
|
|
53
65
|
PAGE = "page"
|
|
@@ -76,26 +88,75 @@ def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
|
|
|
76
88
|
LOGGER = logging.getLogger(__name__)
|
|
77
89
|
|
|
78
90
|
|
|
79
|
-
@dataclass
|
|
91
|
+
@dataclass(frozen=True)
|
|
80
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
|
+
|
|
81
102
|
id: str
|
|
82
103
|
media_type: str
|
|
83
104
|
file_size: int
|
|
84
105
|
comment: str
|
|
85
106
|
|
|
86
107
|
|
|
87
|
-
@dataclass
|
|
88
|
-
class
|
|
108
|
+
@dataclass(frozen=True)
|
|
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
|
+
|
|
89
121
|
id: str
|
|
90
122
|
space_id: str
|
|
91
123
|
parent_id: str
|
|
92
124
|
parent_type: Optional[ConfluencePageParentContentType]
|
|
93
125
|
title: str
|
|
94
126
|
version: int
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
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
|
+
|
|
95
137
|
content: str
|
|
96
138
|
|
|
97
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
|
+
|
|
98
155
|
class ConfluenceAPI:
|
|
156
|
+
"""
|
|
157
|
+
Represents an active connection to a Confluence server.
|
|
158
|
+
"""
|
|
159
|
+
|
|
99
160
|
properties: ConfluenceConnectionProperties
|
|
100
161
|
session: Optional["ConfluenceSession"] = None
|
|
101
162
|
|
|
@@ -136,10 +197,12 @@ class ConfluenceAPI:
|
|
|
136
197
|
|
|
137
198
|
|
|
138
199
|
class ConfluenceSession:
|
|
200
|
+
"""
|
|
201
|
+
Information about an open session to a Confluence server.
|
|
202
|
+
"""
|
|
203
|
+
|
|
139
204
|
session: requests.Session
|
|
140
|
-
|
|
141
|
-
base_path: str
|
|
142
|
-
space_key: Optional[str]
|
|
205
|
+
site: ConfluenceSiteMetadata
|
|
143
206
|
|
|
144
207
|
_space_id_to_key: dict[str, str]
|
|
145
208
|
_space_key_to_id: dict[str, str]
|
|
@@ -152,9 +215,7 @@ class ConfluenceSession:
|
|
|
152
215
|
space_key: Optional[str] = None,
|
|
153
216
|
) -> None:
|
|
154
217
|
self.session = session
|
|
155
|
-
self.
|
|
156
|
-
self.base_path = base_path
|
|
157
|
-
self.space_key = space_key
|
|
218
|
+
self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
|
|
158
219
|
|
|
159
220
|
self._space_id_to_key = {}
|
|
160
221
|
self._space_key_to_id = {}
|
|
@@ -178,7 +239,9 @@ class ConfluenceSession:
|
|
|
178
239
|
:returns: A full URL.
|
|
179
240
|
"""
|
|
180
241
|
|
|
181
|
-
base_url =
|
|
242
|
+
base_url = (
|
|
243
|
+
f"https://{self.site.domain}{self.site.base_path}{version.value}{path}"
|
|
244
|
+
)
|
|
182
245
|
return build_url(base_url, query)
|
|
183
246
|
|
|
184
247
|
def _invoke(
|
|
@@ -187,7 +250,7 @@ class ConfluenceSession:
|
|
|
187
250
|
path: str,
|
|
188
251
|
query: Optional[dict[str, str]] = None,
|
|
189
252
|
) -> JsonType:
|
|
190
|
-
"
|
|
253
|
+
"Executes an HTTP request via Confluence API."
|
|
191
254
|
|
|
192
255
|
url = self._build_url(version, path, query)
|
|
193
256
|
response = self.session.get(url)
|
|
@@ -196,7 +259,33 @@ class ConfluenceSession:
|
|
|
196
259
|
response.raise_for_status()
|
|
197
260
|
return response.json()
|
|
198
261
|
|
|
199
|
-
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
|
+
|
|
200
289
|
url = self._build_url(version, path)
|
|
201
290
|
response = self.session.put(
|
|
202
291
|
url,
|
|
@@ -251,9 +340,36 @@ class ConfluenceSession:
|
|
|
251
340
|
|
|
252
341
|
return id
|
|
253
342
|
|
|
343
|
+
def get_space_id(
|
|
344
|
+
self, *, space_id: Optional[str] = None, space_key: Optional[str] = None
|
|
345
|
+
) -> Optional[str]:
|
|
346
|
+
"""
|
|
347
|
+
Coalesces a space ID or space key into a space ID, accounting for site default.
|
|
348
|
+
|
|
349
|
+
:param space_id: A Confluence space ID.
|
|
350
|
+
:param space_key: A Confluence space key.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
if space_id is not None and space_key is not None:
|
|
354
|
+
raise ConfluenceError("either space ID or space key is required; not both")
|
|
355
|
+
|
|
356
|
+
if space_id is not None:
|
|
357
|
+
return space_id
|
|
358
|
+
|
|
359
|
+
space_key = space_key or self.site.space_key
|
|
360
|
+
if space_key is not None:
|
|
361
|
+
return self.space_key_to_id(space_key)
|
|
362
|
+
|
|
363
|
+
# space ID and key are unset, and no default space is configured
|
|
364
|
+
return None
|
|
365
|
+
|
|
254
366
|
def get_attachment_by_name(
|
|
255
367
|
self, page_id: str, filename: str
|
|
256
368
|
) -> ConfluenceAttachment:
|
|
369
|
+
"""
|
|
370
|
+
Retrieves a Confluence page attachment by an unprefixed file name.
|
|
371
|
+
"""
|
|
372
|
+
|
|
257
373
|
path = f"/pages/{page_id}/attachments"
|
|
258
374
|
query = {"filename": filename}
|
|
259
375
|
data = typing.cast(
|
|
@@ -282,6 +398,18 @@ class ConfluenceSession:
|
|
|
282
398
|
comment: Optional[str] = None,
|
|
283
399
|
force: bool = False,
|
|
284
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
|
+
|
|
285
413
|
if attachment_path is None and raw_data is None:
|
|
286
414
|
raise ArgumentError("required: `attachment_path` or `raw_data`")
|
|
287
415
|
|
|
@@ -378,7 +506,7 @@ class ConfluenceSession:
|
|
|
378
506
|
) -> None:
|
|
379
507
|
id = attachment_id.removeprefix("att")
|
|
380
508
|
path = f"/content/{page_id}/child/attachment/{id}"
|
|
381
|
-
data = {
|
|
509
|
+
data: JsonType = {
|
|
382
510
|
"id": attachment_id,
|
|
383
511
|
"type": "attachment",
|
|
384
512
|
"status": "current",
|
|
@@ -393,13 +521,15 @@ class ConfluenceSession:
|
|
|
393
521
|
self,
|
|
394
522
|
title: str,
|
|
395
523
|
*,
|
|
524
|
+
space_id: Optional[str] = None,
|
|
396
525
|
space_key: Optional[str] = None,
|
|
397
526
|
) -> str:
|
|
398
527
|
"""
|
|
399
|
-
|
|
528
|
+
Looks up a Confluence wiki page ID by title.
|
|
400
529
|
|
|
401
530
|
:param title: The page title.
|
|
402
|
-
: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.
|
|
403
533
|
:returns: Confluence page ID.
|
|
404
534
|
"""
|
|
405
535
|
|
|
@@ -408,9 +538,9 @@ class ConfluenceSession:
|
|
|
408
538
|
query = {
|
|
409
539
|
"title": title,
|
|
410
540
|
}
|
|
411
|
-
|
|
412
|
-
if
|
|
413
|
-
query["space-id"] =
|
|
541
|
+
space_id = self.get_space_id(space_id=space_id, space_key=space_key)
|
|
542
|
+
if space_id is not None:
|
|
543
|
+
query["space-id"] = space_id
|
|
414
544
|
|
|
415
545
|
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
416
546
|
payload = typing.cast(dict[str, JsonType], payload)
|
|
@@ -425,10 +555,10 @@ class ConfluenceSession:
|
|
|
425
555
|
|
|
426
556
|
def get_page(self, page_id: str) -> ConfluencePage:
|
|
427
557
|
"""
|
|
428
|
-
|
|
558
|
+
Retrieves Confluence wiki page details and content.
|
|
429
559
|
|
|
430
560
|
:param page_id: The Confluence page ID.
|
|
431
|
-
:returns: Confluence page info.
|
|
561
|
+
:returns: Confluence page info and content.
|
|
432
562
|
"""
|
|
433
563
|
|
|
434
564
|
path = f"/pages/{page_id}"
|
|
@@ -453,9 +583,36 @@ class ConfluenceSession:
|
|
|
453
583
|
content=typing.cast(str, storage["value"]),
|
|
454
584
|
)
|
|
455
585
|
|
|
586
|
+
@functools.cache
|
|
587
|
+
def get_page_properties(self, page_id: str) -> ConfluencePageProperties:
|
|
588
|
+
"""
|
|
589
|
+
Retrieves Confluence wiki page details.
|
|
590
|
+
|
|
591
|
+
:param page_id: The Confluence page ID.
|
|
592
|
+
:returns: Confluence page info.
|
|
593
|
+
"""
|
|
594
|
+
|
|
595
|
+
path = f"/pages/{page_id}"
|
|
596
|
+
payload = self._invoke(ConfluenceVersion.VERSION_2, path)
|
|
597
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
598
|
+
version = typing.cast(dict[str, JsonType], data["version"])
|
|
599
|
+
|
|
600
|
+
return ConfluencePageProperties(
|
|
601
|
+
id=page_id,
|
|
602
|
+
space_id=typing.cast(str, data["spaceId"]),
|
|
603
|
+
parent_id=typing.cast(str, data["parentId"]),
|
|
604
|
+
parent_type=(
|
|
605
|
+
ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
|
|
606
|
+
if data["parentType"] is not None
|
|
607
|
+
else None
|
|
608
|
+
),
|
|
609
|
+
title=typing.cast(str, data["title"]),
|
|
610
|
+
version=typing.cast(int, version["number"]),
|
|
611
|
+
)
|
|
612
|
+
|
|
456
613
|
def get_page_version(self, page_id: str) -> int:
|
|
457
614
|
"""
|
|
458
|
-
|
|
615
|
+
Retrieves a Confluence wiki page version.
|
|
459
616
|
|
|
460
617
|
:param page_id: The Confluence page ID.
|
|
461
618
|
:returns: Confluence page version.
|
|
@@ -475,7 +632,7 @@ class ConfluenceSession:
|
|
|
475
632
|
title: Optional[str] = None,
|
|
476
633
|
) -> None:
|
|
477
634
|
"""
|
|
478
|
-
|
|
635
|
+
Updates a page via the Confluence API.
|
|
479
636
|
|
|
480
637
|
:param page_id: The Confluence page ID.
|
|
481
638
|
:param new_content: Confluence Storage Format XHTML.
|
|
@@ -494,7 +651,7 @@ class ConfluenceSession:
|
|
|
494
651
|
LOGGER.warning(exc)
|
|
495
652
|
|
|
496
653
|
path = f"/pages/{page_id}"
|
|
497
|
-
data = {
|
|
654
|
+
data: JsonType = {
|
|
498
655
|
"id": page_id,
|
|
499
656
|
"status": "current",
|
|
500
657
|
"title": new_title,
|
|
@@ -507,26 +664,21 @@ class ConfluenceSession:
|
|
|
507
664
|
|
|
508
665
|
def create_page(
|
|
509
666
|
self,
|
|
510
|
-
|
|
667
|
+
parent_id: str,
|
|
511
668
|
title: str,
|
|
512
669
|
new_content: str,
|
|
513
|
-
*,
|
|
514
|
-
space_key: Optional[str] = None,
|
|
515
670
|
) -> ConfluencePage:
|
|
516
671
|
"""
|
|
517
|
-
|
|
672
|
+
Creates a new page via Confluence API.
|
|
518
673
|
"""
|
|
519
674
|
|
|
520
|
-
|
|
521
|
-
if coalesced_space_key is None:
|
|
522
|
-
raise ArgumentError("Confluence space key required for creating a new page")
|
|
523
|
-
|
|
675
|
+
parent_page = self.get_page_properties(parent_id)
|
|
524
676
|
path = "/pages/"
|
|
525
677
|
query = {
|
|
526
|
-
"spaceId":
|
|
678
|
+
"spaceId": parent_page.space_id,
|
|
527
679
|
"status": "current",
|
|
528
680
|
"title": title,
|
|
529
|
-
"parentId":
|
|
681
|
+
"parentId": parent_id,
|
|
530
682
|
"body": {"storage": {"value": new_content, "representation": "storage"}},
|
|
531
683
|
}
|
|
532
684
|
|
|
@@ -561,10 +713,10 @@ class ConfluenceSession:
|
|
|
561
713
|
|
|
562
714
|
def delete_page(self, page_id: str, *, purge: bool = False) -> None:
|
|
563
715
|
"""
|
|
564
|
-
|
|
716
|
+
Deletes a page via Confluence API.
|
|
565
717
|
|
|
566
718
|
:param page_id: The Confluence page ID.
|
|
567
|
-
: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.
|
|
568
720
|
"""
|
|
569
721
|
|
|
570
722
|
path = f"/pages/{page_id}"
|
|
@@ -584,13 +736,26 @@ class ConfluenceSession:
|
|
|
584
736
|
response.raise_for_status()
|
|
585
737
|
|
|
586
738
|
def page_exists(
|
|
587
|
-
self,
|
|
739
|
+
self,
|
|
740
|
+
title: str,
|
|
741
|
+
*,
|
|
742
|
+
space_id: Optional[str] = None,
|
|
743
|
+
space_key: Optional[str] = None,
|
|
588
744
|
) -> Optional[str]:
|
|
745
|
+
"""
|
|
746
|
+
Checks if a Confluence page exists with the given title.
|
|
747
|
+
|
|
748
|
+
:param title: Page title. Pages in the same Confluence space must have a unique title.
|
|
749
|
+
:param space_key: Identifies the Confluence space.
|
|
750
|
+
|
|
751
|
+
:returns: Confluence page ID of a matching page (if found), or `None`.
|
|
752
|
+
"""
|
|
753
|
+
|
|
754
|
+
space_id = self.get_space_id(space_id=space_id, space_key=space_key)
|
|
589
755
|
path = "/pages"
|
|
590
|
-
coalesced_space_key = space_key or self.space_key
|
|
591
756
|
query = {"title": title}
|
|
592
|
-
if
|
|
593
|
-
query["space-id"] =
|
|
757
|
+
if space_id is not None:
|
|
758
|
+
query["space-id"] = space_id
|
|
594
759
|
|
|
595
760
|
LOGGER.info("Checking if page exists with title: %s", title)
|
|
596
761
|
|
|
@@ -609,14 +774,39 @@ class ConfluenceSession:
|
|
|
609
774
|
else:
|
|
610
775
|
return None
|
|
611
776
|
|
|
612
|
-
def get_or_create_page(
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
777
|
+
def get_or_create_page(self, title: str, parent_id: str) -> ConfluencePage:
|
|
778
|
+
"""
|
|
779
|
+
Finds a page with the given title, or creates a new page if no such page exists.
|
|
780
|
+
|
|
781
|
+
:param title: Page title. Pages in the same Confluence space must have a unique title.
|
|
782
|
+
:param parent_id: Identifies the parent page for a new child page.
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
parent_page = self.get_page_properties(parent_id)
|
|
786
|
+
page_id = self.page_exists(title, space_id=parent_page.space_id)
|
|
616
787
|
|
|
617
788
|
if page_id is not None:
|
|
618
789
|
LOGGER.debug("Retrieving existing page: %s", page_id)
|
|
619
790
|
return self.get_page(page_id)
|
|
620
791
|
else:
|
|
621
792
|
LOGGER.debug("Creating new page with title: %s", title)
|
|
622
|
-
return self.create_page(parent_id, title, ""
|
|
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
|