markdown-to-confluence 0.2.7__py3-none-any.whl → 0.3.1__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.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/METADATA +44 -7
- markdown_to_confluence-0.3.1.dist-info/RECORD +20 -0
- {markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +2 -2
- md2conf/__main__.py +12 -4
- md2conf/api.py +203 -146
- md2conf/application.py +27 -19
- md2conf/converter.py +38 -45
- md2conf/emoji.py +1 -1
- md2conf/matcher.py +11 -6
- md2conf/mermaid.py +6 -2
- md2conf/processor.py +7 -7
- md2conf/properties.py +5 -7
- markdown_to_confluence-0.2.7.dist-info/RECORD +0 -21
- md2conf/util.py +0 -27
- {markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/LICENSE +0 -0
- {markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.2.7.dist-info → markdown_to_confluence-0.3.1.dist-info}/zip-safe +0 -0
md2conf/api.py
CHANGED
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Publish Markdown files to Confluence wiki.
|
|
3
3
|
|
|
4
|
-
Copyright 2022-
|
|
4
|
+
Copyright 2022-2025, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import enum
|
|
9
10
|
import io
|
|
10
11
|
import json
|
|
11
12
|
import logging
|
|
12
13
|
import mimetypes
|
|
13
14
|
import typing
|
|
14
|
-
from contextlib import contextmanager
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
from types import TracebackType
|
|
18
|
-
from typing import
|
|
18
|
+
from typing import Optional, Union
|
|
19
19
|
from urllib.parse import urlencode, urlparse, urlunparse
|
|
20
20
|
|
|
21
21
|
import requests
|
|
22
22
|
|
|
23
23
|
from .converter import ParseError, sanitize_confluence
|
|
24
24
|
from .properties import ConfluenceError, ConfluenceProperties
|
|
25
|
-
from .util import removeprefix
|
|
26
25
|
|
|
27
26
|
# a JSON type with possible `null` values
|
|
28
27
|
JsonType = Union[
|
|
@@ -31,12 +30,17 @@ JsonType = Union[
|
|
|
31
30
|
int,
|
|
32
31
|
float,
|
|
33
32
|
str,
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
dict[str, "JsonType"],
|
|
34
|
+
list["JsonType"],
|
|
36
35
|
]
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
|
|
38
|
+
class ConfluenceVersion(enum.Enum):
|
|
39
|
+
VERSION_1 = "rest/api"
|
|
40
|
+
VERSION_2 = "api/v2"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
|
|
40
44
|
"Builds a URL with scheme, host, port, path and query string parameters."
|
|
41
45
|
|
|
42
46
|
scheme, netloc, path, params, query_str, fragment = urlparse(base_url)
|
|
@@ -66,7 +70,7 @@ class ConfluenceAttachment:
|
|
|
66
70
|
@dataclass
|
|
67
71
|
class ConfluencePage:
|
|
68
72
|
id: str
|
|
69
|
-
|
|
73
|
+
space_id: str
|
|
70
74
|
title: str
|
|
71
75
|
version: int
|
|
72
76
|
content: str
|
|
@@ -101,7 +105,7 @@ class ConfluenceAPI:
|
|
|
101
105
|
|
|
102
106
|
def __exit__(
|
|
103
107
|
self,
|
|
104
|
-
exc_type: Optional[
|
|
108
|
+
exc_type: Optional[type[BaseException]],
|
|
105
109
|
exc_val: Optional[BaseException],
|
|
106
110
|
exc_tb: Optional[TracebackType],
|
|
107
111
|
) -> None:
|
|
@@ -114,40 +118,67 @@ class ConfluenceSession:
|
|
|
114
118
|
session: requests.Session
|
|
115
119
|
domain: str
|
|
116
120
|
base_path: str
|
|
117
|
-
space_key: str
|
|
121
|
+
space_key: Optional[str]
|
|
122
|
+
|
|
123
|
+
_space_id_to_key: dict[str, str]
|
|
124
|
+
_space_key_to_id: dict[str, str]
|
|
118
125
|
|
|
119
126
|
def __init__(
|
|
120
|
-
self,
|
|
127
|
+
self,
|
|
128
|
+
session: requests.Session,
|
|
129
|
+
domain: str,
|
|
130
|
+
base_path: str,
|
|
131
|
+
space_key: Optional[str],
|
|
121
132
|
) -> None:
|
|
122
133
|
self.session = session
|
|
123
134
|
self.domain = domain
|
|
124
135
|
self.base_path = base_path
|
|
125
136
|
self.space_key = space_key
|
|
126
137
|
|
|
138
|
+
self._space_id_to_key = {}
|
|
139
|
+
self._space_key_to_id = {}
|
|
140
|
+
|
|
127
141
|
def close(self) -> None:
|
|
128
142
|
self.session.close()
|
|
143
|
+
self.session = requests.Session()
|
|
129
144
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
def _build_url(
|
|
146
|
+
self,
|
|
147
|
+
version: ConfluenceVersion,
|
|
148
|
+
path: str,
|
|
149
|
+
query: Optional[dict[str, str]] = None,
|
|
150
|
+
) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Builds a full URL for invoking the Confluence API.
|
|
153
|
+
|
|
154
|
+
:param prefix: A URL path prefix that depends on the Confluence API version.
|
|
155
|
+
:param path: Path of API endpoint to invoke.
|
|
156
|
+
:param query: Query parameters to pass to the API endpoint.
|
|
157
|
+
:returns: A full URL.
|
|
158
|
+
"""
|
|
138
159
|
|
|
139
|
-
|
|
140
|
-
base_url = f"https://{self.domain}{self.base_path}rest/api{path}"
|
|
160
|
+
base_url = f"https://{self.domain}{self.base_path}{version.value}{path}"
|
|
141
161
|
return build_url(base_url, query)
|
|
142
162
|
|
|
143
|
-
def _invoke(
|
|
144
|
-
|
|
163
|
+
def _invoke(
|
|
164
|
+
self,
|
|
165
|
+
version: ConfluenceVersion,
|
|
166
|
+
path: str,
|
|
167
|
+
query: Optional[dict[str, str]] = None,
|
|
168
|
+
) -> JsonType:
|
|
169
|
+
"Execute an HTTP request via Confluence API."
|
|
170
|
+
|
|
171
|
+
url = self._build_url(version, path, query)
|
|
145
172
|
response = self.session.get(url)
|
|
146
173
|
response.raise_for_status()
|
|
174
|
+
if len(response.text) > 240:
|
|
175
|
+
LOGGER.debug("Received HTTP payload (truncated):\n%.240s...", response.text)
|
|
176
|
+
else:
|
|
177
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
147
178
|
return response.json()
|
|
148
179
|
|
|
149
|
-
def _save(self, path: str, data: dict) -> None:
|
|
150
|
-
url = self._build_url(path)
|
|
180
|
+
def _save(self, version: ConfluenceVersion, path: str, data: dict) -> None:
|
|
181
|
+
url = self._build_url(version, path)
|
|
151
182
|
response = self.session.put(
|
|
152
183
|
url,
|
|
153
184
|
data=json.dumps(data),
|
|
@@ -155,24 +186,68 @@ class ConfluenceSession:
|
|
|
155
186
|
)
|
|
156
187
|
response.raise_for_status()
|
|
157
188
|
|
|
189
|
+
def space_id_to_key(self, id: str) -> str:
|
|
190
|
+
"Finds the Confluence space key for a space ID."
|
|
191
|
+
|
|
192
|
+
key = self._space_id_to_key.get(id)
|
|
193
|
+
if key is None:
|
|
194
|
+
payload = self._invoke(
|
|
195
|
+
ConfluenceVersion.VERSION_2,
|
|
196
|
+
"/spaces",
|
|
197
|
+
{"ids": id, "type": "global", "status": "current"},
|
|
198
|
+
)
|
|
199
|
+
payload = typing.cast(dict[str, JsonType], payload)
|
|
200
|
+
results = typing.cast(list[JsonType], payload["results"])
|
|
201
|
+
if len(results) != 1:
|
|
202
|
+
raise ConfluenceError(f"unique space not found with id: {id}")
|
|
203
|
+
|
|
204
|
+
result = typing.cast(dict[str, JsonType], results[0])
|
|
205
|
+
key = typing.cast(str, result["key"])
|
|
206
|
+
|
|
207
|
+
self._space_id_to_key[id] = key
|
|
208
|
+
|
|
209
|
+
return key
|
|
210
|
+
|
|
211
|
+
def space_key_to_id(self, key: str) -> str:
|
|
212
|
+
"Finds the Confluence space ID for a space key."
|
|
213
|
+
|
|
214
|
+
id = self._space_key_to_id.get(key)
|
|
215
|
+
if id is None:
|
|
216
|
+
payload = self._invoke(
|
|
217
|
+
ConfluenceVersion.VERSION_2,
|
|
218
|
+
"/spaces",
|
|
219
|
+
{"keys": key, "type": "global", "status": "current"},
|
|
220
|
+
)
|
|
221
|
+
payload = typing.cast(dict[str, JsonType], payload)
|
|
222
|
+
results = typing.cast(list[JsonType], payload["results"])
|
|
223
|
+
if len(results) != 1:
|
|
224
|
+
raise ConfluenceError(f"unique space not found with key: {key}")
|
|
225
|
+
|
|
226
|
+
result = typing.cast(dict[str, JsonType], results[0])
|
|
227
|
+
id = typing.cast(str, result["id"])
|
|
228
|
+
|
|
229
|
+
self._space_key_to_id[key] = id
|
|
230
|
+
|
|
231
|
+
return id
|
|
232
|
+
|
|
158
233
|
def get_attachment_by_name(
|
|
159
|
-
self, page_id: str, filename: str
|
|
234
|
+
self, page_id: str, filename: str
|
|
160
235
|
) -> ConfluenceAttachment:
|
|
161
|
-
path = f"/
|
|
162
|
-
query = {"
|
|
163
|
-
data = typing.cast(
|
|
236
|
+
path = f"/pages/{page_id}/attachments"
|
|
237
|
+
query = {"filename": filename}
|
|
238
|
+
data = typing.cast(
|
|
239
|
+
dict[str, JsonType], self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
240
|
+
)
|
|
164
241
|
|
|
165
|
-
results = typing.cast(
|
|
242
|
+
results = typing.cast(list[JsonType], data["results"])
|
|
166
243
|
if len(results) != 1:
|
|
167
244
|
raise ConfluenceError(f"no such attachment on page {page_id}: {filename}")
|
|
168
|
-
result = typing.cast(
|
|
245
|
+
result = typing.cast(dict[str, JsonType], results[0])
|
|
169
246
|
|
|
170
247
|
id = typing.cast(str, result["id"])
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
comment = extensions.get("comment", "")
|
|
175
|
-
comment = typing.cast(str, comment)
|
|
248
|
+
media_type = typing.cast(str, result["mediaType"])
|
|
249
|
+
file_size = typing.cast(int, result["fileSize"])
|
|
250
|
+
comment = typing.cast(str, result.get("comment", ""))
|
|
176
251
|
return ConfluenceAttachment(id, media_type, file_size, comment)
|
|
177
252
|
|
|
178
253
|
def upload_attachment(
|
|
@@ -184,7 +259,6 @@ class ConfluenceSession:
|
|
|
184
259
|
raw_data: Optional[bytes] = None,
|
|
185
260
|
content_type: Optional[str] = None,
|
|
186
261
|
comment: Optional[str] = None,
|
|
187
|
-
space_key: Optional[str] = None,
|
|
188
262
|
force: bool = False,
|
|
189
263
|
) -> None:
|
|
190
264
|
|
|
@@ -205,9 +279,7 @@ class ConfluenceSession:
|
|
|
205
279
|
raise ConfluenceError(f"file not found: {attachment_path}")
|
|
206
280
|
|
|
207
281
|
try:
|
|
208
|
-
attachment = self.get_attachment_by_name(
|
|
209
|
-
page_id, attachment_name, space_key=space_key
|
|
210
|
-
)
|
|
282
|
+
attachment = self.get_attachment_by_name(page_id, attachment_name)
|
|
211
283
|
|
|
212
284
|
if attachment_path is not None:
|
|
213
285
|
if not force and attachment.file_size == attachment_path.stat().st_size:
|
|
@@ -220,13 +292,13 @@ class ConfluenceSession:
|
|
|
220
292
|
else:
|
|
221
293
|
raise NotImplementedError("never occurs")
|
|
222
294
|
|
|
223
|
-
id =
|
|
295
|
+
id = attachment.id.removeprefix("att")
|
|
224
296
|
path = f"/content/{page_id}/child/attachment/{id}/data"
|
|
225
297
|
|
|
226
298
|
except ConfluenceError:
|
|
227
299
|
path = f"/content/{page_id}/child/attachment"
|
|
228
300
|
|
|
229
|
-
url = self._build_url(path)
|
|
301
|
+
url = self._build_url(ConfluenceVersion.VERSION_1, path)
|
|
230
302
|
|
|
231
303
|
if attachment_path is not None:
|
|
232
304
|
with open(attachment_path, "rb") as attachment_file:
|
|
@@ -279,32 +351,23 @@ class ConfluenceSession:
|
|
|
279
351
|
version = result["version"]["number"] + 1
|
|
280
352
|
|
|
281
353
|
# ensure path component is retained in attachment name
|
|
282
|
-
self._update_attachment(
|
|
283
|
-
page_id, attachment_id, version, attachment_name, space_key=space_key
|
|
284
|
-
)
|
|
354
|
+
self._update_attachment(page_id, attachment_id, version, attachment_name)
|
|
285
355
|
|
|
286
356
|
def _update_attachment(
|
|
287
|
-
self,
|
|
288
|
-
page_id: str,
|
|
289
|
-
attachment_id: str,
|
|
290
|
-
version: int,
|
|
291
|
-
attachment_title: str,
|
|
292
|
-
*,
|
|
293
|
-
space_key: Optional[str] = None,
|
|
357
|
+
self, page_id: str, attachment_id: str, version: int, attachment_title: str
|
|
294
358
|
) -> None:
|
|
295
|
-
id = removeprefix(
|
|
359
|
+
id = attachment_id.removeprefix("att")
|
|
296
360
|
path = f"/content/{page_id}/child/attachment/{id}"
|
|
297
361
|
data = {
|
|
298
362
|
"id": attachment_id,
|
|
299
363
|
"type": "attachment",
|
|
300
364
|
"status": "current",
|
|
301
365
|
"title": attachment_title,
|
|
302
|
-
"space": {"key": space_key or self.space_key},
|
|
303
366
|
"version": {"minorEdit": True, "number": version},
|
|
304
367
|
}
|
|
305
368
|
|
|
306
369
|
LOGGER.info("Updating attachment: %s", attachment_id)
|
|
307
|
-
self._save(path, data)
|
|
370
|
+
self._save(ConfluenceVersion.VERSION_1, path, data)
|
|
308
371
|
|
|
309
372
|
def get_page_id_by_title(
|
|
310
373
|
self,
|
|
@@ -321,97 +384,61 @@ class ConfluenceSession:
|
|
|
321
384
|
"""
|
|
322
385
|
|
|
323
386
|
LOGGER.info("Looking up page with title: %s", title)
|
|
324
|
-
path = "/
|
|
325
|
-
query = {
|
|
326
|
-
|
|
387
|
+
path = "/pages"
|
|
388
|
+
query = {
|
|
389
|
+
"title": title,
|
|
390
|
+
}
|
|
391
|
+
coalesced_space_key = space_key or self.space_key
|
|
392
|
+
if coalesced_space_key is not None:
|
|
393
|
+
query["space-id"] = self.space_key_to_id(coalesced_space_key)
|
|
327
394
|
|
|
328
|
-
|
|
395
|
+
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
396
|
+
payload = typing.cast(dict[str, JsonType], payload)
|
|
397
|
+
|
|
398
|
+
results = typing.cast(list[JsonType], payload["results"])
|
|
329
399
|
if len(results) != 1:
|
|
330
|
-
raise ConfluenceError(f"page not found with title: {title}")
|
|
400
|
+
raise ConfluenceError(f"unique page not found with title: {title}")
|
|
331
401
|
|
|
332
|
-
result = typing.cast(
|
|
402
|
+
result = typing.cast(dict[str, JsonType], results[0])
|
|
333
403
|
id = typing.cast(str, result["id"])
|
|
334
404
|
return id
|
|
335
405
|
|
|
336
|
-
def get_page(
|
|
337
|
-
self, page_id: str, *, space_key: Optional[str] = None
|
|
338
|
-
) -> ConfluencePage:
|
|
406
|
+
def get_page(self, page_id: str) -> ConfluencePage:
|
|
339
407
|
"""
|
|
340
408
|
Retrieve Confluence wiki page details.
|
|
341
409
|
|
|
342
410
|
:param page_id: The Confluence page ID.
|
|
343
|
-
:param space_key: The Confluence space key (unless the default space is to be used).
|
|
344
411
|
:returns: Confluence page info.
|
|
345
412
|
"""
|
|
346
413
|
|
|
347
|
-
path = f"/
|
|
348
|
-
query = {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
version = typing.cast(Dict[str, JsonType], data["version"])
|
|
355
|
-
body = typing.cast(Dict[str, JsonType], data["body"])
|
|
356
|
-
storage = typing.cast(Dict[str, JsonType], body["storage"])
|
|
414
|
+
path = f"/pages/{page_id}"
|
|
415
|
+
query = {"body-format": "storage"}
|
|
416
|
+
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
417
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
418
|
+
version = typing.cast(dict[str, JsonType], data["version"])
|
|
419
|
+
body = typing.cast(dict[str, JsonType], data["body"])
|
|
420
|
+
storage = typing.cast(dict[str, JsonType], body["storage"])
|
|
357
421
|
|
|
358
422
|
return ConfluencePage(
|
|
359
423
|
id=page_id,
|
|
360
|
-
|
|
424
|
+
space_id=typing.cast(str, data["spaceId"]),
|
|
361
425
|
title=typing.cast(str, data["title"]),
|
|
362
426
|
version=typing.cast(int, version["number"]),
|
|
363
427
|
content=typing.cast(str, storage["value"]),
|
|
364
428
|
)
|
|
365
429
|
|
|
366
|
-
def
|
|
367
|
-
self, page_id: str, *, space_key: Optional[str] = None
|
|
368
|
-
) -> Dict[str, str]:
|
|
369
|
-
"""
|
|
370
|
-
Retrieve Confluence wiki page ancestors.
|
|
371
|
-
|
|
372
|
-
:param page_id: The Confluence page ID.
|
|
373
|
-
:param space_key: The Confluence space key (unless the default space is to be used).
|
|
374
|
-
:returns: Dictionary of ancestor page ID to title, with topmost ancestor first.
|
|
375
|
-
"""
|
|
376
|
-
|
|
377
|
-
path = f"/content/{page_id}"
|
|
378
|
-
query = {
|
|
379
|
-
"spaceKey": space_key or self.space_key,
|
|
380
|
-
"expand": "ancestors",
|
|
381
|
-
}
|
|
382
|
-
data = typing.cast(Dict[str, JsonType], self._invoke(path, query))
|
|
383
|
-
ancestors = typing.cast(List[JsonType], data["ancestors"])
|
|
384
|
-
|
|
385
|
-
# from the JSON array of ancestors, extract the "id" and "title"
|
|
386
|
-
results: Dict[str, str] = {}
|
|
387
|
-
for node in ancestors:
|
|
388
|
-
ancestor = typing.cast(Dict[str, JsonType], node)
|
|
389
|
-
id = typing.cast(str, ancestor["id"])
|
|
390
|
-
title = typing.cast(str, ancestor["title"])
|
|
391
|
-
results[id] = title
|
|
392
|
-
return results
|
|
393
|
-
|
|
394
|
-
def get_page_version(
|
|
395
|
-
self,
|
|
396
|
-
page_id: str,
|
|
397
|
-
*,
|
|
398
|
-
space_key: Optional[str] = None,
|
|
399
|
-
) -> int:
|
|
430
|
+
def get_page_version(self, page_id: str) -> int:
|
|
400
431
|
"""
|
|
401
432
|
Retrieve a Confluence wiki page version.
|
|
402
433
|
|
|
403
434
|
:param page_id: The Confluence page ID.
|
|
404
|
-
:param space_key: The Confluence space key (unless the default space is to be used).
|
|
405
435
|
:returns: Confluence page version.
|
|
406
436
|
"""
|
|
407
437
|
|
|
408
|
-
path = f"/
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
data = typing.cast(Dict[str, JsonType], self._invoke(path, query))
|
|
414
|
-
version = typing.cast(Dict[str, JsonType], data["version"])
|
|
438
|
+
path = f"/pages/{page_id}"
|
|
439
|
+
payload = self._invoke(ConfluenceVersion.VERSION_2, path)
|
|
440
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
441
|
+
version = typing.cast(dict[str, JsonType], data["version"])
|
|
415
442
|
return typing.cast(int, version["number"])
|
|
416
443
|
|
|
417
444
|
def update_page(
|
|
@@ -419,7 +446,6 @@ class ConfluenceSession:
|
|
|
419
446
|
page_id: str,
|
|
420
447
|
new_content: str,
|
|
421
448
|
*,
|
|
422
|
-
space_key: Optional[str] = None,
|
|
423
449
|
title: Optional[str] = None,
|
|
424
450
|
) -> None:
|
|
425
451
|
"""
|
|
@@ -427,11 +453,10 @@ class ConfluenceSession:
|
|
|
427
453
|
|
|
428
454
|
:param page_id: The Confluence page ID.
|
|
429
455
|
:param new_content: Confluence Storage Format XHTML.
|
|
430
|
-
:param space_key: The Confluence space key (unless the default space is to be used).
|
|
431
456
|
:param title: New title to assign to the page. Needs to be unique within a space.
|
|
432
457
|
"""
|
|
433
458
|
|
|
434
|
-
page = self.get_page(page_id
|
|
459
|
+
page = self.get_page(page_id)
|
|
435
460
|
new_title = title or page.title
|
|
436
461
|
|
|
437
462
|
try:
|
|
@@ -442,18 +467,17 @@ class ConfluenceSession:
|
|
|
442
467
|
except ParseError as exc:
|
|
443
468
|
LOGGER.warning(exc)
|
|
444
469
|
|
|
445
|
-
path = f"/
|
|
470
|
+
path = f"/pages/{page_id}"
|
|
446
471
|
data = {
|
|
447
472
|
"id": page_id,
|
|
448
|
-
"
|
|
473
|
+
"status": "current",
|
|
449
474
|
"title": new_title,
|
|
450
|
-
"space": {"key": space_key or self.space_key},
|
|
451
475
|
"body": {"storage": {"value": new_content, "representation": "storage"}},
|
|
452
476
|
"version": {"minorEdit": True, "number": page.version + 1},
|
|
453
477
|
}
|
|
454
478
|
|
|
455
479
|
LOGGER.info("Updating page: %s", page_id)
|
|
456
|
-
self._save(path, data)
|
|
480
|
+
self._save(ConfluenceVersion.VERSION_2, path, data)
|
|
457
481
|
|
|
458
482
|
def create_page(
|
|
459
483
|
self,
|
|
@@ -463,18 +487,28 @@ class ConfluenceSession:
|
|
|
463
487
|
*,
|
|
464
488
|
space_key: Optional[str] = None,
|
|
465
489
|
) -> ConfluencePage:
|
|
466
|
-
|
|
490
|
+
"""
|
|
491
|
+
Create a new page via Confluence API.
|
|
492
|
+
"""
|
|
493
|
+
|
|
494
|
+
coalesced_space_key = space_key or self.space_key
|
|
495
|
+
if coalesced_space_key is None:
|
|
496
|
+
raise ConfluenceError(
|
|
497
|
+
"Confluence space key required for creating a new page"
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
path = "/pages/"
|
|
467
501
|
query = {
|
|
468
|
-
"
|
|
502
|
+
"spaceId": self.space_key_to_id(coalesced_space_key),
|
|
503
|
+
"status": "current",
|
|
469
504
|
"title": title,
|
|
470
|
-
"
|
|
505
|
+
"parentId": parent_page_id,
|
|
471
506
|
"body": {"storage": {"value": new_content, "representation": "storage"}},
|
|
472
|
-
"ancestors": [{"type": "page", "id": parent_page_id}],
|
|
473
507
|
}
|
|
474
508
|
|
|
475
509
|
LOGGER.info("Creating page: %s", title)
|
|
476
510
|
|
|
477
|
-
url = self._build_url(path)
|
|
511
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
478
512
|
response = self.session.post(
|
|
479
513
|
url,
|
|
480
514
|
data=json.dumps(query),
|
|
@@ -482,43 +516,66 @@ class ConfluenceSession:
|
|
|
482
516
|
)
|
|
483
517
|
response.raise_for_status()
|
|
484
518
|
|
|
485
|
-
data = typing.cast(
|
|
486
|
-
version = typing.cast(
|
|
487
|
-
body = typing.cast(
|
|
488
|
-
storage = typing.cast(
|
|
519
|
+
data = typing.cast(dict[str, JsonType], response.json())
|
|
520
|
+
version = typing.cast(dict[str, JsonType], data["version"])
|
|
521
|
+
body = typing.cast(dict[str, JsonType], data["body"])
|
|
522
|
+
storage = typing.cast(dict[str, JsonType], body["storage"])
|
|
489
523
|
|
|
490
524
|
return ConfluencePage(
|
|
491
525
|
id=typing.cast(str, data["id"]),
|
|
492
|
-
|
|
526
|
+
space_id=typing.cast(str, data["spaceId"]),
|
|
493
527
|
title=typing.cast(str, data["title"]),
|
|
494
528
|
version=typing.cast(int, version["number"]),
|
|
495
529
|
content=typing.cast(str, storage["value"]),
|
|
496
530
|
)
|
|
497
531
|
|
|
532
|
+
def delete_page(self, page_id: str, *, purge: bool = False) -> None:
|
|
533
|
+
"""
|
|
534
|
+
Delete a page via Confluence API.
|
|
535
|
+
|
|
536
|
+
:param page_id: The Confluence page ID.
|
|
537
|
+
:param purge: True to completely purge the page, False to move to trash only.
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
path = f"/pages/{page_id}"
|
|
541
|
+
|
|
542
|
+
# move to trash
|
|
543
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
544
|
+
LOGGER.info("Moving page to trash: %s", page_id)
|
|
545
|
+
response = self.session.delete(url)
|
|
546
|
+
response.raise_for_status()
|
|
547
|
+
|
|
548
|
+
if purge:
|
|
549
|
+
# purge from trash
|
|
550
|
+
query = {"purge": "true"}
|
|
551
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path, query)
|
|
552
|
+
LOGGER.info("Permanently deleting page: %s", page_id)
|
|
553
|
+
response = self.session.delete(url)
|
|
554
|
+
response.raise_for_status()
|
|
555
|
+
|
|
498
556
|
def page_exists(
|
|
499
557
|
self, title: str, *, space_key: Optional[str] = None
|
|
500
558
|
) -> Optional[str]:
|
|
501
|
-
path = "/
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
"
|
|
506
|
-
}
|
|
559
|
+
path = "/pages"
|
|
560
|
+
coalesced_space_key = space_key or self.space_key
|
|
561
|
+
query = {"title": title}
|
|
562
|
+
if coalesced_space_key is not None:
|
|
563
|
+
query["space-id"] = self.space_key_to_id(coalesced_space_key)
|
|
507
564
|
|
|
508
565
|
LOGGER.info("Checking if page exists with title: %s", title)
|
|
509
566
|
|
|
510
|
-
url = self._build_url(path)
|
|
567
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
511
568
|
response = self.session.get(
|
|
512
569
|
url, params=query, headers={"Content-Type": "application/json"}
|
|
513
570
|
)
|
|
514
571
|
response.raise_for_status()
|
|
515
572
|
|
|
516
|
-
data = typing.cast(
|
|
517
|
-
results = typing.cast(
|
|
573
|
+
data = typing.cast(dict[str, JsonType], response.json())
|
|
574
|
+
results = typing.cast(list[JsonType], data["results"])
|
|
518
575
|
|
|
519
576
|
if len(results) == 1:
|
|
520
|
-
|
|
521
|
-
return typing.cast(str,
|
|
577
|
+
result = typing.cast(dict[str, JsonType], results[0])
|
|
578
|
+
return typing.cast(str, result["id"])
|
|
522
579
|
else:
|
|
523
580
|
return None
|
|
524
581
|
|