markdown-to-confluence 0.3.2__py3-none-any.whl → 0.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/METADATA +32 -8
- markdown_to_confluence-0.3.4.dist-info/RECORD +22 -0
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +40 -14
- md2conf/api.py +135 -50
- md2conf/application.py +89 -160
- md2conf/converter.py +142 -44
- md2conf/emoji.py +3 -1
- md2conf/local.py +132 -0
- md2conf/mermaid.py +1 -1
- md2conf/metadata.py +42 -0
- md2conf/processor.py +159 -88
- md2conf/properties.py +40 -16
- markdown_to_confluence-0.3.2.dist-info/RECORD +0 -20
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.3.2.dist-info → markdown_to_confluence-0.3.4.dist-info}/zip-safe +0 -0
|
@@ -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
|
|
@@ -167,7 +167,7 @@ The concepts above are illustrated in the following sections.
|
|
|
167
167
|
|
|
168
168
|
#### File-system directory hierarchy
|
|
169
169
|
|
|
170
|
-
The title of each Markdown file (either the text of the
|
|
170
|
+
The title of each Markdown file (either the text of the topmost unique heading (`#`), or the title specified in front-matter) is shown next to the file name.
|
|
171
171
|
|
|
172
172
|
```
|
|
173
173
|
.
|
|
@@ -198,12 +198,37 @@ root
|
|
|
198
198
|
└── Mean vs. median
|
|
199
199
|
```
|
|
200
200
|
|
|
201
|
+
### Publishing images
|
|
202
|
+
|
|
203
|
+
Local images referenced in a Markdown file are automatically published to Confluence as attachments to the page.
|
|
204
|
+
|
|
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.
|
|
206
|
+
|
|
207
|
+
External images referenced with an absolute URL retain the original URL.
|
|
208
|
+
|
|
201
209
|
### Ignoring files
|
|
202
210
|
|
|
203
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.
|
|
204
212
|
|
|
205
213
|
Files that don't have the extension `*.md` are skipped automatically. Hidden directories (whose name starts with `.`) are not recursed into.
|
|
206
214
|
|
|
215
|
+
### Page title
|
|
216
|
+
|
|
217
|
+
*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:
|
|
218
|
+
|
|
219
|
+
1. The `title` attribute set in the [front-matter](https://daily-dev-tips.com/posts/what-exactly-is-frontmatter/). Front-matter is a block delimited by `---` at the beginning of a Markdown document. Currently, only YAML syntax is supported.
|
|
220
|
+
2. The text of the topmost unique Markdown heading (`#`). For example, if a document has a single first-level heading (e.g. `# My document`), its text is used. However, if there are multiple first-level headings, this step is skipped.
|
|
221
|
+
3. The file name (without the extension `.md`).
|
|
222
|
+
|
|
223
|
+
If a matching Confluence page already exists for a Markdown file, the page title in Confluence is left unchanged.
|
|
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
|
+
|
|
207
232
|
### Running the tool
|
|
208
233
|
|
|
209
234
|
You execute the command-line tool `md2conf` to synchronize the Markdown file with Confluence:
|
|
@@ -216,10 +241,8 @@ Use the `--help` switch to get a full list of supported command-line options:
|
|
|
216
241
|
|
|
217
242
|
```console
|
|
218
243
|
$ python3 -m md2conf --help
|
|
219
|
-
usage: md2conf [-h] [--version] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE]
|
|
220
|
-
[-
|
|
221
|
-
[--render-mermaid] [--no-render-mermaid] [--render-mermaid-format {png,svg}] [--heading-anchors]
|
|
222
|
-
[--ignore-invalid-url] [--local] [--headers [KEY=VALUE ...]] [--webui-links]
|
|
244
|
+
usage: md2conf [-h] [--version] [-d DOMAIN] [-p PATH] [-u USERNAME] [-a APIKEY] [-s SPACE] [-l {debug,info,warning,error,critical}] [-r ROOT_PAGE] [--keep-hierarchy] [--generated-by GENERATED_BY] [--no-generated-by]
|
|
245
|
+
[--render-mermaid] [--no-render-mermaid] [--render-mermaid-format {png,svg}] [--heading-anchors] [--ignore-invalid-url] [--local] [--headers [KEY=VALUE ...]] [--webui-links]
|
|
223
246
|
mdpath
|
|
224
247
|
|
|
225
248
|
positional arguments:
|
|
@@ -240,6 +263,7 @@ options:
|
|
|
240
263
|
-l {debug,info,warning,error,critical}, --loglevel {debug,info,warning,error,critical}
|
|
241
264
|
Use this option to set the log verbosity.
|
|
242
265
|
-r ROOT_PAGE Root Confluence page to create new pages. If omitted, will raise exception when creating new pages.
|
|
266
|
+
--keep-hierarchy Maintain source directory structure when exporting to Confluence.
|
|
243
267
|
--generated-by GENERATED_BY
|
|
244
268
|
Add prompt to pages (default: 'This page has been generated with a tool.').
|
|
245
269
|
--no-generated-by Do not add 'generated by a tool' prompt to pages.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
markdown_to_confluence-0.3.4.dist-info/licenses/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
|
|
2
|
+
md2conf/__init__.py,sha256=9gI6OYCv9-54FzxjNHLOH09H5quUDEMWq9pdbhnwoXM,402
|
|
3
|
+
md2conf/__main__.py,sha256=bFcfmSnTWeuhmDm7bJ3jJabZ2S8W9biuAP6_R-Cc9As,8034
|
|
4
|
+
md2conf/api.py,sha256=ZIYoBXclLbzrrQ_oFRllsTEnQIMbxqd9OD80-AC5qM0,22769
|
|
5
|
+
md2conf/application.py,sha256=eIVeAGUzfdIq1uYLYpTg30UNSq-YcUIY-OgKKK3M4E4,6436
|
|
6
|
+
md2conf/converter.py,sha256=2Sgq1WQd-dCtrdTVrBwhowPC8PmubMNCH1aAcRwntjs,39404
|
|
7
|
+
md2conf/emoji.py,sha256=48QJtOD0F3Be1laYLvAOwe0GxrJS-vcfjtCdiBsNcAc,1960
|
|
8
|
+
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
9
|
+
md2conf/local.py,sha256=AOuwyvPOXrRRPGOTDeoVYkMPJ9MI2zqRGAvHuY35wy4,3884
|
|
10
|
+
md2conf/matcher.py,sha256=FgMFPvGiOqGezCs8OyerfsVo-iIHFoI6LRMzdcjM5UY,3693
|
|
11
|
+
md2conf/mermaid.py,sha256=un_KHBDpG5Zad_QD3HN1uBwUxp4I-HVJYhNKbH7KwcA,2312
|
|
12
|
+
md2conf/metadata.py,sha256=9BtNRsICbKzPTs63P70XekNARePdW1DtdKNJqXh2ZFM,1013
|
|
13
|
+
md2conf/processor.py,sha256=Ko_3WqLK6jM-bEN7OD9Vc3g3vhSjRYawz3fG6uoUsXc,6733
|
|
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
|
+
markdown_to_confluence-0.3.4.dist-info/METADATA,sha256=PUtJXudDooVfwOzVtohxweWHMjgDv5CIrDvyqiJ0tlg,17745
|
|
18
|
+
markdown_to_confluence-0.3.4.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
|
|
19
|
+
markdown_to_confluence-0.3.4.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
20
|
+
markdown_to_confluence-0.3.4.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
21
|
+
markdown_to_confluence-0.3.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
22
|
+
markdown_to_confluence-0.3.4.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.4"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
10
|
__copyright__ = "Copyright 2022-2025, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
md2conf/__main__.py
CHANGED
|
@@ -22,18 +22,23 @@ 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 .
|
|
27
|
-
from .
|
|
25
|
+
from .converter import ConfluenceDocumentOptions, ConfluencePageID
|
|
26
|
+
from .local import LocalConverter
|
|
27
|
+
from .metadata import ConfluenceSiteMetadata
|
|
28
|
+
from .properties import (
|
|
29
|
+
ArgumentError,
|
|
30
|
+
ConfluenceConnectionProperties,
|
|
31
|
+
ConfluenceSiteProperties,
|
|
32
|
+
)
|
|
28
33
|
|
|
29
34
|
|
|
30
35
|
class Arguments(argparse.Namespace):
|
|
31
36
|
mdpath: Path
|
|
32
|
-
domain: str
|
|
33
|
-
path: str
|
|
34
|
-
username: str
|
|
35
|
-
apikey: str
|
|
36
|
-
space: str
|
|
37
|
+
domain: Optional[str]
|
|
38
|
+
path: Optional[str]
|
|
39
|
+
username: Optional[str]
|
|
40
|
+
apikey: Optional[str]
|
|
41
|
+
space: Optional[str]
|
|
37
42
|
loglevel: str
|
|
38
43
|
ignore_invalid_url: bool
|
|
39
44
|
heading_anchors: bool
|
|
@@ -195,24 +200,45 @@ def main() -> None:
|
|
|
195
200
|
heading_anchors=args.heading_anchors,
|
|
196
201
|
ignore_invalid_url=args.ignore_invalid_url,
|
|
197
202
|
generated_by=args.generated_by,
|
|
198
|
-
root_page_id=args.root_page,
|
|
203
|
+
root_page_id=ConfluencePageID(args.root_page) if args.root_page else None,
|
|
199
204
|
keep_hierarchy=args.keep_hierarchy,
|
|
200
205
|
render_mermaid=args.render_mermaid,
|
|
201
206
|
diagram_output_format=args.diagram_output_format,
|
|
202
207
|
webui_links=args.webui_links,
|
|
203
208
|
)
|
|
204
|
-
properties = ConfluenceProperties(
|
|
205
|
-
args.domain, args.path, args.username, args.apikey, args.space, args.headers
|
|
206
|
-
)
|
|
207
209
|
if args.local:
|
|
208
|
-
|
|
210
|
+
try:
|
|
211
|
+
site_properties = ConfluenceSiteProperties(
|
|
212
|
+
domain=args.domain,
|
|
213
|
+
base_path=args.path,
|
|
214
|
+
space_key=args.space,
|
|
215
|
+
)
|
|
216
|
+
except ArgumentError as e:
|
|
217
|
+
parser.error(str(e))
|
|
218
|
+
site_metadata = ConfluenceSiteMetadata(
|
|
219
|
+
domain=site_properties.domain,
|
|
220
|
+
base_path=site_properties.base_path,
|
|
221
|
+
space_key=site_properties.space_key,
|
|
222
|
+
)
|
|
223
|
+
LocalConverter(options, site_metadata).process(args.mdpath)
|
|
209
224
|
else:
|
|
225
|
+
try:
|
|
226
|
+
properties = ConfluenceConnectionProperties(
|
|
227
|
+
args.domain,
|
|
228
|
+
args.path,
|
|
229
|
+
args.username,
|
|
230
|
+
args.apikey,
|
|
231
|
+
args.space,
|
|
232
|
+
args.headers,
|
|
233
|
+
)
|
|
234
|
+
except ArgumentError as e:
|
|
235
|
+
parser.error(str(e))
|
|
210
236
|
try:
|
|
211
237
|
with ConfluenceAPI(properties) as api:
|
|
212
238
|
Application(
|
|
213
239
|
api,
|
|
214
240
|
options,
|
|
215
|
-
).
|
|
241
|
+
).process(args.mdpath)
|
|
216
242
|
except requests.exceptions.HTTPError as err:
|
|
217
243
|
logging.error(err)
|
|
218
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,7 +22,13 @@ from urllib.parse import urlencode, urlparse, urlunparse
|
|
|
21
22
|
import requests
|
|
22
23
|
|
|
23
24
|
from .converter import ParseError, sanitize_confluence
|
|
24
|
-
from .
|
|
25
|
+
from .metadata import ConfluenceSiteMetadata
|
|
26
|
+
from .properties import (
|
|
27
|
+
ArgumentError,
|
|
28
|
+
ConfluenceConnectionProperties,
|
|
29
|
+
ConfluenceError,
|
|
30
|
+
PageError,
|
|
31
|
+
)
|
|
25
32
|
|
|
26
33
|
# a JSON type with possible `null` values
|
|
27
34
|
JsonType = Union[
|
|
@@ -44,6 +51,7 @@ class ConfluencePageParentContentType(enum.Enum):
|
|
|
44
51
|
"""
|
|
45
52
|
Content types that can be a parent to a Confluence page
|
|
46
53
|
"""
|
|
54
|
+
|
|
47
55
|
PAGE = "page"
|
|
48
56
|
WHITEBOARD = "whiteboard"
|
|
49
57
|
DATABASE = "database"
|
|
@@ -70,7 +78,7 @@ def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
|
|
|
70
78
|
LOGGER = logging.getLogger(__name__)
|
|
71
79
|
|
|
72
80
|
|
|
73
|
-
@dataclass
|
|
81
|
+
@dataclass(frozen=True)
|
|
74
82
|
class ConfluenceAttachment:
|
|
75
83
|
id: str
|
|
76
84
|
media_type: str
|
|
@@ -78,23 +86,29 @@ class ConfluenceAttachment:
|
|
|
78
86
|
comment: str
|
|
79
87
|
|
|
80
88
|
|
|
81
|
-
@dataclass
|
|
82
|
-
class
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class ConfluencePageMetadata:
|
|
83
91
|
id: str
|
|
84
92
|
space_id: str
|
|
85
93
|
parent_id: str
|
|
86
|
-
parent_type: ConfluencePageParentContentType
|
|
94
|
+
parent_type: Optional[ConfluencePageParentContentType]
|
|
87
95
|
title: str
|
|
88
96
|
version: int
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class ConfluencePage(ConfluencePageMetadata):
|
|
89
101
|
content: str
|
|
90
102
|
|
|
91
103
|
|
|
92
104
|
class ConfluenceAPI:
|
|
93
|
-
properties:
|
|
105
|
+
properties: ConfluenceConnectionProperties
|
|
94
106
|
session: Optional["ConfluenceSession"] = None
|
|
95
107
|
|
|
96
|
-
def __init__(
|
|
97
|
-
self
|
|
108
|
+
def __init__(
|
|
109
|
+
self, properties: Optional[ConfluenceConnectionProperties] = None
|
|
110
|
+
) -> None:
|
|
111
|
+
self.properties = properties or ConfluenceConnectionProperties()
|
|
98
112
|
|
|
99
113
|
def __enter__(self) -> "ConfluenceSession":
|
|
100
114
|
session = requests.Session()
|
|
@@ -128,10 +142,12 @@ class ConfluenceAPI:
|
|
|
128
142
|
|
|
129
143
|
|
|
130
144
|
class ConfluenceSession:
|
|
145
|
+
"""
|
|
146
|
+
Information about an open session to a Confluence server.
|
|
147
|
+
"""
|
|
148
|
+
|
|
131
149
|
session: requests.Session
|
|
132
|
-
|
|
133
|
-
base_path: str
|
|
134
|
-
space_key: Optional[str]
|
|
150
|
+
site: ConfluenceSiteMetadata
|
|
135
151
|
|
|
136
152
|
_space_id_to_key: dict[str, str]
|
|
137
153
|
_space_key_to_id: dict[str, str]
|
|
@@ -141,12 +157,10 @@ class ConfluenceSession:
|
|
|
141
157
|
session: requests.Session,
|
|
142
158
|
domain: str,
|
|
143
159
|
base_path: str,
|
|
144
|
-
space_key: Optional[str],
|
|
160
|
+
space_key: Optional[str] = None,
|
|
145
161
|
) -> None:
|
|
146
162
|
self.session = session
|
|
147
|
-
self.
|
|
148
|
-
self.base_path = base_path
|
|
149
|
-
self.space_key = space_key
|
|
163
|
+
self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
|
|
150
164
|
|
|
151
165
|
self._space_id_to_key = {}
|
|
152
166
|
self._space_key_to_id = {}
|
|
@@ -170,7 +184,9 @@ class ConfluenceSession:
|
|
|
170
184
|
:returns: A full URL.
|
|
171
185
|
"""
|
|
172
186
|
|
|
173
|
-
base_url =
|
|
187
|
+
base_url = (
|
|
188
|
+
f"https://{self.site.domain}{self.site.base_path}{version.value}{path}"
|
|
189
|
+
)
|
|
174
190
|
return build_url(base_url, query)
|
|
175
191
|
|
|
176
192
|
def _invoke(
|
|
@@ -183,11 +199,9 @@ class ConfluenceSession:
|
|
|
183
199
|
|
|
184
200
|
url = self._build_url(version, path, query)
|
|
185
201
|
response = self.session.get(url)
|
|
186
|
-
response.
|
|
187
|
-
if len(response.text) > 240:
|
|
188
|
-
LOGGER.debug("Received HTTP payload (truncated):\n%.240s...", response.text)
|
|
189
|
-
else:
|
|
202
|
+
if response.text:
|
|
190
203
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
204
|
+
response.raise_for_status()
|
|
191
205
|
return response.json()
|
|
192
206
|
|
|
193
207
|
def _save(self, version: ConfluenceVersion, path: str, data: dict) -> None:
|
|
@@ -197,6 +211,8 @@ class ConfluenceSession:
|
|
|
197
211
|
data=json.dumps(data),
|
|
198
212
|
headers={"Content-Type": "application/json"},
|
|
199
213
|
)
|
|
214
|
+
if response.text:
|
|
215
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
200
216
|
response.raise_for_status()
|
|
201
217
|
|
|
202
218
|
def space_id_to_key(self, id: str) -> str:
|
|
@@ -243,6 +259,29 @@ class ConfluenceSession:
|
|
|
243
259
|
|
|
244
260
|
return id
|
|
245
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
|
+
|
|
246
285
|
def get_attachment_by_name(
|
|
247
286
|
self, page_id: str, filename: str
|
|
248
287
|
) -> ConfluenceAttachment:
|
|
@@ -275,10 +314,10 @@ class ConfluenceSession:
|
|
|
275
314
|
force: bool = False,
|
|
276
315
|
) -> None:
|
|
277
316
|
if attachment_path is None and raw_data is None:
|
|
278
|
-
raise
|
|
317
|
+
raise ArgumentError("required: `attachment_path` or `raw_data`")
|
|
279
318
|
|
|
280
319
|
if attachment_path is not None and raw_data is not None:
|
|
281
|
-
raise
|
|
320
|
+
raise ArgumentError("expected: either `attachment_path` or `raw_data`")
|
|
282
321
|
|
|
283
322
|
if content_type is None:
|
|
284
323
|
if attachment_path is not None:
|
|
@@ -288,7 +327,7 @@ class ConfluenceSession:
|
|
|
288
327
|
content_type, _ = mimetypes.guess_type(name, strict=True)
|
|
289
328
|
|
|
290
329
|
if attachment_path is not None and not attachment_path.is_file():
|
|
291
|
-
raise
|
|
330
|
+
raise PageError(f"file not found: {attachment_path}")
|
|
292
331
|
|
|
293
332
|
try:
|
|
294
333
|
attachment = self.get_attachment_by_name(page_id, attachment_name)
|
|
@@ -385,6 +424,7 @@ class ConfluenceSession:
|
|
|
385
424
|
self,
|
|
386
425
|
title: str,
|
|
387
426
|
*,
|
|
427
|
+
space_id: Optional[str] = None,
|
|
388
428
|
space_key: Optional[str] = None,
|
|
389
429
|
) -> str:
|
|
390
430
|
"""
|
|
@@ -400,9 +440,9 @@ class ConfluenceSession:
|
|
|
400
440
|
query = {
|
|
401
441
|
"title": title,
|
|
402
442
|
}
|
|
403
|
-
|
|
404
|
-
if
|
|
405
|
-
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
|
|
406
446
|
|
|
407
447
|
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
408
448
|
payload = typing.cast(dict[str, JsonType], payload)
|
|
@@ -417,10 +457,10 @@ class ConfluenceSession:
|
|
|
417
457
|
|
|
418
458
|
def get_page(self, page_id: str) -> ConfluencePage:
|
|
419
459
|
"""
|
|
420
|
-
Retrieve Confluence wiki page details.
|
|
460
|
+
Retrieve Confluence wiki page details and content.
|
|
421
461
|
|
|
422
462
|
:param page_id: The Confluence page ID.
|
|
423
|
-
:returns: Confluence page info.
|
|
463
|
+
:returns: Confluence page info and content.
|
|
424
464
|
"""
|
|
425
465
|
|
|
426
466
|
path = f"/pages/{page_id}"
|
|
@@ -435,12 +475,43 @@ class ConfluenceSession:
|
|
|
435
475
|
id=page_id,
|
|
436
476
|
space_id=typing.cast(str, data["spaceId"]),
|
|
437
477
|
parent_id=typing.cast(str, data["parentId"]),
|
|
438
|
-
parent_type=
|
|
478
|
+
parent_type=(
|
|
479
|
+
ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
|
|
480
|
+
if data["parentType"] is not None
|
|
481
|
+
else None
|
|
482
|
+
),
|
|
439
483
|
title=typing.cast(str, data["title"]),
|
|
440
484
|
version=typing.cast(int, version["number"]),
|
|
441
485
|
content=typing.cast(str, storage["value"]),
|
|
442
486
|
)
|
|
443
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
|
+
|
|
444
515
|
def get_page_version(self, page_id: str) -> int:
|
|
445
516
|
"""
|
|
446
517
|
Retrieve a Confluence wiki page version.
|
|
@@ -495,28 +566,21 @@ class ConfluenceSession:
|
|
|
495
566
|
|
|
496
567
|
def create_page(
|
|
497
568
|
self,
|
|
498
|
-
|
|
569
|
+
parent_id: str,
|
|
499
570
|
title: str,
|
|
500
571
|
new_content: str,
|
|
501
|
-
*,
|
|
502
|
-
space_key: Optional[str] = None,
|
|
503
572
|
) -> ConfluencePage:
|
|
504
573
|
"""
|
|
505
574
|
Create a new page via Confluence API.
|
|
506
575
|
"""
|
|
507
576
|
|
|
508
|
-
|
|
509
|
-
if coalesced_space_key is None:
|
|
510
|
-
raise ConfluenceError(
|
|
511
|
-
"Confluence space key required for creating a new page"
|
|
512
|
-
)
|
|
513
|
-
|
|
577
|
+
parent_page = self.get_page_metadata(parent_id)
|
|
514
578
|
path = "/pages/"
|
|
515
579
|
query = {
|
|
516
|
-
"spaceId":
|
|
580
|
+
"spaceId": parent_page.space_id,
|
|
517
581
|
"status": "current",
|
|
518
582
|
"title": title,
|
|
519
|
-
"parentId":
|
|
583
|
+
"parentId": parent_id,
|
|
520
584
|
"body": {"storage": {"value": new_content, "representation": "storage"}},
|
|
521
585
|
}
|
|
522
586
|
|
|
@@ -539,7 +603,11 @@ class ConfluenceSession:
|
|
|
539
603
|
id=typing.cast(str, data["id"]),
|
|
540
604
|
space_id=typing.cast(str, data["spaceId"]),
|
|
541
605
|
parent_id=typing.cast(str, data["parentId"]),
|
|
542
|
-
parent_type=
|
|
606
|
+
parent_type=(
|
|
607
|
+
ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
|
|
608
|
+
if data["parentType"] is not None
|
|
609
|
+
else None
|
|
610
|
+
),
|
|
543
611
|
title=typing.cast(str, data["title"]),
|
|
544
612
|
version=typing.cast(int, version["number"]),
|
|
545
613
|
content=typing.cast(str, storage["value"]),
|
|
@@ -570,13 +638,24 @@ class ConfluenceSession:
|
|
|
570
638
|
response.raise_for_status()
|
|
571
639
|
|
|
572
640
|
def page_exists(
|
|
573
|
-
self,
|
|
641
|
+
self,
|
|
642
|
+
title: str,
|
|
643
|
+
*,
|
|
644
|
+
space_id: Optional[str] = None,
|
|
645
|
+
space_key: Optional[str] = None,
|
|
574
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)
|
|
575
655
|
path = "/pages"
|
|
576
|
-
coalesced_space_key = space_key or self.space_key
|
|
577
656
|
query = {"title": title}
|
|
578
|
-
if
|
|
579
|
-
query["space-id"] =
|
|
657
|
+
if space_id is not None:
|
|
658
|
+
query["space-id"] = space_id
|
|
580
659
|
|
|
581
660
|
LOGGER.info("Checking if page exists with title: %s", title)
|
|
582
661
|
|
|
@@ -595,14 +674,20 @@ class ConfluenceSession:
|
|
|
595
674
|
else:
|
|
596
675
|
return None
|
|
597
676
|
|
|
598
|
-
def get_or_create_page(
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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)
|
|
602
687
|
|
|
603
688
|
if page_id is not None:
|
|
604
689
|
LOGGER.debug("Retrieving existing page: %s", page_id)
|
|
605
690
|
return self.get_page(page_id)
|
|
606
691
|
else:
|
|
607
692
|
LOGGER.debug("Creating new page with title: %s", title)
|
|
608
|
-
return self.create_page(parent_id, title, ""
|
|
693
|
+
return self.create_page(parent_id, title, "")
|