markdown-to-confluence 0.3.5__py3-none-any.whl → 0.4.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.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/METADATA +150 -17
- markdown_to_confluence-0.4.1.dist-info/RECORD +25 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +20 -17
- md2conf/api.py +529 -216
- md2conf/application.py +85 -96
- md2conf/collection.py +31 -0
- md2conf/converter.py +99 -78
- md2conf/emoji.py +28 -3
- md2conf/extra.py +27 -0
- md2conf/local.py +28 -41
- md2conf/matcher.py +1 -3
- md2conf/mermaid.py +2 -7
- md2conf/metadata.py +0 -2
- md2conf/processor.py +135 -57
- md2conf/properties.py +66 -14
- md2conf/scanner.py +56 -23
- markdown_to_confluence-0.3.5.dist-info/RECORD +0 -23
- {markdown_to_confluence-0.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.3.5.dist-info → markdown_to_confluence-0.4.1.dist-info}/zip-safe +0 -0
md2conf/api.py
CHANGED
|
@@ -6,42 +6,56 @@ 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
|
-
import functools
|
|
11
11
|
import io
|
|
12
|
-
import json
|
|
13
12
|
import logging
|
|
14
13
|
import mimetypes
|
|
15
14
|
import typing
|
|
16
15
|
from dataclasses import dataclass
|
|
17
16
|
from pathlib import Path
|
|
18
17
|
from types import TracebackType
|
|
19
|
-
from typing import Optional,
|
|
18
|
+
from typing import Any, Optional, TypeVar
|
|
20
19
|
from urllib.parse import urlencode, urlparse, urlunparse
|
|
21
20
|
|
|
22
21
|
import requests
|
|
22
|
+
from strong_typing.core import JsonType
|
|
23
|
+
from strong_typing.serialization import DeserializerOptions, json_dump_string, json_to_object, object_to_json
|
|
23
24
|
|
|
24
25
|
from .converter import ParseError, sanitize_confluence
|
|
25
26
|
from .metadata import ConfluenceSiteMetadata
|
|
26
|
-
from .properties import
|
|
27
|
-
ArgumentError,
|
|
28
|
-
ConfluenceConnectionProperties,
|
|
29
|
-
ConfluenceError,
|
|
30
|
-
PageError,
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
# a JSON type with possible `null` values
|
|
34
|
-
JsonType = Union[
|
|
35
|
-
None,
|
|
36
|
-
bool,
|
|
37
|
-
int,
|
|
38
|
-
float,
|
|
39
|
-
str,
|
|
40
|
-
dict[str, "JsonType"],
|
|
41
|
-
list["JsonType"],
|
|
42
|
-
]
|
|
27
|
+
from .properties import ArgumentError, ConfluenceConnectionProperties, ConfluenceError, PageError
|
|
43
28
|
|
|
29
|
+
T = TypeVar("T")
|
|
44
30
|
|
|
31
|
+
|
|
32
|
+
def _json_to_object(
|
|
33
|
+
typ: type[T],
|
|
34
|
+
data: JsonType,
|
|
35
|
+
) -> T:
|
|
36
|
+
return json_to_object(typ, data, options=DeserializerOptions(skip_unassigned=True))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
|
|
40
|
+
"Builds a URL with scheme, host, port, path and query string parameters."
|
|
41
|
+
|
|
42
|
+
scheme, netloc, path, params, query_str, fragment = urlparse(base_url)
|
|
43
|
+
|
|
44
|
+
if params:
|
|
45
|
+
raise ValueError("expected: url with no parameters")
|
|
46
|
+
if query_str:
|
|
47
|
+
raise ValueError("expected: url with no query string")
|
|
48
|
+
if fragment:
|
|
49
|
+
raise ValueError("expected: url with no fragment")
|
|
50
|
+
|
|
51
|
+
url_parts = (scheme, netloc, path, None, urlencode(query) if query else None, None)
|
|
52
|
+
return urlunparse(url_parts)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
LOGGER = logging.getLogger(__name__)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@enum.unique
|
|
45
59
|
class ConfluenceVersion(enum.Enum):
|
|
46
60
|
"""
|
|
47
61
|
Confluence REST API version an HTTP request corresponds to.
|
|
@@ -57,6 +71,7 @@ class ConfluenceVersion(enum.Enum):
|
|
|
57
71
|
VERSION_2 = "api/v2"
|
|
58
72
|
|
|
59
73
|
|
|
74
|
+
@enum.unique
|
|
60
75
|
class ConfluencePageParentContentType(enum.Enum):
|
|
61
76
|
"""
|
|
62
77
|
Content types that can be a parent to a Confluence page.
|
|
@@ -69,23 +84,44 @@ class ConfluencePageParentContentType(enum.Enum):
|
|
|
69
84
|
FOLDER = "folder"
|
|
70
85
|
|
|
71
86
|
|
|
72
|
-
|
|
73
|
-
|
|
87
|
+
@enum.unique
|
|
88
|
+
class ConfluenceRepresentation(enum.Enum):
|
|
89
|
+
STORAGE = "storage"
|
|
90
|
+
ATLAS = "atlas_doc_format"
|
|
91
|
+
WIKI = "wiki"
|
|
74
92
|
|
|
75
|
-
scheme, netloc, path, params, query_str, fragment = urlparse(base_url)
|
|
76
93
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
raise ValueError("expected: url with no fragment")
|
|
94
|
+
@enum.unique
|
|
95
|
+
class ConfluenceStatus(enum.Enum):
|
|
96
|
+
CURRENT = "current"
|
|
97
|
+
DRAFT = "draft"
|
|
98
|
+
ARCHIVED = "archived"
|
|
83
99
|
|
|
84
|
-
url_parts = (scheme, netloc, path, None, urlencode(query) if query else None, None)
|
|
85
|
-
return urlunparse(url_parts)
|
|
86
100
|
|
|
101
|
+
@enum.unique
|
|
102
|
+
class ConfluenceLegacyType(enum.Enum):
|
|
103
|
+
ATTACHMENT = "attachment"
|
|
87
104
|
|
|
88
|
-
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class ConfluenceLinks:
|
|
108
|
+
next: str
|
|
109
|
+
base: str
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class ConfluenceResultSet:
|
|
114
|
+
results: list[JsonType]
|
|
115
|
+
_links: ConfluenceLinks
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass(frozen=True)
|
|
119
|
+
class ConfluenceContentVersion:
|
|
120
|
+
number: int
|
|
121
|
+
minorEdit: bool = False
|
|
122
|
+
createdAt: Optional[datetime.datetime] = None
|
|
123
|
+
message: Optional[str] = None
|
|
124
|
+
authorId: Optional[str] = None
|
|
89
125
|
|
|
90
126
|
|
|
91
127
|
@dataclass(frozen=True)
|
|
@@ -94,15 +130,33 @@ class ConfluenceAttachment:
|
|
|
94
130
|
Holds data for an object uploaded to Confluence as a page attachment.
|
|
95
131
|
|
|
96
132
|
:param id: Unique ID for the attachment.
|
|
97
|
-
:param
|
|
98
|
-
:param
|
|
133
|
+
:param status: Attachment status.
|
|
134
|
+
:param title: Attachment title.
|
|
135
|
+
:param createdAt: Date and time when the attachment was created.
|
|
136
|
+
:param pageId: The Confluence page that the attachment is coupled with.
|
|
137
|
+
:param mediaType: MIME type for the attachment.
|
|
138
|
+
:param mediaTypeDescription: Media type description for the attachment.
|
|
99
139
|
:param comment: Description for the attachment.
|
|
140
|
+
:param fileId: File ID of the attachment, distinct from the attachment ID.
|
|
141
|
+
:param fileSize: Size in bytes.
|
|
142
|
+
:param webuiLink: WebUI link of the attachment.
|
|
143
|
+
:param downloadLink: Download link of the attachment.
|
|
144
|
+
:param version: Version information for the attachment.
|
|
100
145
|
"""
|
|
101
146
|
|
|
102
147
|
id: str
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
148
|
+
status: ConfluenceStatus
|
|
149
|
+
title: Optional[str]
|
|
150
|
+
createdAt: datetime.datetime
|
|
151
|
+
pageId: str
|
|
152
|
+
mediaType: str
|
|
153
|
+
mediaTypeDescription: str
|
|
154
|
+
comment: Optional[str]
|
|
155
|
+
fileId: str
|
|
156
|
+
fileSize: int
|
|
157
|
+
webuiLink: str
|
|
158
|
+
downloadLink: str
|
|
159
|
+
version: ConfluenceContentVersion
|
|
106
160
|
|
|
107
161
|
|
|
108
162
|
@dataclass(frozen=True)
|
|
@@ -111,19 +165,55 @@ class ConfluencePageProperties:
|
|
|
111
165
|
Holds Confluence page properties used for page synchronization.
|
|
112
166
|
|
|
113
167
|
:param id: Confluence page ID.
|
|
114
|
-
:param
|
|
115
|
-
:param parent_id: Confluence page ID of the immediate parent.
|
|
116
|
-
:param parent_type: Identifies the content type of the parent.
|
|
168
|
+
:param status: Page status.
|
|
117
169
|
:param title: Page title.
|
|
170
|
+
:param spaceId: Confluence space ID.
|
|
171
|
+
:param parentId: Confluence page ID of the immediate parent.
|
|
172
|
+
:param parentType: Identifies the content type of the parent.
|
|
173
|
+
:param position: Position of child page within the given parent page tree.
|
|
174
|
+
:param authorId: The account ID of the user who created this page originally.
|
|
175
|
+
:param ownerId: The account ID of the user who owns this page.
|
|
176
|
+
:param lastOwnerId: The account ID of the user who owned this page previously, or `None` if there is no previous owner.
|
|
177
|
+
:param createdAt: Date and time when the page was created.
|
|
118
178
|
:param version: Page version. Incremented when the page is updated.
|
|
119
179
|
"""
|
|
120
180
|
|
|
121
181
|
id: str
|
|
122
|
-
|
|
123
|
-
parent_id: str
|
|
124
|
-
parent_type: Optional[ConfluencePageParentContentType]
|
|
182
|
+
status: ConfluenceStatus
|
|
125
183
|
title: str
|
|
126
|
-
|
|
184
|
+
spaceId: str
|
|
185
|
+
parentId: Optional[str]
|
|
186
|
+
parentType: Optional[ConfluencePageParentContentType]
|
|
187
|
+
position: Optional[int]
|
|
188
|
+
authorId: str
|
|
189
|
+
ownerId: str
|
|
190
|
+
lastOwnerId: Optional[str]
|
|
191
|
+
createdAt: datetime.datetime
|
|
192
|
+
version: ConfluenceContentVersion
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True)
|
|
196
|
+
class ConfluencePageStorage:
|
|
197
|
+
"""
|
|
198
|
+
Holds Confluence page content.
|
|
199
|
+
|
|
200
|
+
:param representation: Type of content representation used (e.g. Confluence Storage Format).
|
|
201
|
+
:param value: Body of the content, in the format found in the representation field.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
representation: ConfluenceRepresentation
|
|
205
|
+
value: str
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass(frozen=True)
|
|
209
|
+
class ConfluencePageBody:
|
|
210
|
+
"""
|
|
211
|
+
Holds Confluence page content.
|
|
212
|
+
|
|
213
|
+
:param storage: Encapsulates content with meta-information about its representation.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
storage: ConfluencePageStorage
|
|
127
217
|
|
|
128
218
|
|
|
129
219
|
@dataclass(frozen=True)
|
|
@@ -131,27 +221,102 @@ class ConfluencePage(ConfluencePageProperties):
|
|
|
131
221
|
"""
|
|
132
222
|
Holds Confluence page data used for page synchronization.
|
|
133
223
|
|
|
134
|
-
:param
|
|
224
|
+
:param body: Page content.
|
|
135
225
|
"""
|
|
136
226
|
|
|
137
|
-
|
|
227
|
+
body: ConfluencePageBody
|
|
138
228
|
|
|
229
|
+
@property
|
|
230
|
+
def content(self) -> str:
|
|
231
|
+
return self.body.storage.value
|
|
139
232
|
|
|
140
|
-
|
|
233
|
+
|
|
234
|
+
@dataclass(frozen=True, eq=True, order=True)
|
|
141
235
|
class ConfluenceLabel:
|
|
142
236
|
"""
|
|
143
237
|
Holds information about a single label.
|
|
144
238
|
|
|
145
|
-
:param id: ID of the label.
|
|
146
239
|
:param name: Name of the label.
|
|
147
240
|
:param prefix: Prefix of the label.
|
|
148
241
|
"""
|
|
149
242
|
|
|
150
|
-
id: str
|
|
151
243
|
name: str
|
|
152
244
|
prefix: str
|
|
153
245
|
|
|
154
246
|
|
|
247
|
+
@dataclass(frozen=True, eq=True, order=True)
|
|
248
|
+
class ConfluenceIdentifiedLabel(ConfluenceLabel):
|
|
249
|
+
"""
|
|
250
|
+
Holds information about a single label.
|
|
251
|
+
|
|
252
|
+
:param id: ID of the label.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
id: str
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@dataclass(frozen=True)
|
|
259
|
+
class ConfluenceContentProperty:
|
|
260
|
+
"""
|
|
261
|
+
Represents a content property.
|
|
262
|
+
|
|
263
|
+
:param key: Property key.
|
|
264
|
+
:param value: Property value as JSON.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
key: str
|
|
268
|
+
value: JsonType
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass(frozen=True)
|
|
272
|
+
class ConfluenceVersionedContentProperty(ConfluenceContentProperty):
|
|
273
|
+
"""
|
|
274
|
+
Represents a content property.
|
|
275
|
+
|
|
276
|
+
:param version: Version information about the property.
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
version: ConfluenceContentVersion
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass(frozen=True)
|
|
283
|
+
class ConfluenceIdentifiedContentProperty(ConfluenceVersionedContentProperty):
|
|
284
|
+
"""
|
|
285
|
+
Represents a content property.
|
|
286
|
+
|
|
287
|
+
:param id: Property ID.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
id: str
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass(frozen=True)
|
|
294
|
+
class ConfluenceCreatePageRequest:
|
|
295
|
+
spaceId: str
|
|
296
|
+
status: Optional[ConfluenceStatus]
|
|
297
|
+
title: Optional[str]
|
|
298
|
+
parentId: Optional[str]
|
|
299
|
+
body: ConfluencePageBody
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@dataclass(frozen=True)
|
|
303
|
+
class ConfluenceUpdatePageRequest:
|
|
304
|
+
id: str
|
|
305
|
+
status: ConfluenceStatus
|
|
306
|
+
title: str
|
|
307
|
+
body: ConfluencePageBody
|
|
308
|
+
version: ConfluenceContentVersion
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@dataclass(frozen=True)
|
|
312
|
+
class ConfluenceUpdateAttachmentRequest:
|
|
313
|
+
id: str
|
|
314
|
+
type: ConfluenceLegacyType
|
|
315
|
+
status: ConfluenceStatus
|
|
316
|
+
title: str
|
|
317
|
+
version: ConfluenceContentVersion
|
|
318
|
+
|
|
319
|
+
|
|
155
320
|
class ConfluenceAPI:
|
|
156
321
|
"""
|
|
157
322
|
Represents an active connection to a Confluence server.
|
|
@@ -160,9 +325,7 @@ class ConfluenceAPI:
|
|
|
160
325
|
properties: ConfluenceConnectionProperties
|
|
161
326
|
session: Optional["ConfluenceSession"] = None
|
|
162
327
|
|
|
163
|
-
def __init__(
|
|
164
|
-
self, properties: Optional[ConfluenceConnectionProperties] = None
|
|
165
|
-
) -> None:
|
|
328
|
+
def __init__(self, properties: Optional[ConfluenceConnectionProperties] = None) -> None:
|
|
166
329
|
self.properties = properties or ConfluenceConnectionProperties()
|
|
167
330
|
|
|
168
331
|
def __enter__(self) -> "ConfluenceSession":
|
|
@@ -170,18 +333,17 @@ class ConfluenceAPI:
|
|
|
170
333
|
if self.properties.user_name:
|
|
171
334
|
session.auth = (self.properties.user_name, self.properties.api_key)
|
|
172
335
|
else:
|
|
173
|
-
session.headers.update(
|
|
174
|
-
{"Authorization": f"Bearer {self.properties.api_key}"}
|
|
175
|
-
)
|
|
336
|
+
session.headers.update({"Authorization": f"Bearer {self.properties.api_key}"})
|
|
176
337
|
|
|
177
338
|
if self.properties.headers:
|
|
178
339
|
session.headers.update(self.properties.headers)
|
|
179
340
|
|
|
180
341
|
self.session = ConfluenceSession(
|
|
181
342
|
session,
|
|
182
|
-
self.properties.
|
|
183
|
-
self.properties.
|
|
184
|
-
self.properties.
|
|
343
|
+
api_url=self.properties.api_url,
|
|
344
|
+
domain=self.properties.domain,
|
|
345
|
+
base_path=self.properties.base_path,
|
|
346
|
+
space_key=self.properties.space_key,
|
|
185
347
|
)
|
|
186
348
|
return self.session
|
|
187
349
|
|
|
@@ -202,6 +364,7 @@ class ConfluenceSession:
|
|
|
202
364
|
"""
|
|
203
365
|
|
|
204
366
|
session: requests.Session
|
|
367
|
+
api_url: str
|
|
205
368
|
site: ConfluenceSiteMetadata
|
|
206
369
|
|
|
207
370
|
_space_id_to_key: dict[str, str]
|
|
@@ -210,16 +373,36 @@ class ConfluenceSession:
|
|
|
210
373
|
def __init__(
|
|
211
374
|
self,
|
|
212
375
|
session: requests.Session,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
376
|
+
*,
|
|
377
|
+
api_url: Optional[str],
|
|
378
|
+
domain: Optional[str],
|
|
379
|
+
base_path: Optional[str],
|
|
380
|
+
space_key: Optional[str],
|
|
216
381
|
) -> None:
|
|
217
382
|
self.session = session
|
|
218
|
-
self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
|
|
219
|
-
|
|
220
383
|
self._space_id_to_key = {}
|
|
221
384
|
self._space_key_to_id = {}
|
|
222
385
|
|
|
386
|
+
if api_url:
|
|
387
|
+
self.api_url = api_url
|
|
388
|
+
|
|
389
|
+
if not domain or not base_path:
|
|
390
|
+
payload = self._invoke(ConfluenceVersion.VERSION_2, "/spaces", {"limit": "1"})
|
|
391
|
+
data = json_to_object(ConfluenceResultSet, payload)
|
|
392
|
+
base_url = data._links.base
|
|
393
|
+
|
|
394
|
+
_, domain, base_path, _, _, _ = urlparse(base_url)
|
|
395
|
+
if not base_path.endswith("/"):
|
|
396
|
+
base_path = f"{base_path}/"
|
|
397
|
+
|
|
398
|
+
if not domain:
|
|
399
|
+
raise ArgumentError("Confluence domain not specified and cannot be inferred")
|
|
400
|
+
if not base_path:
|
|
401
|
+
raise ArgumentError("Confluence base path not specified and cannot be inferred")
|
|
402
|
+
self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
|
|
403
|
+
if not api_url:
|
|
404
|
+
self.api_url = f"https://{self.site.domain}{self.site.base_path}"
|
|
405
|
+
|
|
223
406
|
def close(self) -> None:
|
|
224
407
|
self.session.close()
|
|
225
408
|
self.session = requests.Session()
|
|
@@ -239,9 +422,7 @@ class ConfluenceSession:
|
|
|
239
422
|
:returns: A full URL.
|
|
240
423
|
"""
|
|
241
424
|
|
|
242
|
-
base_url =
|
|
243
|
-
f"https://{self.site.domain}{self.site.base_path}{version.value}{path}"
|
|
244
|
-
)
|
|
425
|
+
base_url = f"{self.api_url}{version.value}{path}"
|
|
245
426
|
return build_url(base_url, query)
|
|
246
427
|
|
|
247
428
|
def _invoke(
|
|
@@ -253,21 +434,19 @@ class ConfluenceSession:
|
|
|
253
434
|
"Executes an HTTP request via Confluence API."
|
|
254
435
|
|
|
255
436
|
url = self._build_url(version, path, query)
|
|
256
|
-
response = self.session.get(url)
|
|
437
|
+
response = self.session.get(url, headers={"Accept": "application/json"})
|
|
257
438
|
if response.text:
|
|
258
439
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
259
440
|
response.raise_for_status()
|
|
260
|
-
return response.json()
|
|
441
|
+
return typing.cast(JsonType, response.json())
|
|
261
442
|
|
|
262
|
-
def _fetch(
|
|
263
|
-
self, path: str, query: Optional[dict[str, str]] = None
|
|
264
|
-
) -> list[JsonType]:
|
|
443
|
+
def _fetch(self, path: str, query: Optional[dict[str, str]] = None) -> list[JsonType]:
|
|
265
444
|
"Retrieves all results of a REST API v2 paginated result-set."
|
|
266
445
|
|
|
267
446
|
items: list[JsonType] = []
|
|
268
447
|
url = self._build_url(ConfluenceVersion.VERSION_2, path, query)
|
|
269
448
|
while True:
|
|
270
|
-
response = self.session.get(url)
|
|
449
|
+
response = self.session.get(url, headers={"Accept": "application/json"})
|
|
271
450
|
response.raise_for_status()
|
|
272
451
|
|
|
273
452
|
payload = typing.cast(dict[str, JsonType], response.json())
|
|
@@ -289,7 +468,7 @@ class ConfluenceSession:
|
|
|
289
468
|
url = self._build_url(version, path)
|
|
290
469
|
response = self.session.put(
|
|
291
470
|
url,
|
|
292
|
-
data=
|
|
471
|
+
data=json_dump_string(data),
|
|
293
472
|
headers={"Content-Type": "application/json"},
|
|
294
473
|
)
|
|
295
474
|
if response.text:
|
|
@@ -306,8 +485,8 @@ class ConfluenceSession:
|
|
|
306
485
|
"/spaces",
|
|
307
486
|
{"ids": id, "status": "current"},
|
|
308
487
|
)
|
|
309
|
-
|
|
310
|
-
results = typing.cast(list[JsonType],
|
|
488
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
489
|
+
results = typing.cast(list[JsonType], data["results"])
|
|
311
490
|
if len(results) != 1:
|
|
312
491
|
raise ConfluenceError(f"unique space not found with id: {id}")
|
|
313
492
|
|
|
@@ -328,8 +507,8 @@ class ConfluenceSession:
|
|
|
328
507
|
"/spaces",
|
|
329
508
|
{"keys": key, "status": "current"},
|
|
330
509
|
)
|
|
331
|
-
|
|
332
|
-
results = typing.cast(list[JsonType],
|
|
510
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
511
|
+
results = typing.cast(list[JsonType], data["results"])
|
|
333
512
|
if len(results) != 1:
|
|
334
513
|
raise ConfluenceError(f"unique space not found with key: {key}")
|
|
335
514
|
|
|
@@ -340,9 +519,7 @@ class ConfluenceSession:
|
|
|
340
519
|
|
|
341
520
|
return id
|
|
342
521
|
|
|
343
|
-
def get_space_id(
|
|
344
|
-
self, *, space_id: Optional[str] = None, space_key: Optional[str] = None
|
|
345
|
-
) -> Optional[str]:
|
|
522
|
+
def get_space_id(self, *, space_id: Optional[str] = None, space_key: Optional[str] = None) -> Optional[str]:
|
|
346
523
|
"""
|
|
347
524
|
Coalesces a space ID or space key into a space ID, accounting for site default.
|
|
348
525
|
|
|
@@ -363,29 +540,21 @@ class ConfluenceSession:
|
|
|
363
540
|
# space ID and key are unset, and no default space is configured
|
|
364
541
|
return None
|
|
365
542
|
|
|
366
|
-
def get_attachment_by_name(
|
|
367
|
-
self, page_id: str, filename: str
|
|
368
|
-
) -> ConfluenceAttachment:
|
|
543
|
+
def get_attachment_by_name(self, page_id: str, filename: str) -> ConfluenceAttachment:
|
|
369
544
|
"""
|
|
370
545
|
Retrieves a Confluence page attachment by an unprefixed file name.
|
|
371
546
|
"""
|
|
372
547
|
|
|
373
548
|
path = f"/pages/{page_id}/attachments"
|
|
374
549
|
query = {"filename": filename}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
)
|
|
550
|
+
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
551
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
378
552
|
|
|
379
553
|
results = typing.cast(list[JsonType], data["results"])
|
|
380
554
|
if len(results) != 1:
|
|
381
555
|
raise ConfluenceError(f"no such attachment on page {page_id}: {filename}")
|
|
382
556
|
result = typing.cast(dict[str, JsonType], results[0])
|
|
383
|
-
|
|
384
|
-
id = typing.cast(str, result["id"])
|
|
385
|
-
media_type = typing.cast(str, result["mediaType"])
|
|
386
|
-
file_size = typing.cast(int, result["fileSize"])
|
|
387
|
-
comment = typing.cast(str, result.get("comment", ""))
|
|
388
|
-
return ConfluenceAttachment(id, media_type, file_size, comment)
|
|
557
|
+
return _json_to_object(ConfluenceAttachment, result)
|
|
389
558
|
|
|
390
559
|
def upload_attachment(
|
|
391
560
|
self,
|
|
@@ -423,6 +592,9 @@ class ConfluenceSession:
|
|
|
423
592
|
name = attachment_name
|
|
424
593
|
content_type, _ = mimetypes.guess_type(name, strict=True)
|
|
425
594
|
|
|
595
|
+
if content_type is None:
|
|
596
|
+
content_type = "application/octet-stream"
|
|
597
|
+
|
|
426
598
|
if attachment_path is not None and not attachment_path.is_file():
|
|
427
599
|
raise PageError(f"file not found: {attachment_path}")
|
|
428
600
|
|
|
@@ -430,15 +602,15 @@ class ConfluenceSession:
|
|
|
430
602
|
attachment = self.get_attachment_by_name(page_id, attachment_name)
|
|
431
603
|
|
|
432
604
|
if attachment_path is not None:
|
|
433
|
-
if not force and attachment.
|
|
605
|
+
if not force and attachment.fileSize == attachment_path.stat().st_size:
|
|
434
606
|
LOGGER.info("Up-to-date attachment: %s", attachment_name)
|
|
435
607
|
return
|
|
436
608
|
elif raw_data is not None:
|
|
437
|
-
if not force and attachment.
|
|
609
|
+
if not force and attachment.fileSize == len(raw_data):
|
|
438
610
|
LOGGER.info("Up-to-date embedded image: %s", attachment_name)
|
|
439
611
|
return
|
|
440
612
|
else:
|
|
441
|
-
raise NotImplementedError("
|
|
613
|
+
raise NotImplementedError("parameter match not exhaustive")
|
|
442
614
|
|
|
443
615
|
id = attachment.id.removeprefix("att")
|
|
444
616
|
path = f"/content/{page_id}/child/attachment/{id}/data"
|
|
@@ -450,8 +622,13 @@ class ConfluenceSession:
|
|
|
450
622
|
|
|
451
623
|
if attachment_path is not None:
|
|
452
624
|
with open(attachment_path, "rb") as attachment_file:
|
|
453
|
-
file_to_upload = {
|
|
454
|
-
"comment":
|
|
625
|
+
file_to_upload: dict[str, tuple[Optional[str], Any, str, dict[str, str]]] = {
|
|
626
|
+
"comment": (
|
|
627
|
+
None,
|
|
628
|
+
comment,
|
|
629
|
+
"text/plain; charset=utf-8",
|
|
630
|
+
{},
|
|
631
|
+
),
|
|
455
632
|
"file": (
|
|
456
633
|
attachment_name, # will truncate path component
|
|
457
634
|
attachment_file,
|
|
@@ -462,8 +639,11 @@ class ConfluenceSession:
|
|
|
462
639
|
LOGGER.info("Uploading attachment: %s", attachment_name)
|
|
463
640
|
response = self.session.post(
|
|
464
641
|
url,
|
|
465
|
-
files=file_to_upload,
|
|
466
|
-
headers={
|
|
642
|
+
files=file_to_upload,
|
|
643
|
+
headers={
|
|
644
|
+
"X-Atlassian-Token": "no-check",
|
|
645
|
+
"Accept": "application/json",
|
|
646
|
+
},
|
|
467
647
|
)
|
|
468
648
|
elif raw_data is not None:
|
|
469
649
|
LOGGER.info("Uploading raw data: %s", attachment_name)
|
|
@@ -471,21 +651,29 @@ class ConfluenceSession:
|
|
|
471
651
|
raw_file = io.BytesIO(raw_data)
|
|
472
652
|
raw_file.name = attachment_name
|
|
473
653
|
file_to_upload = {
|
|
474
|
-
"comment":
|
|
654
|
+
"comment": (
|
|
655
|
+
None,
|
|
656
|
+
comment,
|
|
657
|
+
"text/plain; charset=utf-8",
|
|
658
|
+
{},
|
|
659
|
+
),
|
|
475
660
|
"file": (
|
|
476
661
|
attachment_name, # will truncate path component
|
|
477
|
-
raw_file,
|
|
662
|
+
raw_file,
|
|
478
663
|
content_type,
|
|
479
664
|
{"Expires": "0"},
|
|
480
665
|
),
|
|
481
666
|
}
|
|
482
667
|
response = self.session.post(
|
|
483
668
|
url,
|
|
484
|
-
files=file_to_upload,
|
|
485
|
-
headers={
|
|
669
|
+
files=file_to_upload,
|
|
670
|
+
headers={
|
|
671
|
+
"X-Atlassian-Token": "no-check",
|
|
672
|
+
"Accept": "application/json",
|
|
673
|
+
},
|
|
486
674
|
)
|
|
487
675
|
else:
|
|
488
|
-
raise NotImplementedError("
|
|
676
|
+
raise NotImplementedError("parameter match not exhaustive")
|
|
489
677
|
|
|
490
678
|
response.raise_for_status()
|
|
491
679
|
data = response.json()
|
|
@@ -501,29 +689,27 @@ class ConfluenceSession:
|
|
|
501
689
|
# ensure path component is retained in attachment name
|
|
502
690
|
self._update_attachment(page_id, attachment_id, version, attachment_name)
|
|
503
691
|
|
|
504
|
-
def _update_attachment(
|
|
505
|
-
self, page_id: str, attachment_id: str, version: int, attachment_title: str
|
|
506
|
-
) -> None:
|
|
692
|
+
def _update_attachment(self, page_id: str, attachment_id: str, version: int, attachment_title: str) -> None:
|
|
507
693
|
id = attachment_id.removeprefix("att")
|
|
508
694
|
path = f"/content/{page_id}/child/attachment/{id}"
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
695
|
+
request = ConfluenceUpdateAttachmentRequest(
|
|
696
|
+
id=attachment_id,
|
|
697
|
+
type=ConfluenceLegacyType.ATTACHMENT,
|
|
698
|
+
status=ConfluenceStatus.CURRENT,
|
|
699
|
+
title=attachment_title,
|
|
700
|
+
version=ConfluenceContentVersion(number=version, minorEdit=True),
|
|
701
|
+
)
|
|
516
702
|
|
|
517
703
|
LOGGER.info("Updating attachment: %s", attachment_id)
|
|
518
|
-
self._save(ConfluenceVersion.VERSION_1, path,
|
|
704
|
+
self._save(ConfluenceVersion.VERSION_1, path, object_to_json(request))
|
|
519
705
|
|
|
520
|
-
def
|
|
706
|
+
def get_page_properties_by_title(
|
|
521
707
|
self,
|
|
522
708
|
title: str,
|
|
523
709
|
*,
|
|
524
710
|
space_id: Optional[str] = None,
|
|
525
711
|
space_key: Optional[str] = None,
|
|
526
|
-
) ->
|
|
712
|
+
) -> ConfluencePageProperties:
|
|
527
713
|
"""
|
|
528
714
|
Looks up a Confluence wiki page ID by title.
|
|
529
715
|
|
|
@@ -543,15 +729,13 @@ class ConfluenceSession:
|
|
|
543
729
|
query["space-id"] = space_id
|
|
544
730
|
|
|
545
731
|
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
results = typing.cast(list[JsonType], payload["results"])
|
|
732
|
+
data = typing.cast(dict[str, JsonType], payload)
|
|
733
|
+
results = typing.cast(list[JsonType], data["results"])
|
|
549
734
|
if len(results) != 1:
|
|
550
735
|
raise ConfluenceError(f"unique page not found with title: {title}")
|
|
551
736
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return id
|
|
737
|
+
page = _json_to_object(ConfluencePageProperties, results[0])
|
|
738
|
+
return page
|
|
555
739
|
|
|
556
740
|
def get_page(self, page_id: str) -> ConfluencePage:
|
|
557
741
|
"""
|
|
@@ -564,26 +748,8 @@ class ConfluenceSession:
|
|
|
564
748
|
path = f"/pages/{page_id}"
|
|
565
749
|
query = {"body-format": "storage"}
|
|
566
750
|
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
567
|
-
|
|
568
|
-
version = typing.cast(dict[str, JsonType], data["version"])
|
|
569
|
-
body = typing.cast(dict[str, JsonType], data["body"])
|
|
570
|
-
storage = typing.cast(dict[str, JsonType], body["storage"])
|
|
751
|
+
return _json_to_object(ConfluencePage, payload)
|
|
571
752
|
|
|
572
|
-
return ConfluencePage(
|
|
573
|
-
id=page_id,
|
|
574
|
-
space_id=typing.cast(str, data["spaceId"]),
|
|
575
|
-
parent_id=typing.cast(str, data["parentId"]),
|
|
576
|
-
parent_type=(
|
|
577
|
-
ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
|
|
578
|
-
if data["parentType"] is not None
|
|
579
|
-
else None
|
|
580
|
-
),
|
|
581
|
-
title=typing.cast(str, data["title"]),
|
|
582
|
-
version=typing.cast(int, version["number"]),
|
|
583
|
-
content=typing.cast(str, storage["value"]),
|
|
584
|
-
)
|
|
585
|
-
|
|
586
|
-
@functools.cache
|
|
587
753
|
def get_page_properties(self, page_id: str) -> ConfluencePageProperties:
|
|
588
754
|
"""
|
|
589
755
|
Retrieves Confluence wiki page details.
|
|
@@ -594,21 +760,7 @@ class ConfluenceSession:
|
|
|
594
760
|
|
|
595
761
|
path = f"/pages/{page_id}"
|
|
596
762
|
payload = self._invoke(ConfluenceVersion.VERSION_2, path)
|
|
597
|
-
|
|
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
|
-
)
|
|
763
|
+
return _json_to_object(ConfluencePageProperties, payload)
|
|
612
764
|
|
|
613
765
|
def get_page_version(self, page_id: str) -> int:
|
|
614
766
|
"""
|
|
@@ -618,11 +770,7 @@ class ConfluenceSession:
|
|
|
618
770
|
:returns: Confluence page version.
|
|
619
771
|
"""
|
|
620
772
|
|
|
621
|
-
|
|
622
|
-
payload = self._invoke(ConfluenceVersion.VERSION_2, path)
|
|
623
|
-
data = typing.cast(dict[str, JsonType], payload)
|
|
624
|
-
version = typing.cast(dict[str, JsonType], data["version"])
|
|
625
|
-
return typing.cast(int, version["number"])
|
|
773
|
+
return self.get_page_properties(page_id).version.number
|
|
626
774
|
|
|
627
775
|
def update_page(
|
|
628
776
|
self,
|
|
@@ -651,16 +799,15 @@ class ConfluenceSession:
|
|
|
651
799
|
LOGGER.warning(exc)
|
|
652
800
|
|
|
653
801
|
path = f"/pages/{page_id}"
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
802
|
+
request = ConfluenceUpdatePageRequest(
|
|
803
|
+
id=page_id,
|
|
804
|
+
status=ConfluenceStatus.CURRENT,
|
|
805
|
+
title=new_title,
|
|
806
|
+
body=ConfluencePageBody(storage=ConfluencePageStorage(representation=ConfluenceRepresentation.STORAGE, value=new_content)),
|
|
807
|
+
version=ConfluenceContentVersion(number=page.version.number + 1, minorEdit=True),
|
|
808
|
+
)
|
|
662
809
|
LOGGER.info("Updating page: %s", page_id)
|
|
663
|
-
self._save(ConfluenceVersion.VERSION_2, path,
|
|
810
|
+
self._save(ConfluenceVersion.VERSION_2, path, object_to_json(request))
|
|
664
811
|
|
|
665
812
|
def create_page(
|
|
666
813
|
self,
|
|
@@ -672,44 +819,35 @@ class ConfluenceSession:
|
|
|
672
819
|
Creates a new page via Confluence API.
|
|
673
820
|
"""
|
|
674
821
|
|
|
822
|
+
LOGGER.info("Creating page: %s", title)
|
|
823
|
+
|
|
675
824
|
parent_page = self.get_page_properties(parent_id)
|
|
676
|
-
path = "/pages/"
|
|
677
|
-
query = {
|
|
678
|
-
"spaceId": parent_page.space_id,
|
|
679
|
-
"status": "current",
|
|
680
|
-
"title": title,
|
|
681
|
-
"parentId": parent_id,
|
|
682
|
-
"body": {"storage": {"value": new_content, "representation": "storage"}},
|
|
683
|
-
}
|
|
684
825
|
|
|
685
|
-
|
|
826
|
+
path = "/pages/"
|
|
827
|
+
request = ConfluenceCreatePageRequest(
|
|
828
|
+
spaceId=parent_page.spaceId,
|
|
829
|
+
status=ConfluenceStatus.CURRENT,
|
|
830
|
+
title=title,
|
|
831
|
+
parentId=parent_id,
|
|
832
|
+
body=ConfluencePageBody(
|
|
833
|
+
storage=ConfluencePageStorage(
|
|
834
|
+
representation=ConfluenceRepresentation.STORAGE,
|
|
835
|
+
value=new_content,
|
|
836
|
+
)
|
|
837
|
+
),
|
|
838
|
+
)
|
|
686
839
|
|
|
687
840
|
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
688
841
|
response = self.session.post(
|
|
689
842
|
url,
|
|
690
|
-
data=
|
|
691
|
-
headers={
|
|
843
|
+
data=json_dump_string(object_to_json(request)),
|
|
844
|
+
headers={
|
|
845
|
+
"Content-Type": "application/json",
|
|
846
|
+
"Accept": "application/json",
|
|
847
|
+
},
|
|
692
848
|
)
|
|
693
849
|
response.raise_for_status()
|
|
694
|
-
|
|
695
|
-
data = typing.cast(dict[str, JsonType], response.json())
|
|
696
|
-
version = typing.cast(dict[str, JsonType], data["version"])
|
|
697
|
-
body = typing.cast(dict[str, JsonType], data["body"])
|
|
698
|
-
storage = typing.cast(dict[str, JsonType], body["storage"])
|
|
699
|
-
|
|
700
|
-
return ConfluencePage(
|
|
701
|
-
id=typing.cast(str, data["id"]),
|
|
702
|
-
space_id=typing.cast(str, data["spaceId"]),
|
|
703
|
-
parent_id=typing.cast(str, data["parentId"]),
|
|
704
|
-
parent_type=(
|
|
705
|
-
ConfluencePageParentContentType(typing.cast(str, data["parentType"]))
|
|
706
|
-
if data["parentType"] is not None
|
|
707
|
-
else None
|
|
708
|
-
),
|
|
709
|
-
title=typing.cast(str, data["title"]),
|
|
710
|
-
version=typing.cast(int, version["number"]),
|
|
711
|
-
content=typing.cast(str, storage["value"]),
|
|
712
|
-
)
|
|
850
|
+
return _json_to_object(ConfluencePage, response.json())
|
|
713
851
|
|
|
714
852
|
def delete_page(self, page_id: str, *, purge: bool = False) -> None:
|
|
715
853
|
"""
|
|
@@ -761,16 +899,19 @@ class ConfluenceSession:
|
|
|
761
899
|
|
|
762
900
|
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
763
901
|
response = self.session.get(
|
|
764
|
-
url,
|
|
902
|
+
url,
|
|
903
|
+
params=query,
|
|
904
|
+
headers={
|
|
905
|
+
"Content-Type": "application/json",
|
|
906
|
+
"Accept": "application/json",
|
|
907
|
+
},
|
|
765
908
|
)
|
|
766
909
|
response.raise_for_status()
|
|
767
|
-
|
|
768
910
|
data = typing.cast(dict[str, JsonType], response.json())
|
|
769
|
-
results =
|
|
911
|
+
results = _json_to_object(list[ConfluencePageProperties], data["results"])
|
|
770
912
|
|
|
771
913
|
if len(results) == 1:
|
|
772
|
-
|
|
773
|
-
return typing.cast(str, result["id"])
|
|
914
|
+
return results[0].id
|
|
774
915
|
else:
|
|
775
916
|
return None
|
|
776
917
|
|
|
@@ -783,7 +924,7 @@ class ConfluenceSession:
|
|
|
783
924
|
"""
|
|
784
925
|
|
|
785
926
|
parent_page = self.get_page_properties(parent_id)
|
|
786
|
-
page_id = self.page_exists(title, space_id=parent_page.
|
|
927
|
+
page_id = self.page_exists(title, space_id=parent_page.spaceId)
|
|
787
928
|
|
|
788
929
|
if page_id is not None:
|
|
789
930
|
LOGGER.debug("Retrieving existing page: %s", page_id)
|
|
@@ -792,7 +933,7 @@ class ConfluenceSession:
|
|
|
792
933
|
LOGGER.debug("Creating new page with title: %s", title)
|
|
793
934
|
return self.create_page(parent_id, title, "")
|
|
794
935
|
|
|
795
|
-
def get_labels(self, page_id: str) -> list[
|
|
936
|
+
def get_labels(self, page_id: str) -> list[ConfluenceIdentifiedLabel]:
|
|
796
937
|
"""
|
|
797
938
|
Retrieves labels for a Confluence page.
|
|
798
939
|
|
|
@@ -800,13 +941,185 @@ class ConfluenceSession:
|
|
|
800
941
|
:returns: A list of page labels.
|
|
801
942
|
"""
|
|
802
943
|
|
|
803
|
-
items: list[ConfluenceLabel] = []
|
|
804
944
|
path = f"/pages/{page_id}/labels"
|
|
805
945
|
results = self._fetch(path)
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
946
|
+
return _json_to_object(list[ConfluenceIdentifiedLabel], results)
|
|
947
|
+
|
|
948
|
+
def add_labels(self, page_id: str, labels: list[ConfluenceLabel]) -> None:
|
|
949
|
+
"""
|
|
950
|
+
Adds labels to a Confluence page.
|
|
951
|
+
|
|
952
|
+
:param page_id: The Confluence page ID.
|
|
953
|
+
:param labels: A list of page labels to add.
|
|
954
|
+
"""
|
|
955
|
+
|
|
956
|
+
path = f"/content/{page_id}/label"
|
|
957
|
+
|
|
958
|
+
url = self._build_url(ConfluenceVersion.VERSION_1, path)
|
|
959
|
+
response = self.session.post(
|
|
960
|
+
url,
|
|
961
|
+
data=json_dump_string(object_to_json(labels)),
|
|
962
|
+
headers={
|
|
963
|
+
"Content-Type": "application/json",
|
|
964
|
+
"Accept": "application/json",
|
|
965
|
+
},
|
|
966
|
+
)
|
|
967
|
+
if response.text:
|
|
968
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
969
|
+
response.raise_for_status()
|
|
970
|
+
|
|
971
|
+
def remove_labels(self, page_id: str, labels: list[ConfluenceLabel]) -> None:
|
|
972
|
+
"""
|
|
973
|
+
Removes labels from a Confluence page.
|
|
974
|
+
|
|
975
|
+
:param page_id: The Confluence page ID.
|
|
976
|
+
:param labels: A list of page labels to remove.
|
|
977
|
+
"""
|
|
978
|
+
|
|
979
|
+
path = f"/content/{page_id}/label"
|
|
980
|
+
for label in labels:
|
|
981
|
+
query = {"name": label.name}
|
|
982
|
+
|
|
983
|
+
url = self._build_url(ConfluenceVersion.VERSION_1, path, query)
|
|
984
|
+
response = self.session.delete(url)
|
|
985
|
+
if response.text:
|
|
986
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
987
|
+
response.raise_for_status()
|
|
988
|
+
|
|
989
|
+
def update_labels(self, page_id: str, labels: list[ConfluenceLabel], *, keep_existing: bool = False) -> None:
|
|
990
|
+
"""
|
|
991
|
+
Assigns the specified labels to a Confluence page. Existing labels are removed.
|
|
992
|
+
|
|
993
|
+
:param page_id: The Confluence page ID.
|
|
994
|
+
:param labels: A list of page labels to assign.
|
|
995
|
+
"""
|
|
996
|
+
|
|
997
|
+
new_labels = set(labels)
|
|
998
|
+
old_labels = set(ConfluenceLabel(name=label.name, prefix=label.prefix) for label in self.get_labels(page_id))
|
|
999
|
+
|
|
1000
|
+
add_labels = list(new_labels - old_labels)
|
|
1001
|
+
remove_labels = list(old_labels - new_labels)
|
|
1002
|
+
|
|
1003
|
+
if add_labels:
|
|
1004
|
+
add_labels.sort()
|
|
1005
|
+
self.add_labels(page_id, add_labels)
|
|
1006
|
+
if not keep_existing and remove_labels:
|
|
1007
|
+
remove_labels.sort()
|
|
1008
|
+
self.remove_labels(page_id, remove_labels)
|
|
1009
|
+
|
|
1010
|
+
def get_content_properties_for_page(self, page_id: str) -> list[ConfluenceIdentifiedContentProperty]:
|
|
1011
|
+
"""
|
|
1012
|
+
Retrieves content properties for a Confluence page.
|
|
1013
|
+
|
|
1014
|
+
:param page_id: The Confluence page ID.
|
|
1015
|
+
:returns: A list of content properties.
|
|
1016
|
+
"""
|
|
1017
|
+
|
|
1018
|
+
path = f"/pages/{page_id}/properties"
|
|
1019
|
+
results = self._fetch(path)
|
|
1020
|
+
return _json_to_object(list[ConfluenceIdentifiedContentProperty], results)
|
|
1021
|
+
|
|
1022
|
+
def add_content_property_to_page(self, page_id: str, property: ConfluenceContentProperty) -> ConfluenceIdentifiedContentProperty:
|
|
1023
|
+
"""
|
|
1024
|
+
Adds a new content property to a Confluence page.
|
|
1025
|
+
|
|
1026
|
+
:param page_id: The Confluence page ID.
|
|
1027
|
+
:param property: Content property to add.
|
|
1028
|
+
"""
|
|
1029
|
+
|
|
1030
|
+
path = f"/pages/{page_id}/properties"
|
|
1031
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
1032
|
+
response = self.session.post(
|
|
1033
|
+
url,
|
|
1034
|
+
data=json_dump_string(object_to_json(property)),
|
|
1035
|
+
headers={
|
|
1036
|
+
"Content-Type": "application/json",
|
|
1037
|
+
"Accept": "application/json",
|
|
1038
|
+
},
|
|
1039
|
+
)
|
|
1040
|
+
if response.text:
|
|
1041
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
1042
|
+
response.raise_for_status()
|
|
1043
|
+
return _json_to_object(ConfluenceIdentifiedContentProperty, response.json())
|
|
1044
|
+
|
|
1045
|
+
def remove_content_property_from_page(self, page_id: str, property_id: str) -> None:
|
|
1046
|
+
"""
|
|
1047
|
+
Removes a content property from a Confluence page.
|
|
1048
|
+
|
|
1049
|
+
:param page_id: The Confluence page ID.
|
|
1050
|
+
:param property_id: Property ID, which uniquely identifies the property.
|
|
1051
|
+
"""
|
|
1052
|
+
|
|
1053
|
+
path = f"/pages/{page_id}/properties/{property_id}"
|
|
1054
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
1055
|
+
response = self.session.delete(url)
|
|
1056
|
+
response.raise_for_status()
|
|
1057
|
+
|
|
1058
|
+
def update_content_property_for_page(
|
|
1059
|
+
self, page_id: str, property_id: str, version: int, property: ConfluenceContentProperty
|
|
1060
|
+
) -> ConfluenceIdentifiedContentProperty:
|
|
1061
|
+
"""
|
|
1062
|
+
Updates an existing content property associated with a Confluence page.
|
|
1063
|
+
|
|
1064
|
+
:param page_id: The Confluence page ID.
|
|
1065
|
+
:param property_id: Property ID, which uniquely identifies the property.
|
|
1066
|
+
:param version: Version number to assign.
|
|
1067
|
+
:param property: Content property data to assign.
|
|
1068
|
+
:returns: Updated content property data.
|
|
1069
|
+
"""
|
|
1070
|
+
|
|
1071
|
+
path = f"/pages/{page_id}/properties/{property_id}"
|
|
1072
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
1073
|
+
response = self.session.put(
|
|
1074
|
+
url,
|
|
1075
|
+
data=json_dump_string(
|
|
1076
|
+
object_to_json(
|
|
1077
|
+
ConfluenceVersionedContentProperty(
|
|
1078
|
+
key=property.key,
|
|
1079
|
+
value=property.value,
|
|
1080
|
+
version=ConfluenceContentVersion(number=version),
|
|
1081
|
+
)
|
|
1082
|
+
)
|
|
1083
|
+
),
|
|
1084
|
+
headers={"Content-Type": "application/json"},
|
|
1085
|
+
)
|
|
1086
|
+
if response.text:
|
|
1087
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
1088
|
+
response.raise_for_status()
|
|
1089
|
+
return json_to_object(ConfluenceIdentifiedContentProperty, response.json())
|
|
1090
|
+
|
|
1091
|
+
def update_content_properties_for_page(self, page_id: str, properties: list[ConfluenceContentProperty], *, keep_existing: bool = False) -> None:
|
|
1092
|
+
"""
|
|
1093
|
+
Updates content properties associated with a Confluence page.
|
|
1094
|
+
|
|
1095
|
+
:param page_id: The Confluence page ID.
|
|
1096
|
+
:param properties: A list of content property data to update.
|
|
1097
|
+
:param keep_existing: Whether to keep content property data whose key is not included in the list of properties passed as an argument.
|
|
1098
|
+
"""
|
|
1099
|
+
|
|
1100
|
+
old_mapping = {p.key: p for p in self.get_content_properties_for_page(page_id)}
|
|
1101
|
+
new_mapping = {p.key: p for p in properties}
|
|
1102
|
+
|
|
1103
|
+
new_props = set(p.key for p in properties)
|
|
1104
|
+
old_props = set(old_mapping.keys())
|
|
1105
|
+
|
|
1106
|
+
add_props = list(new_props - old_props)
|
|
1107
|
+
remove_props = list(old_props - new_props)
|
|
1108
|
+
update_props = list(old_props & new_props)
|
|
1109
|
+
|
|
1110
|
+
if add_props:
|
|
1111
|
+
add_props.sort()
|
|
1112
|
+
for key in add_props:
|
|
1113
|
+
self.add_content_property_to_page(page_id, new_mapping[key])
|
|
1114
|
+
if not keep_existing and remove_props:
|
|
1115
|
+
remove_props.sort()
|
|
1116
|
+
for key in remove_props:
|
|
1117
|
+
self.remove_content_property_from_page(page_id, old_mapping[key].id)
|
|
1118
|
+
if update_props:
|
|
1119
|
+
update_props.sort()
|
|
1120
|
+
for key in update_props:
|
|
1121
|
+
old_prop = old_mapping[key]
|
|
1122
|
+
new_prop = new_mapping[key]
|
|
1123
|
+
if old_prop.value == new_prop.value:
|
|
1124
|
+
continue
|
|
1125
|
+
self.update_content_property_for_page(page_id, old_prop.id, old_prop.version.number + 1, new_prop)
|