markdown-to-confluence 0.3.4__py3-none-any.whl → 0.4.0__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.4.dist-info → markdown_to_confluence-0.4.0.dist-info}/METADATA +131 -14
- markdown_to_confluence-0.4.0.dist-info/RECORD +25 -0
- {markdown_to_confluence-0.3.4.dist-info → markdown_to_confluence-0.4.0.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +18 -7
- md2conf/api.py +492 -187
- md2conf/application.py +100 -83
- md2conf/collection.py +31 -0
- md2conf/converter.py +51 -112
- md2conf/emoji.py +28 -3
- md2conf/extra.py +14 -0
- md2conf/local.py +33 -45
- md2conf/matcher.py +54 -13
- md2conf/mermaid.py +10 -4
- md2conf/metadata.py +1 -3
- md2conf/processor.py +137 -43
- md2conf/properties.py +24 -5
- md2conf/scanner.py +149 -0
- markdown_to_confluence-0.3.4.dist-info/RECORD +0 -22
- {markdown_to_confluence-0.3.4.dist-info → markdown_to_confluence-0.4.0.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.3.4.dist-info → markdown_to_confluence-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.3.4.dist-info → markdown_to_confluence-0.4.0.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.3.4.dist-info → markdown_to_confluence-0.4.0.dist-info}/zip-safe +0 -0
md2conf/api.py
CHANGED
|
@@ -6,20 +6,27 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import datetime
|
|
9
10
|
import enum
|
|
10
11
|
import functools
|
|
11
12
|
import io
|
|
12
|
-
import json
|
|
13
13
|
import logging
|
|
14
14
|
import mimetypes
|
|
15
15
|
import typing
|
|
16
16
|
from dataclasses import dataclass
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from types import TracebackType
|
|
19
|
-
from typing import Optional,
|
|
19
|
+
from typing import Optional, TypeVar
|
|
20
20
|
from urllib.parse import urlencode, urlparse, urlunparse
|
|
21
21
|
|
|
22
22
|
import requests
|
|
23
|
+
from strong_typing.core import JsonType
|
|
24
|
+
from strong_typing.serialization import (
|
|
25
|
+
DeserializerOptions,
|
|
26
|
+
json_dump_string,
|
|
27
|
+
json_to_object,
|
|
28
|
+
object_to_json,
|
|
29
|
+
)
|
|
23
30
|
|
|
24
31
|
from .converter import ParseError, sanitize_confluence
|
|
25
32
|
from .metadata import ConfluenceSiteMetadata
|
|
@@ -30,26 +37,55 @@ from .properties import (
|
|
|
30
37
|
PageError,
|
|
31
38
|
)
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
T = TypeVar("T")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _json_to_object(
|
|
44
|
+
typ: type[T],
|
|
45
|
+
data: JsonType,
|
|
46
|
+
) -> T:
|
|
47
|
+
return json_to_object(typ, data, options=DeserializerOptions(skip_unassigned=True))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
|
|
51
|
+
"Builds a URL with scheme, host, port, path and query string parameters."
|
|
52
|
+
|
|
53
|
+
scheme, netloc, path, params, query_str, fragment = urlparse(base_url)
|
|
54
|
+
|
|
55
|
+
if params:
|
|
56
|
+
raise ValueError("expected: url with no parameters")
|
|
57
|
+
if query_str:
|
|
58
|
+
raise ValueError("expected: url with no query string")
|
|
59
|
+
if fragment:
|
|
60
|
+
raise ValueError("expected: url with no fragment")
|
|
61
|
+
|
|
62
|
+
url_parts = (scheme, netloc, path, None, urlencode(query) if query else None, None)
|
|
63
|
+
return urlunparse(url_parts)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
LOGGER = logging.getLogger(__name__)
|
|
43
67
|
|
|
44
68
|
|
|
69
|
+
@enum.unique
|
|
45
70
|
class ConfluenceVersion(enum.Enum):
|
|
71
|
+
"""
|
|
72
|
+
Confluence REST API version an HTTP request corresponds to.
|
|
73
|
+
|
|
74
|
+
For some operations, Confluence Cloud supports v2 endpoints exclusively. However, for other operations, only v1 endpoints are available via REST API.
|
|
75
|
+
Some versions of Confluence Server and Data Center, unfortunately, don't support v2 endpoints at all.
|
|
76
|
+
|
|
77
|
+
The principal use case for *md2conf* is Confluence Cloud. As such, *md2conf* uses v2 endpoints when available, and resorts to v1 endpoints only when
|
|
78
|
+
necessary.
|
|
79
|
+
"""
|
|
80
|
+
|
|
46
81
|
VERSION_1 = "rest/api"
|
|
47
82
|
VERSION_2 = "api/v2"
|
|
48
83
|
|
|
49
84
|
|
|
85
|
+
@enum.unique
|
|
50
86
|
class ConfluencePageParentContentType(enum.Enum):
|
|
51
87
|
"""
|
|
52
|
-
Content types that can be a parent to a Confluence page
|
|
88
|
+
Content types that can be a parent to a Confluence page.
|
|
53
89
|
"""
|
|
54
90
|
|
|
55
91
|
PAGE = "page"
|
|
@@ -59,49 +95,208 @@ class ConfluencePageParentContentType(enum.Enum):
|
|
|
59
95
|
FOLDER = "folder"
|
|
60
96
|
|
|
61
97
|
|
|
62
|
-
|
|
63
|
-
|
|
98
|
+
@enum.unique
|
|
99
|
+
class ConfluenceRepresentation(enum.Enum):
|
|
100
|
+
STORAGE = "storage"
|
|
101
|
+
ATLAS = "atlas_doc_format"
|
|
102
|
+
WIKI = "wiki"
|
|
64
103
|
|
|
65
|
-
scheme, netloc, path, params, query_str, fragment = urlparse(base_url)
|
|
66
104
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if fragment:
|
|
72
|
-
raise ValueError("expected: url with no fragment")
|
|
105
|
+
@enum.unique
|
|
106
|
+
class ConfluenceStatus(enum.Enum):
|
|
107
|
+
CURRENT = "current"
|
|
108
|
+
DRAFT = "draft"
|
|
73
109
|
|
|
74
|
-
url_parts = (scheme, netloc, path, None, urlencode(query) if query else None, None)
|
|
75
|
-
return urlunparse(url_parts)
|
|
76
110
|
|
|
111
|
+
@enum.unique
|
|
112
|
+
class ConfluenceLegacyType(enum.Enum):
|
|
113
|
+
ATTACHMENT = "attachment"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class ConfluenceLinks:
|
|
118
|
+
next: str
|
|
119
|
+
base: str
|
|
77
120
|
|
|
78
|
-
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True)
|
|
123
|
+
class ConfluenceResultSet:
|
|
124
|
+
results: list[JsonType]
|
|
125
|
+
_links: ConfluenceLinks
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True)
|
|
129
|
+
class ConfluenceContentVersion:
|
|
130
|
+
number: int
|
|
131
|
+
minorEdit: bool = False
|
|
132
|
+
createdAt: Optional[datetime.datetime] = None
|
|
133
|
+
message: Optional[str] = None
|
|
134
|
+
authorId: Optional[str] = None
|
|
79
135
|
|
|
80
136
|
|
|
81
137
|
@dataclass(frozen=True)
|
|
82
138
|
class ConfluenceAttachment:
|
|
139
|
+
"""
|
|
140
|
+
Holds data for an object uploaded to Confluence as a page attachment.
|
|
141
|
+
|
|
142
|
+
:param id: Unique ID for the attachment.
|
|
143
|
+
:param status: Attachment status.
|
|
144
|
+
:param title: Attachment title.
|
|
145
|
+
:param createdAt: Date and time when the attachment was created.
|
|
146
|
+
:param pageId: The Confluence page that the attachment is coupled with.
|
|
147
|
+
:param mediaType: MIME type for the attachment.
|
|
148
|
+
:param mediaTypeDescription: Media type description for the attachment.
|
|
149
|
+
:param comment: Description for the attachment.
|
|
150
|
+
:param fileId: File ID of the attachment, distinct from the attachment ID.
|
|
151
|
+
:param fileSize: Size in bytes.
|
|
152
|
+
:param webuiLink: WebUI link of the attachment.
|
|
153
|
+
:param downloadLink: Download link of the attachment.
|
|
154
|
+
:param version: Version information for the attachment.
|
|
155
|
+
"""
|
|
156
|
+
|
|
83
157
|
id: str
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
158
|
+
status: ConfluenceStatus
|
|
159
|
+
title: Optional[str]
|
|
160
|
+
createdAt: datetime.datetime
|
|
161
|
+
pageId: str
|
|
162
|
+
mediaType: str
|
|
163
|
+
mediaTypeDescription: str
|
|
164
|
+
comment: Optional[str]
|
|
165
|
+
fileId: str
|
|
166
|
+
fileSize: int
|
|
167
|
+
webuiLink: str
|
|
168
|
+
downloadLink: str
|
|
169
|
+
version: ConfluenceContentVersion
|
|
87
170
|
|
|
88
171
|
|
|
89
172
|
@dataclass(frozen=True)
|
|
90
|
-
class
|
|
173
|
+
class ConfluencePageProperties:
|
|
174
|
+
"""
|
|
175
|
+
Holds Confluence page properties used for page synchronization.
|
|
176
|
+
|
|
177
|
+
:param id: Confluence page ID.
|
|
178
|
+
:param status: Page status.
|
|
179
|
+
:param title: Page title.
|
|
180
|
+
:param spaceId: Confluence space ID.
|
|
181
|
+
:param parentId: Confluence page ID of the immediate parent.
|
|
182
|
+
:param parentType: Identifies the content type of the parent.
|
|
183
|
+
:param position: Position of child page within the given parent page tree.
|
|
184
|
+
:param authorId: The account ID of the user who created this page originally.
|
|
185
|
+
:param ownerId: The account ID of the user who owns this page.
|
|
186
|
+
:param lastOwnerId: The account ID of the user who owned this page previously, or `None` if there is no previous owner.
|
|
187
|
+
:param createdAt: Date and time when the page was created.
|
|
188
|
+
:param version: Page version. Incremented when the page is updated.
|
|
189
|
+
"""
|
|
190
|
+
|
|
91
191
|
id: str
|
|
92
|
-
|
|
93
|
-
parent_id: str
|
|
94
|
-
parent_type: Optional[ConfluencePageParentContentType]
|
|
192
|
+
status: ConfluenceStatus
|
|
95
193
|
title: str
|
|
96
|
-
|
|
194
|
+
spaceId: str
|
|
195
|
+
parentId: Optional[str]
|
|
196
|
+
parentType: Optional[ConfluencePageParentContentType]
|
|
197
|
+
position: Optional[int]
|
|
198
|
+
authorId: str
|
|
199
|
+
ownerId: str
|
|
200
|
+
lastOwnerId: Optional[str]
|
|
201
|
+
createdAt: datetime.datetime
|
|
202
|
+
version: ConfluenceContentVersion
|
|
97
203
|
|
|
98
204
|
|
|
99
205
|
@dataclass(frozen=True)
|
|
100
|
-
class
|
|
101
|
-
|
|
206
|
+
class ConfluencePageStorage:
|
|
207
|
+
"""
|
|
208
|
+
Holds Confluence page content.
|
|
209
|
+
|
|
210
|
+
:param representation: Type of content representation used (e.g. Confluence Storage Format).
|
|
211
|
+
:param value: Body of the content, in the format found in the representation field.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
representation: ConfluenceRepresentation
|
|
215
|
+
value: str
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass(frozen=True)
|
|
219
|
+
class ConfluencePageBody:
|
|
220
|
+
"""
|
|
221
|
+
Holds Confluence page content.
|
|
222
|
+
|
|
223
|
+
:param storage: Encapsulates content with meta-information about its representation.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
storage: ConfluencePageStorage
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass(frozen=True)
|
|
230
|
+
class ConfluencePage(ConfluencePageProperties):
|
|
231
|
+
"""
|
|
232
|
+
Holds Confluence page data used for page synchronization.
|
|
233
|
+
|
|
234
|
+
:param body: Page content.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
body: ConfluencePageBody
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def content(self) -> str:
|
|
241
|
+
return self.body.storage.value
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass(frozen=True, eq=True, order=True)
|
|
245
|
+
class ConfluenceLabel:
|
|
246
|
+
"""
|
|
247
|
+
Holds information about a single label.
|
|
248
|
+
|
|
249
|
+
:param name: Name of the label.
|
|
250
|
+
:param prefix: Prefix of the label.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
name: str
|
|
254
|
+
prefix: str
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass(frozen=True, eq=True, order=True)
|
|
258
|
+
class ConfluenceIdentifiedLabel(ConfluenceLabel):
|
|
259
|
+
"""
|
|
260
|
+
Holds information about a single label.
|
|
261
|
+
|
|
262
|
+
:param id: ID of the label.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
id: str
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@dataclass(frozen=True)
|
|
269
|
+
class ConfluenceCreatePageRequest:
|
|
270
|
+
spaceId: str
|
|
271
|
+
status: Optional[ConfluenceStatus]
|
|
272
|
+
title: Optional[str]
|
|
273
|
+
parentId: Optional[str]
|
|
274
|
+
body: ConfluencePageBody
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@dataclass(frozen=True)
|
|
278
|
+
class ConfluenceUpdatePageRequest:
|
|
279
|
+
id: str
|
|
280
|
+
status: ConfluenceStatus
|
|
281
|
+
title: str
|
|
282
|
+
body: ConfluencePageBody
|
|
283
|
+
version: ConfluenceContentVersion
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@dataclass(frozen=True)
|
|
287
|
+
class ConfluenceUpdateAttachmentRequest:
|
|
288
|
+
id: str
|
|
289
|
+
type: ConfluenceLegacyType
|
|
290
|
+
status: ConfluenceStatus
|
|
291
|
+
title: str
|
|
292
|
+
version: ConfluenceContentVersion
|
|
102
293
|
|
|
103
294
|
|
|
104
295
|
class ConfluenceAPI:
|
|
296
|
+
"""
|
|
297
|
+
Represents an active connection to a Confluence server.
|
|
298
|
+
"""
|
|
299
|
+
|
|
105
300
|
properties: ConfluenceConnectionProperties
|
|
106
301
|
session: Optional["ConfluenceSession"] = None
|
|
107
302
|
|
|
@@ -124,9 +319,10 @@ class ConfluenceAPI:
|
|
|
124
319
|
|
|
125
320
|
self.session = ConfluenceSession(
|
|
126
321
|
session,
|
|
127
|
-
self.properties.
|
|
128
|
-
self.properties.
|
|
129
|
-
self.properties.
|
|
322
|
+
api_url=self.properties.api_url,
|
|
323
|
+
domain=self.properties.domain,
|
|
324
|
+
base_path=self.properties.base_path,
|
|
325
|
+
space_key=self.properties.space_key,
|
|
130
326
|
)
|
|
131
327
|
return self.session
|
|
132
328
|
|
|
@@ -147,6 +343,7 @@ class ConfluenceSession:
|
|
|
147
343
|
"""
|
|
148
344
|
|
|
149
345
|
session: requests.Session
|
|
346
|
+
api_url: str
|
|
150
347
|
site: ConfluenceSiteMetadata
|
|
151
348
|
|
|
152
349
|
_space_id_to_key: dict[str, str]
|
|
@@ -155,16 +352,42 @@ class ConfluenceSession:
|
|
|
155
352
|
def __init__(
|
|
156
353
|
self,
|
|
157
354
|
session: requests.Session,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
355
|
+
*,
|
|
356
|
+
api_url: Optional[str],
|
|
357
|
+
domain: Optional[str],
|
|
358
|
+
base_path: Optional[str],
|
|
359
|
+
space_key: Optional[str],
|
|
161
360
|
) -> None:
|
|
162
361
|
self.session = session
|
|
163
|
-
self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
|
|
164
|
-
|
|
165
362
|
self._space_id_to_key = {}
|
|
166
363
|
self._space_key_to_id = {}
|
|
167
364
|
|
|
365
|
+
if api_url:
|
|
366
|
+
self.api_url = api_url
|
|
367
|
+
|
|
368
|
+
if not domain or not base_path:
|
|
369
|
+
payload = self._invoke(
|
|
370
|
+
ConfluenceVersion.VERSION_2, "/spaces", {"limit": "1"}
|
|
371
|
+
)
|
|
372
|
+
data = json_to_object(ConfluenceResultSet, payload)
|
|
373
|
+
base_url = data._links.base
|
|
374
|
+
|
|
375
|
+
_, domain, base_path, _, _, _ = urlparse(base_url)
|
|
376
|
+
if not base_path.endswith("/"):
|
|
377
|
+
base_path = f"{base_path}/"
|
|
378
|
+
|
|
379
|
+
if not domain:
|
|
380
|
+
raise ArgumentError(
|
|
381
|
+
"Confluence domain not specified and cannot be inferred"
|
|
382
|
+
)
|
|
383
|
+
if not base_path:
|
|
384
|
+
raise ArgumentError(
|
|
385
|
+
"Confluence base path not specified and cannot be inferred"
|
|
386
|
+
)
|
|
387
|
+
self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
|
|
388
|
+
if not api_url:
|
|
389
|
+
self.api_url = f"https://{self.site.domain}{self.site.base_path}"
|
|
390
|
+
|
|
168
391
|
def close(self) -> None:
|
|
169
392
|
self.session.close()
|
|
170
393
|
self.session = requests.Session()
|
|
@@ -184,9 +407,7 @@ class ConfluenceSession:
|
|
|
184
407
|
:returns: A full URL.
|
|
185
408
|
"""
|
|
186
409
|
|
|
187
|
-
base_url =
|
|
188
|
-
f"https://{self.site.domain}{self.site.base_path}{version.value}{path}"
|
|
189
|
-
)
|
|
410
|
+
base_url = f"{self.api_url}{version.value}{path}"
|
|
190
411
|
return build_url(base_url, query)
|
|
191
412
|
|
|
192
413
|
def _invoke(
|
|
@@ -195,20 +416,46 @@ class ConfluenceSession:
|
|
|
195
416
|
path: str,
|
|
196
417
|
query: Optional[dict[str, str]] = None,
|
|
197
418
|
) -> JsonType:
|
|
198
|
-
"
|
|
419
|
+
"Executes an HTTP request via Confluence API."
|
|
199
420
|
|
|
200
421
|
url = self._build_url(version, path, query)
|
|
201
|
-
response = self.session.get(url)
|
|
422
|
+
response = self.session.get(url, headers={"Accept": "application/json"})
|
|
202
423
|
if response.text:
|
|
203
424
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
204
425
|
response.raise_for_status()
|
|
205
|
-
return response.json()
|
|
426
|
+
return typing.cast(JsonType, response.json())
|
|
427
|
+
|
|
428
|
+
def _fetch(
|
|
429
|
+
self, path: str, query: Optional[dict[str, str]] = None
|
|
430
|
+
) -> list[JsonType]:
|
|
431
|
+
"Retrieves all results of a REST API v2 paginated result-set."
|
|
432
|
+
|
|
433
|
+
items: list[JsonType] = []
|
|
434
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path, query)
|
|
435
|
+
while True:
|
|
436
|
+
response = self.session.get(url, headers={"Accept": "application/json"})
|
|
437
|
+
response.raise_for_status()
|
|
438
|
+
|
|
439
|
+
payload = typing.cast(dict[str, JsonType], response.json())
|
|
440
|
+
results = typing.cast(list[JsonType], payload["results"])
|
|
441
|
+
items.extend(results)
|
|
442
|
+
|
|
443
|
+
links = typing.cast(dict[str, JsonType], payload.get("_links", {}))
|
|
444
|
+
link = typing.cast(str, links.get("next", ""))
|
|
445
|
+
if link:
|
|
446
|
+
url = f"https://{self.site.domain}{link}"
|
|
447
|
+
else:
|
|
448
|
+
break
|
|
449
|
+
|
|
450
|
+
return items
|
|
451
|
+
|
|
452
|
+
def _save(self, version: ConfluenceVersion, path: str, data: JsonType) -> None:
|
|
453
|
+
"Persists data via Confluence REST API."
|
|
206
454
|
|
|
207
|
-
def _save(self, version: ConfluenceVersion, path: str, data: dict) -> None:
|
|
208
455
|
url = self._build_url(version, path)
|
|
209
456
|
response = self.session.put(
|
|
210
457
|
url,
|
|
211
|
-
data=
|
|
458
|
+
data=json_dump_string(data),
|
|
212
459
|
headers={"Content-Type": "application/json"},
|
|
213
460
|
)
|
|
214
461
|
if response.text:
|
|
@@ -225,8 +472,8 @@ class ConfluenceSession:
|
|
|
225
472
|
"/spaces",
|
|
226
473
|
{"ids": id, "status": "current"},
|
|
227
474
|
)
|
|
228
|
-
|
|
229
|
-
results = typing.cast(list[JsonType],
|
|
475
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
476
|
+
results = typing.cast(list[JsonType], data["results"])
|
|
230
477
|
if len(results) != 1:
|
|
231
478
|
raise ConfluenceError(f"unique space not found with id: {id}")
|
|
232
479
|
|
|
@@ -247,8 +494,8 @@ class ConfluenceSession:
|
|
|
247
494
|
"/spaces",
|
|
248
495
|
{"keys": key, "status": "current"},
|
|
249
496
|
)
|
|
250
|
-
|
|
251
|
-
results = typing.cast(list[JsonType],
|
|
497
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
498
|
+
results = typing.cast(list[JsonType], data["results"])
|
|
252
499
|
if len(results) != 1:
|
|
253
500
|
raise ConfluenceError(f"unique space not found with key: {key}")
|
|
254
501
|
|
|
@@ -263,7 +510,7 @@ class ConfluenceSession:
|
|
|
263
510
|
self, *, space_id: Optional[str] = None, space_key: Optional[str] = None
|
|
264
511
|
) -> Optional[str]:
|
|
265
512
|
"""
|
|
266
|
-
|
|
513
|
+
Coalesces a space ID or space key into a space ID, accounting for site default.
|
|
267
514
|
|
|
268
515
|
:param space_id: A Confluence space ID.
|
|
269
516
|
:param space_key: A Confluence space key.
|
|
@@ -285,22 +532,20 @@ class ConfluenceSession:
|
|
|
285
532
|
def get_attachment_by_name(
|
|
286
533
|
self, page_id: str, filename: str
|
|
287
534
|
) -> ConfluenceAttachment:
|
|
535
|
+
"""
|
|
536
|
+
Retrieves a Confluence page attachment by an unprefixed file name.
|
|
537
|
+
"""
|
|
538
|
+
|
|
288
539
|
path = f"/pages/{page_id}/attachments"
|
|
289
540
|
query = {"filename": filename}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
)
|
|
541
|
+
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
542
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
293
543
|
|
|
294
544
|
results = typing.cast(list[JsonType], data["results"])
|
|
295
545
|
if len(results) != 1:
|
|
296
546
|
raise ConfluenceError(f"no such attachment on page {page_id}: {filename}")
|
|
297
547
|
result = typing.cast(dict[str, JsonType], results[0])
|
|
298
|
-
|
|
299
|
-
id = typing.cast(str, result["id"])
|
|
300
|
-
media_type = typing.cast(str, result["mediaType"])
|
|
301
|
-
file_size = typing.cast(int, result["fileSize"])
|
|
302
|
-
comment = typing.cast(str, result.get("comment", ""))
|
|
303
|
-
return ConfluenceAttachment(id, media_type, file_size, comment)
|
|
548
|
+
return _json_to_object(ConfluenceAttachment, result)
|
|
304
549
|
|
|
305
550
|
def upload_attachment(
|
|
306
551
|
self,
|
|
@@ -313,6 +558,18 @@ class ConfluenceSession:
|
|
|
313
558
|
comment: Optional[str] = None,
|
|
314
559
|
force: bool = False,
|
|
315
560
|
) -> None:
|
|
561
|
+
"""
|
|
562
|
+
Uploads a new attachment to a Confluence page.
|
|
563
|
+
|
|
564
|
+
:param page_id: Confluence page ID.
|
|
565
|
+
:param attachment_name: Unprefixed name unique to the page.
|
|
566
|
+
:param attachment_path: Path to the file to upload as an attachment.
|
|
567
|
+
:param raw_data: Raw data to upload as an attachment.
|
|
568
|
+
:param content_type: Attachment MIME type.
|
|
569
|
+
:param comment: Attachment description.
|
|
570
|
+
:param force: Overwrite an existing attachment even if there seem to be no changes.
|
|
571
|
+
"""
|
|
572
|
+
|
|
316
573
|
if attachment_path is None and raw_data is None:
|
|
317
574
|
raise ArgumentError("required: `attachment_path` or `raw_data`")
|
|
318
575
|
|
|
@@ -333,15 +590,15 @@ class ConfluenceSession:
|
|
|
333
590
|
attachment = self.get_attachment_by_name(page_id, attachment_name)
|
|
334
591
|
|
|
335
592
|
if attachment_path is not None:
|
|
336
|
-
if not force and attachment.
|
|
593
|
+
if not force and attachment.fileSize == attachment_path.stat().st_size:
|
|
337
594
|
LOGGER.info("Up-to-date attachment: %s", attachment_name)
|
|
338
595
|
return
|
|
339
596
|
elif raw_data is not None:
|
|
340
|
-
if not force and attachment.
|
|
597
|
+
if not force and attachment.fileSize == len(raw_data):
|
|
341
598
|
LOGGER.info("Up-to-date embedded image: %s", attachment_name)
|
|
342
599
|
return
|
|
343
600
|
else:
|
|
344
|
-
raise NotImplementedError("
|
|
601
|
+
raise NotImplementedError("parameter match not exhaustive")
|
|
345
602
|
|
|
346
603
|
id = attachment.id.removeprefix("att")
|
|
347
604
|
path = f"/content/{page_id}/child/attachment/{id}/data"
|
|
@@ -365,8 +622,11 @@ class ConfluenceSession:
|
|
|
365
622
|
LOGGER.info("Uploading attachment: %s", attachment_name)
|
|
366
623
|
response = self.session.post(
|
|
367
624
|
url,
|
|
368
|
-
files=file_to_upload, # type: ignore
|
|
369
|
-
headers={
|
|
625
|
+
files=file_to_upload, # type: ignore[arg-type]
|
|
626
|
+
headers={
|
|
627
|
+
"X-Atlassian-Token": "no-check",
|
|
628
|
+
"Accept": "application/json",
|
|
629
|
+
},
|
|
370
630
|
)
|
|
371
631
|
elif raw_data is not None:
|
|
372
632
|
LOGGER.info("Uploading raw data: %s", attachment_name)
|
|
@@ -377,18 +637,21 @@ class ConfluenceSession:
|
|
|
377
637
|
"comment": comment,
|
|
378
638
|
"file": (
|
|
379
639
|
attachment_name, # will truncate path component
|
|
380
|
-
raw_file, # type: ignore
|
|
640
|
+
raw_file, # type: ignore[dict-item]
|
|
381
641
|
content_type,
|
|
382
642
|
{"Expires": "0"},
|
|
383
643
|
),
|
|
384
644
|
}
|
|
385
645
|
response = self.session.post(
|
|
386
646
|
url,
|
|
387
|
-
files=file_to_upload, # type: ignore
|
|
388
|
-
headers={
|
|
647
|
+
files=file_to_upload, # type: ignore[arg-type]
|
|
648
|
+
headers={
|
|
649
|
+
"X-Atlassian-Token": "no-check",
|
|
650
|
+
"Accept": "application/json",
|
|
651
|
+
},
|
|
389
652
|
)
|
|
390
653
|
else:
|
|
391
|
-
raise NotImplementedError("
|
|
654
|
+
raise NotImplementedError("parameter match not exhaustive")
|
|
392
655
|
|
|
393
656
|
response.raise_for_status()
|
|
394
657
|
data = response.json()
|
|
@@ -409,29 +672,30 @@ class ConfluenceSession:
|
|
|
409
672
|
) -> None:
|
|
410
673
|
id = attachment_id.removeprefix("att")
|
|
411
674
|
path = f"/content/{page_id}/child/attachment/{id}"
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
675
|
+
request = ConfluenceUpdateAttachmentRequest(
|
|
676
|
+
id=attachment_id,
|
|
677
|
+
type=ConfluenceLegacyType.ATTACHMENT,
|
|
678
|
+
status=ConfluenceStatus.CURRENT,
|
|
679
|
+
title=attachment_title,
|
|
680
|
+
version=ConfluenceContentVersion(number=version, minorEdit=True),
|
|
681
|
+
)
|
|
419
682
|
|
|
420
683
|
LOGGER.info("Updating attachment: %s", attachment_id)
|
|
421
|
-
self._save(ConfluenceVersion.VERSION_1, path,
|
|
684
|
+
self._save(ConfluenceVersion.VERSION_1, path, object_to_json(request))
|
|
422
685
|
|
|
423
|
-
def
|
|
686
|
+
def get_page_properties_by_title(
|
|
424
687
|
self,
|
|
425
688
|
title: str,
|
|
426
689
|
*,
|
|
427
690
|
space_id: Optional[str] = None,
|
|
428
691
|
space_key: Optional[str] = None,
|
|
429
|
-
) ->
|
|
692
|
+
) -> ConfluencePageProperties:
|
|
430
693
|
"""
|
|
431
|
-
|
|
694
|
+
Looks up a Confluence wiki page ID by title.
|
|
432
695
|
|
|
433
696
|
:param title: The page title.
|
|
434
|
-
:param
|
|
697
|
+
:param space_id: The Confluence space ID (unless the default space is to be used). Exclusive with space key.
|
|
698
|
+
:param space_key: The Confluence space key (unless the default space is to be used). Exclusive with space ID.
|
|
435
699
|
:returns: Confluence page ID.
|
|
436
700
|
"""
|
|
437
701
|
|
|
@@ -445,19 +709,17 @@ class ConfluenceSession:
|
|
|
445
709
|
query["space-id"] = space_id
|
|
446
710
|
|
|
447
711
|
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
results = typing.cast(list[JsonType], payload["results"])
|
|
712
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
713
|
+
results = typing.cast(list[JsonType], data["results"])
|
|
451
714
|
if len(results) != 1:
|
|
452
715
|
raise ConfluenceError(f"unique page not found with title: {title}")
|
|
453
716
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
return id
|
|
717
|
+
page = _json_to_object(ConfluencePageProperties, results[0])
|
|
718
|
+
return page
|
|
457
719
|
|
|
458
720
|
def get_page(self, page_id: str) -> ConfluencePage:
|
|
459
721
|
"""
|
|
460
|
-
|
|
722
|
+
Retrieves Confluence wiki page details and content.
|
|
461
723
|
|
|
462
724
|
:param page_id: The Confluence page ID.
|
|
463
725
|
:returns: Confluence page info and content.
|
|
@@ -466,29 +728,12 @@ class ConfluenceSession:
|
|
|
466
728
|
path = f"/pages/{page_id}"
|
|
467
729
|
query = {"body-format": "storage"}
|
|
468
730
|
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
469
|
-
|
|
470
|
-
version = typing.cast(dict[str, JsonType], data["version"])
|
|
471
|
-
body = typing.cast(dict[str, JsonType], data["body"])
|
|
472
|
-
storage = typing.cast(dict[str, JsonType], body["storage"])
|
|
473
|
-
|
|
474
|
-
return ConfluencePage(
|
|
475
|
-
id=page_id,
|
|
476
|
-
space_id=typing.cast(str, data["spaceId"]),
|
|
477
|
-
parent_id=typing.cast(str, data["parentId"]),
|
|
478
|
-
parent_type=(
|
|
479
|
-
ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
|
|
480
|
-
if data["parentType"] is not None
|
|
481
|
-
else None
|
|
482
|
-
),
|
|
483
|
-
title=typing.cast(str, data["title"]),
|
|
484
|
-
version=typing.cast(int, version["number"]),
|
|
485
|
-
content=typing.cast(str, storage["value"]),
|
|
486
|
-
)
|
|
731
|
+
return _json_to_object(ConfluencePage, payload)
|
|
487
732
|
|
|
488
733
|
@functools.cache
|
|
489
|
-
def
|
|
734
|
+
def get_page_properties(self, page_id: str) -> ConfluencePageProperties:
|
|
490
735
|
"""
|
|
491
|
-
|
|
736
|
+
Retrieves Confluence wiki page details.
|
|
492
737
|
|
|
493
738
|
:param page_id: The Confluence page ID.
|
|
494
739
|
:returns: Confluence page info.
|
|
@@ -496,35 +741,17 @@ class ConfluenceSession:
|
|
|
496
741
|
|
|
497
742
|
path = f"/pages/{page_id}"
|
|
498
743
|
payload = self._invoke(ConfluenceVersion.VERSION_2, path)
|
|
499
|
-
|
|
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
|
-
)
|
|
744
|
+
return _json_to_object(ConfluencePageProperties, payload)
|
|
514
745
|
|
|
515
746
|
def get_page_version(self, page_id: str) -> int:
|
|
516
747
|
"""
|
|
517
|
-
|
|
748
|
+
Retrieves a Confluence wiki page version.
|
|
518
749
|
|
|
519
750
|
:param page_id: The Confluence page ID.
|
|
520
751
|
:returns: Confluence page version.
|
|
521
752
|
"""
|
|
522
753
|
|
|
523
|
-
|
|
524
|
-
payload = self._invoke(ConfluenceVersion.VERSION_2, path)
|
|
525
|
-
data = typing.cast(dict[str, JsonType], payload)
|
|
526
|
-
version = typing.cast(dict[str, JsonType], data["version"])
|
|
527
|
-
return typing.cast(int, version["number"])
|
|
754
|
+
return self.get_page_properties(page_id).version.number
|
|
528
755
|
|
|
529
756
|
def update_page(
|
|
530
757
|
self,
|
|
@@ -534,7 +761,7 @@ class ConfluenceSession:
|
|
|
534
761
|
title: Optional[str] = None,
|
|
535
762
|
) -> None:
|
|
536
763
|
"""
|
|
537
|
-
|
|
764
|
+
Updates a page via the Confluence API.
|
|
538
765
|
|
|
539
766
|
:param page_id: The Confluence page ID.
|
|
540
767
|
:param new_content: Confluence Storage Format XHTML.
|
|
@@ -553,16 +780,21 @@ class ConfluenceSession:
|
|
|
553
780
|
LOGGER.warning(exc)
|
|
554
781
|
|
|
555
782
|
path = f"/pages/{page_id}"
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
783
|
+
request = ConfluenceUpdatePageRequest(
|
|
784
|
+
id=page_id,
|
|
785
|
+
status=ConfluenceStatus.CURRENT,
|
|
786
|
+
title=new_title,
|
|
787
|
+
body=ConfluencePageBody(
|
|
788
|
+
storage=ConfluencePageStorage(
|
|
789
|
+
representation=ConfluenceRepresentation.STORAGE, value=new_content
|
|
790
|
+
)
|
|
791
|
+
),
|
|
792
|
+
version=ConfluenceContentVersion(
|
|
793
|
+
number=page.version.number + 1, minorEdit=True
|
|
794
|
+
),
|
|
795
|
+
)
|
|
564
796
|
LOGGER.info("Updating page: %s", page_id)
|
|
565
|
-
self._save(ConfluenceVersion.VERSION_2, path,
|
|
797
|
+
self._save(ConfluenceVersion.VERSION_2, path, object_to_json(request))
|
|
566
798
|
|
|
567
799
|
def create_page(
|
|
568
800
|
self,
|
|
@@ -571,54 +803,45 @@ class ConfluenceSession:
|
|
|
571
803
|
new_content: str,
|
|
572
804
|
) -> ConfluencePage:
|
|
573
805
|
"""
|
|
574
|
-
|
|
806
|
+
Creates a new page via Confluence API.
|
|
575
807
|
"""
|
|
576
808
|
|
|
577
|
-
parent_page = self.get_page_metadata(parent_id)
|
|
578
|
-
path = "/pages/"
|
|
579
|
-
query = {
|
|
580
|
-
"spaceId": parent_page.space_id,
|
|
581
|
-
"status": "current",
|
|
582
|
-
"title": title,
|
|
583
|
-
"parentId": parent_id,
|
|
584
|
-
"body": {"storage": {"value": new_content, "representation": "storage"}},
|
|
585
|
-
}
|
|
586
|
-
|
|
587
809
|
LOGGER.info("Creating page: %s", title)
|
|
588
810
|
|
|
811
|
+
parent_page = self.get_page_properties(parent_id)
|
|
812
|
+
|
|
813
|
+
path = "/pages/"
|
|
814
|
+
request = ConfluenceCreatePageRequest(
|
|
815
|
+
spaceId=parent_page.spaceId,
|
|
816
|
+
status=ConfluenceStatus.CURRENT,
|
|
817
|
+
title=title,
|
|
818
|
+
parentId=parent_id,
|
|
819
|
+
body=ConfluencePageBody(
|
|
820
|
+
storage=ConfluencePageStorage(
|
|
821
|
+
representation=ConfluenceRepresentation.STORAGE,
|
|
822
|
+
value=new_content,
|
|
823
|
+
)
|
|
824
|
+
),
|
|
825
|
+
)
|
|
826
|
+
|
|
589
827
|
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
590
828
|
response = self.session.post(
|
|
591
829
|
url,
|
|
592
|
-
data=
|
|
593
|
-
headers={
|
|
830
|
+
data=json_dump_string(object_to_json(request)),
|
|
831
|
+
headers={
|
|
832
|
+
"Content-Type": "application/json",
|
|
833
|
+
"Accept": "application/json",
|
|
834
|
+
},
|
|
594
835
|
)
|
|
595
836
|
response.raise_for_status()
|
|
596
|
-
|
|
597
|
-
data = typing.cast(dict[str, JsonType], response.json())
|
|
598
|
-
version = typing.cast(dict[str, JsonType], data["version"])
|
|
599
|
-
body = typing.cast(dict[str, JsonType], data["body"])
|
|
600
|
-
storage = typing.cast(dict[str, JsonType], body["storage"])
|
|
601
|
-
|
|
602
|
-
return ConfluencePage(
|
|
603
|
-
id=typing.cast(str, data["id"]),
|
|
604
|
-
space_id=typing.cast(str, data["spaceId"]),
|
|
605
|
-
parent_id=typing.cast(str, data["parentId"]),
|
|
606
|
-
parent_type=(
|
|
607
|
-
ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
|
|
608
|
-
if data["parentType"] is not None
|
|
609
|
-
else None
|
|
610
|
-
),
|
|
611
|
-
title=typing.cast(str, data["title"]),
|
|
612
|
-
version=typing.cast(int, version["number"]),
|
|
613
|
-
content=typing.cast(str, storage["value"]),
|
|
614
|
-
)
|
|
837
|
+
return _json_to_object(ConfluencePage, response.json())
|
|
615
838
|
|
|
616
839
|
def delete_page(self, page_id: str, *, purge: bool = False) -> None:
|
|
617
840
|
"""
|
|
618
|
-
|
|
841
|
+
Deletes a page via Confluence API.
|
|
619
842
|
|
|
620
843
|
:param page_id: The Confluence page ID.
|
|
621
|
-
:param purge: True to completely purge the page, False to move to trash only.
|
|
844
|
+
:param purge: `True` to completely purge the page, `False` to move to trash only.
|
|
622
845
|
"""
|
|
623
846
|
|
|
624
847
|
path = f"/pages/{page_id}"
|
|
@@ -645,10 +868,12 @@ class ConfluenceSession:
|
|
|
645
868
|
space_key: Optional[str] = None,
|
|
646
869
|
) -> Optional[str]:
|
|
647
870
|
"""
|
|
648
|
-
|
|
871
|
+
Checks if a Confluence page exists with the given title.
|
|
649
872
|
|
|
650
873
|
:param title: Page title. Pages in the same Confluence space must have a unique title.
|
|
651
874
|
:param space_key: Identifies the Confluence space.
|
|
875
|
+
|
|
876
|
+
:returns: Confluence page ID of a matching page (if found), or `None`.
|
|
652
877
|
"""
|
|
653
878
|
|
|
654
879
|
space_id = self.get_space_id(space_id=space_id, space_key=space_key)
|
|
@@ -661,29 +886,32 @@ class ConfluenceSession:
|
|
|
661
886
|
|
|
662
887
|
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
663
888
|
response = self.session.get(
|
|
664
|
-
url,
|
|
889
|
+
url,
|
|
890
|
+
params=query,
|
|
891
|
+
headers={
|
|
892
|
+
"Content-Type": "application/json",
|
|
893
|
+
"Accept": "application/json",
|
|
894
|
+
},
|
|
665
895
|
)
|
|
666
896
|
response.raise_for_status()
|
|
667
|
-
|
|
668
897
|
data = typing.cast(dict[str, JsonType], response.json())
|
|
669
|
-
results =
|
|
898
|
+
results = _json_to_object(list[ConfluencePageProperties], data["results"])
|
|
670
899
|
|
|
671
900
|
if len(results) == 1:
|
|
672
|
-
|
|
673
|
-
return typing.cast(str, result["id"])
|
|
901
|
+
return results[0].id
|
|
674
902
|
else:
|
|
675
903
|
return None
|
|
676
904
|
|
|
677
905
|
def get_or_create_page(self, title: str, parent_id: str) -> ConfluencePage:
|
|
678
906
|
"""
|
|
679
|
-
|
|
907
|
+
Finds a page with the given title, or creates a new page if no such page exists.
|
|
680
908
|
|
|
681
909
|
:param title: Page title. Pages in the same Confluence space must have a unique title.
|
|
682
910
|
:param parent_id: Identifies the parent page for a new child page.
|
|
683
911
|
"""
|
|
684
912
|
|
|
685
|
-
parent_page = self.
|
|
686
|
-
page_id = self.page_exists(title, space_id=parent_page.
|
|
913
|
+
parent_page = self.get_page_properties(parent_id)
|
|
914
|
+
page_id = self.page_exists(title, space_id=parent_page.spaceId)
|
|
687
915
|
|
|
688
916
|
if page_id is not None:
|
|
689
917
|
LOGGER.debug("Retrieving existing page: %s", page_id)
|
|
@@ -691,3 +919,80 @@ class ConfluenceSession:
|
|
|
691
919
|
else:
|
|
692
920
|
LOGGER.debug("Creating new page with title: %s", title)
|
|
693
921
|
return self.create_page(parent_id, title, "")
|
|
922
|
+
|
|
923
|
+
def get_labels(self, page_id: str) -> list[ConfluenceIdentifiedLabel]:
|
|
924
|
+
"""
|
|
925
|
+
Retrieves labels for a Confluence page.
|
|
926
|
+
|
|
927
|
+
:param page_id: The Confluence page ID.
|
|
928
|
+
:returns: A list of page labels.
|
|
929
|
+
"""
|
|
930
|
+
|
|
931
|
+
path = f"/pages/{page_id}/labels"
|
|
932
|
+
results = self._fetch(path)
|
|
933
|
+
return _json_to_object(list[ConfluenceIdentifiedLabel], results)
|
|
934
|
+
|
|
935
|
+
def add_labels(self, page_id: str, labels: list[ConfluenceLabel]) -> None:
|
|
936
|
+
"""
|
|
937
|
+
Adds labels to a Confluence page.
|
|
938
|
+
|
|
939
|
+
:param page_id: The Confluence page ID.
|
|
940
|
+
:param labels: A list of page labels to add.
|
|
941
|
+
"""
|
|
942
|
+
|
|
943
|
+
path = f"/content/{page_id}/label"
|
|
944
|
+
|
|
945
|
+
url = self._build_url(ConfluenceVersion.VERSION_1, path)
|
|
946
|
+
response = self.session.post(
|
|
947
|
+
url,
|
|
948
|
+
data=json_dump_string(object_to_json(labels)),
|
|
949
|
+
headers={
|
|
950
|
+
"Content-Type": "application/json",
|
|
951
|
+
"Accept": "application/json",
|
|
952
|
+
},
|
|
953
|
+
)
|
|
954
|
+
if response.text:
|
|
955
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
956
|
+
response.raise_for_status()
|
|
957
|
+
|
|
958
|
+
def remove_labels(self, page_id: str, labels: list[ConfluenceLabel]) -> None:
|
|
959
|
+
"""
|
|
960
|
+
Removes labels from a Confluence page.
|
|
961
|
+
|
|
962
|
+
:param page_id: The Confluence page ID.
|
|
963
|
+
:param labels: A list of page labels to remove.
|
|
964
|
+
"""
|
|
965
|
+
|
|
966
|
+
path = f"/content/{page_id}/label"
|
|
967
|
+
for label in labels:
|
|
968
|
+
query = {"name": label.name}
|
|
969
|
+
|
|
970
|
+
url = self._build_url(ConfluenceVersion.VERSION_1, path, query)
|
|
971
|
+
response = self.session.delete(url)
|
|
972
|
+
if response.text:
|
|
973
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
974
|
+
response.raise_for_status()
|
|
975
|
+
|
|
976
|
+
def update_labels(self, page_id: str, labels: list[ConfluenceLabel]) -> None:
|
|
977
|
+
"""
|
|
978
|
+
Assigns the specified labels to a Confluence page. Existing labels are removed.
|
|
979
|
+
|
|
980
|
+
:param page_id: The Confluence page ID.
|
|
981
|
+
:param labels: A list of page labels to assign.
|
|
982
|
+
"""
|
|
983
|
+
|
|
984
|
+
new_labels = set(labels)
|
|
985
|
+
old_labels = set(
|
|
986
|
+
ConfluenceLabel(name=label.name, prefix=label.prefix)
|
|
987
|
+
for label in self.get_labels(page_id)
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
add_labels = list(new_labels - old_labels)
|
|
991
|
+
remove_labels = list(old_labels - new_labels)
|
|
992
|
+
|
|
993
|
+
if add_labels:
|
|
994
|
+
add_labels.sort()
|
|
995
|
+
self.add_labels(page_id, add_labels)
|
|
996
|
+
if remove_labels:
|
|
997
|
+
remove_labels.sort()
|
|
998
|
+
self.remove_labels(page_id, remove_labels)
|