markdown-to-confluence 0.4.0__py3-none-any.whl → 0.4.2__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.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/METADATA +133 -43
- markdown_to_confluence-0.4.2.dist-info/RECORD +27 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +57 -18
- md2conf/api.py +242 -125
- md2conf/application.py +40 -48
- md2conf/collection.py +17 -11
- md2conf/converter.py +540 -107
- md2conf/drawio.py +222 -0
- md2conf/extra.py +13 -0
- md2conf/local.py +5 -12
- md2conf/matcher.py +64 -7
- md2conf/mermaid.py +2 -7
- md2conf/metadata.py +2 -0
- md2conf/processor.py +48 -57
- md2conf/properties.py +45 -12
- md2conf/scanner.py +17 -9
- md2conf/xml.py +70 -0
- markdown_to_confluence-0.4.0.dist-info/RECORD +0 -25
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.0.dist-info → markdown_to_confluence-0.4.2.dist-info}/zip-safe +0 -0
md2conf/api.py
CHANGED
|
@@ -8,7 +8,6 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
8
8
|
|
|
9
9
|
import datetime
|
|
10
10
|
import enum
|
|
11
|
-
import functools
|
|
12
11
|
import io
|
|
13
12
|
import logging
|
|
14
13
|
import mimetypes
|
|
@@ -16,26 +15,15 @@ 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, TypeVar
|
|
18
|
+
from typing import Any, Optional, TypeVar
|
|
20
19
|
from urllib.parse import urlencode, urlparse, urlunparse
|
|
21
20
|
|
|
22
21
|
import requests
|
|
23
22
|
from strong_typing.core import JsonType
|
|
24
|
-
from strong_typing.serialization import
|
|
25
|
-
|
|
26
|
-
json_dump_string,
|
|
27
|
-
json_to_object,
|
|
28
|
-
object_to_json,
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
from .converter import ParseError, sanitize_confluence
|
|
23
|
+
from strong_typing.serialization import DeserializerOptions, json_dump_string, json_to_object, object_to_json
|
|
24
|
+
|
|
32
25
|
from .metadata import ConfluenceSiteMetadata
|
|
33
|
-
from .properties import
|
|
34
|
-
ArgumentError,
|
|
35
|
-
ConfluenceConnectionProperties,
|
|
36
|
-
ConfluenceError,
|
|
37
|
-
PageError,
|
|
38
|
-
)
|
|
26
|
+
from .properties import ArgumentError, ConfluenceConnectionProperties, ConfluenceError, PageError
|
|
39
27
|
|
|
40
28
|
T = TypeVar("T")
|
|
41
29
|
|
|
@@ -66,6 +54,18 @@ def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
|
|
|
66
54
|
LOGGER = logging.getLogger(__name__)
|
|
67
55
|
|
|
68
56
|
|
|
57
|
+
def response_cast(response_type: type[T], response: requests.Response) -> T:
|
|
58
|
+
"Converts a response body into the expected type."
|
|
59
|
+
|
|
60
|
+
if response.text:
|
|
61
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
if response_type is not type(None):
|
|
64
|
+
return _json_to_object(response_type, response.json())
|
|
65
|
+
else:
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
69
|
@enum.unique
|
|
70
70
|
class ConfluenceVersion(enum.Enum):
|
|
71
71
|
"""
|
|
@@ -106,6 +106,7 @@ class ConfluenceRepresentation(enum.Enum):
|
|
|
106
106
|
class ConfluenceStatus(enum.Enum):
|
|
107
107
|
CURRENT = "current"
|
|
108
108
|
DRAFT = "draft"
|
|
109
|
+
ARCHIVED = "archived"
|
|
109
110
|
|
|
110
111
|
|
|
111
112
|
@enum.unique
|
|
@@ -160,7 +161,7 @@ class ConfluenceAttachment:
|
|
|
160
161
|
createdAt: datetime.datetime
|
|
161
162
|
pageId: str
|
|
162
163
|
mediaType: str
|
|
163
|
-
mediaTypeDescription: str
|
|
164
|
+
mediaTypeDescription: Optional[str]
|
|
164
165
|
comment: Optional[str]
|
|
165
166
|
fileId: str
|
|
166
167
|
fileSize: int
|
|
@@ -265,6 +266,41 @@ class ConfluenceIdentifiedLabel(ConfluenceLabel):
|
|
|
265
266
|
id: str
|
|
266
267
|
|
|
267
268
|
|
|
269
|
+
@dataclass(frozen=True)
|
|
270
|
+
class ConfluenceContentProperty:
|
|
271
|
+
"""
|
|
272
|
+
Represents a content property.
|
|
273
|
+
|
|
274
|
+
:param key: Property key.
|
|
275
|
+
:param value: Property value as JSON.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
key: str
|
|
279
|
+
value: JsonType
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass(frozen=True)
|
|
283
|
+
class ConfluenceVersionedContentProperty(ConfluenceContentProperty):
|
|
284
|
+
"""
|
|
285
|
+
Represents a content property.
|
|
286
|
+
|
|
287
|
+
:param version: Version information about the property.
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
version: ConfluenceContentVersion
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass(frozen=True)
|
|
294
|
+
class ConfluenceIdentifiedContentProperty(ConfluenceVersionedContentProperty):
|
|
295
|
+
"""
|
|
296
|
+
Represents a content property.
|
|
297
|
+
|
|
298
|
+
:param id: Property ID.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
id: str
|
|
302
|
+
|
|
303
|
+
|
|
268
304
|
@dataclass(frozen=True)
|
|
269
305
|
class ConfluenceCreatePageRequest:
|
|
270
306
|
spaceId: str
|
|
@@ -300,9 +336,7 @@ class ConfluenceAPI:
|
|
|
300
336
|
properties: ConfluenceConnectionProperties
|
|
301
337
|
session: Optional["ConfluenceSession"] = None
|
|
302
338
|
|
|
303
|
-
def __init__(
|
|
304
|
-
self, properties: Optional[ConfluenceConnectionProperties] = None
|
|
305
|
-
) -> None:
|
|
339
|
+
def __init__(self, properties: Optional[ConfluenceConnectionProperties] = None) -> None:
|
|
306
340
|
self.properties = properties or ConfluenceConnectionProperties()
|
|
307
341
|
|
|
308
342
|
def __enter__(self) -> "ConfluenceSession":
|
|
@@ -310,9 +344,7 @@ class ConfluenceAPI:
|
|
|
310
344
|
if self.properties.user_name:
|
|
311
345
|
session.auth = (self.properties.user_name, self.properties.api_key)
|
|
312
346
|
else:
|
|
313
|
-
session.headers.update(
|
|
314
|
-
{"Authorization": f"Bearer {self.properties.api_key}"}
|
|
315
|
-
)
|
|
347
|
+
session.headers.update({"Authorization": f"Bearer {self.properties.api_key}"})
|
|
316
348
|
|
|
317
349
|
if self.properties.headers:
|
|
318
350
|
session.headers.update(self.properties.headers)
|
|
@@ -366,10 +398,7 @@ class ConfluenceSession:
|
|
|
366
398
|
self.api_url = api_url
|
|
367
399
|
|
|
368
400
|
if not domain or not base_path:
|
|
369
|
-
|
|
370
|
-
ConfluenceVersion.VERSION_2, "/spaces", {"limit": "1"}
|
|
371
|
-
)
|
|
372
|
-
data = json_to_object(ConfluenceResultSet, payload)
|
|
401
|
+
data = self._get(ConfluenceVersion.VERSION_2, "/spaces", ConfluenceResultSet, query={"limit": "1"})
|
|
373
402
|
base_url = data._links.base
|
|
374
403
|
|
|
375
404
|
_, domain, base_path, _, _, _ = urlparse(base_url)
|
|
@@ -377,13 +406,9 @@ class ConfluenceSession:
|
|
|
377
406
|
base_path = f"{base_path}/"
|
|
378
407
|
|
|
379
408
|
if not domain:
|
|
380
|
-
raise ArgumentError(
|
|
381
|
-
"Confluence domain not specified and cannot be inferred"
|
|
382
|
-
)
|
|
409
|
+
raise ArgumentError("Confluence domain not specified and cannot be inferred")
|
|
383
410
|
if not base_path:
|
|
384
|
-
raise ArgumentError(
|
|
385
|
-
"Confluence base path not specified and cannot be inferred"
|
|
386
|
-
)
|
|
411
|
+
raise ArgumentError("Confluence base path not specified and cannot be inferred")
|
|
387
412
|
self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
|
|
388
413
|
if not api_url:
|
|
389
414
|
self.api_url = f"https://{self.site.domain}{self.site.base_path}"
|
|
@@ -410,12 +435,14 @@ class ConfluenceSession:
|
|
|
410
435
|
base_url = f"{self.api_url}{version.value}{path}"
|
|
411
436
|
return build_url(base_url, query)
|
|
412
437
|
|
|
413
|
-
def
|
|
438
|
+
def _get(
|
|
414
439
|
self,
|
|
415
440
|
version: ConfluenceVersion,
|
|
416
441
|
path: str,
|
|
442
|
+
response_type: type[T],
|
|
443
|
+
*,
|
|
417
444
|
query: Optional[dict[str, str]] = None,
|
|
418
|
-
) ->
|
|
445
|
+
) -> T:
|
|
419
446
|
"Executes an HTTP request via Confluence API."
|
|
420
447
|
|
|
421
448
|
url = self._build_url(version, path, query)
|
|
@@ -423,11 +450,9 @@ class ConfluenceSession:
|
|
|
423
450
|
if response.text:
|
|
424
451
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
425
452
|
response.raise_for_status()
|
|
426
|
-
return
|
|
453
|
+
return _json_to_object(response_type, response.json())
|
|
427
454
|
|
|
428
|
-
def _fetch(
|
|
429
|
-
self, path: str, query: Optional[dict[str, str]] = None
|
|
430
|
-
) -> list[JsonType]:
|
|
455
|
+
def _fetch(self, path: str, query: Optional[dict[str, str]] = None) -> list[JsonType]:
|
|
431
456
|
"Retrieves all results of a REST API v2 paginated result-set."
|
|
432
457
|
|
|
433
458
|
items: list[JsonType] = []
|
|
@@ -449,30 +474,55 @@ class ConfluenceSession:
|
|
|
449
474
|
|
|
450
475
|
return items
|
|
451
476
|
|
|
452
|
-
def
|
|
453
|
-
"
|
|
477
|
+
def _build_request(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T]) -> tuple[str, dict[str, str], bytes]:
|
|
478
|
+
"Generates URL, headers and raw payload for a typed request/response."
|
|
454
479
|
|
|
455
480
|
url = self._build_url(version, path)
|
|
481
|
+
if response_type is not type(None):
|
|
482
|
+
headers = {
|
|
483
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
484
|
+
"Accept": "application/json",
|
|
485
|
+
}
|
|
486
|
+
else:
|
|
487
|
+
headers = {
|
|
488
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
489
|
+
}
|
|
490
|
+
data = json_dump_string(object_to_json(body)).encode("utf-8")
|
|
491
|
+
return url, headers, data
|
|
492
|
+
|
|
493
|
+
def _post(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T]) -> T:
|
|
494
|
+
"Creates a new object via Confluence REST API."
|
|
495
|
+
|
|
496
|
+
url, headers, data = self._build_request(version, path, body, response_type)
|
|
497
|
+
response = self.session.post(
|
|
498
|
+
url,
|
|
499
|
+
data=data,
|
|
500
|
+
headers=headers,
|
|
501
|
+
)
|
|
502
|
+
return response_cast(response_type, response)
|
|
503
|
+
|
|
504
|
+
def _put(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T]) -> T:
|
|
505
|
+
"Updates an existing object via Confluence REST API."
|
|
506
|
+
|
|
507
|
+
url, headers, data = self._build_request(version, path, body, response_type)
|
|
456
508
|
response = self.session.put(
|
|
457
509
|
url,
|
|
458
|
-
data=
|
|
459
|
-
headers=
|
|
510
|
+
data=data,
|
|
511
|
+
headers=headers,
|
|
460
512
|
)
|
|
461
|
-
|
|
462
|
-
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
463
|
-
response.raise_for_status()
|
|
513
|
+
return response_cast(response_type, response)
|
|
464
514
|
|
|
465
515
|
def space_id_to_key(self, id: str) -> str:
|
|
466
516
|
"Finds the Confluence space key for a space ID."
|
|
467
517
|
|
|
468
518
|
key = self._space_id_to_key.get(id)
|
|
469
519
|
if key is None:
|
|
470
|
-
|
|
520
|
+
data = self._get(
|
|
471
521
|
ConfluenceVersion.VERSION_2,
|
|
472
522
|
"/spaces",
|
|
473
|
-
|
|
523
|
+
dict[str, JsonType],
|
|
524
|
+
query={"ids": id, "status": "current"},
|
|
474
525
|
)
|
|
475
|
-
data = typing.cast(dict[str, JsonType], payload)
|
|
476
526
|
results = typing.cast(list[JsonType], data["results"])
|
|
477
527
|
if len(results) != 1:
|
|
478
528
|
raise ConfluenceError(f"unique space not found with id: {id}")
|
|
@@ -489,12 +539,12 @@ class ConfluenceSession:
|
|
|
489
539
|
|
|
490
540
|
id = self._space_key_to_id.get(key)
|
|
491
541
|
if id is None:
|
|
492
|
-
|
|
542
|
+
data = self._get(
|
|
493
543
|
ConfluenceVersion.VERSION_2,
|
|
494
544
|
"/spaces",
|
|
495
|
-
|
|
545
|
+
dict[str, JsonType],
|
|
546
|
+
query={"keys": key, "status": "current"},
|
|
496
547
|
)
|
|
497
|
-
data = typing.cast(dict[str, JsonType], payload)
|
|
498
548
|
results = typing.cast(list[JsonType], data["results"])
|
|
499
549
|
if len(results) != 1:
|
|
500
550
|
raise ConfluenceError(f"unique space not found with key: {key}")
|
|
@@ -506,9 +556,7 @@ class ConfluenceSession:
|
|
|
506
556
|
|
|
507
557
|
return id
|
|
508
558
|
|
|
509
|
-
def get_space_id(
|
|
510
|
-
self, *, space_id: Optional[str] = None, space_key: Optional[str] = None
|
|
511
|
-
) -> Optional[str]:
|
|
559
|
+
def get_space_id(self, *, space_id: Optional[str] = None, space_key: Optional[str] = None) -> Optional[str]:
|
|
512
560
|
"""
|
|
513
561
|
Coalesces a space ID or space key into a space ID, accounting for site default.
|
|
514
562
|
|
|
@@ -529,17 +577,13 @@ class ConfluenceSession:
|
|
|
529
577
|
# space ID and key are unset, and no default space is configured
|
|
530
578
|
return None
|
|
531
579
|
|
|
532
|
-
def get_attachment_by_name(
|
|
533
|
-
self, page_id: str, filename: str
|
|
534
|
-
) -> ConfluenceAttachment:
|
|
580
|
+
def get_attachment_by_name(self, page_id: str, filename: str) -> ConfluenceAttachment:
|
|
535
581
|
"""
|
|
536
582
|
Retrieves a Confluence page attachment by an unprefixed file name.
|
|
537
583
|
"""
|
|
538
584
|
|
|
539
585
|
path = f"/pages/{page_id}/attachments"
|
|
540
|
-
|
|
541
|
-
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
542
|
-
data = typing.cast(dict[str, JsonType], payload)
|
|
586
|
+
data = self._get(ConfluenceVersion.VERSION_2, path, dict[str, JsonType], query={"filename": filename})
|
|
543
587
|
|
|
544
588
|
results = typing.cast(list[JsonType], data["results"])
|
|
545
589
|
if len(results) != 1:
|
|
@@ -583,6 +627,9 @@ class ConfluenceSession:
|
|
|
583
627
|
name = attachment_name
|
|
584
628
|
content_type, _ = mimetypes.guess_type(name, strict=True)
|
|
585
629
|
|
|
630
|
+
if content_type is None:
|
|
631
|
+
content_type = "application/octet-stream"
|
|
632
|
+
|
|
586
633
|
if attachment_path is not None and not attachment_path.is_file():
|
|
587
634
|
raise PageError(f"file not found: {attachment_path}")
|
|
588
635
|
|
|
@@ -610,8 +657,13 @@ class ConfluenceSession:
|
|
|
610
657
|
|
|
611
658
|
if attachment_path is not None:
|
|
612
659
|
with open(attachment_path, "rb") as attachment_file:
|
|
613
|
-
file_to_upload = {
|
|
614
|
-
"comment":
|
|
660
|
+
file_to_upload: dict[str, tuple[Optional[str], Any, str, dict[str, str]]] = {
|
|
661
|
+
"comment": (
|
|
662
|
+
None,
|
|
663
|
+
comment,
|
|
664
|
+
"text/plain; charset=utf-8",
|
|
665
|
+
{},
|
|
666
|
+
),
|
|
615
667
|
"file": (
|
|
616
668
|
attachment_name, # will truncate path component
|
|
617
669
|
attachment_file,
|
|
@@ -622,7 +674,7 @@ class ConfluenceSession:
|
|
|
622
674
|
LOGGER.info("Uploading attachment: %s", attachment_name)
|
|
623
675
|
response = self.session.post(
|
|
624
676
|
url,
|
|
625
|
-
files=file_to_upload,
|
|
677
|
+
files=file_to_upload,
|
|
626
678
|
headers={
|
|
627
679
|
"X-Atlassian-Token": "no-check",
|
|
628
680
|
"Accept": "application/json",
|
|
@@ -634,17 +686,22 @@ class ConfluenceSession:
|
|
|
634
686
|
raw_file = io.BytesIO(raw_data)
|
|
635
687
|
raw_file.name = attachment_name
|
|
636
688
|
file_to_upload = {
|
|
637
|
-
"comment":
|
|
689
|
+
"comment": (
|
|
690
|
+
None,
|
|
691
|
+
comment,
|
|
692
|
+
"text/plain; charset=utf-8",
|
|
693
|
+
{},
|
|
694
|
+
),
|
|
638
695
|
"file": (
|
|
639
696
|
attachment_name, # will truncate path component
|
|
640
|
-
raw_file,
|
|
697
|
+
raw_file,
|
|
641
698
|
content_type,
|
|
642
699
|
{"Expires": "0"},
|
|
643
700
|
),
|
|
644
701
|
}
|
|
645
702
|
response = self.session.post(
|
|
646
703
|
url,
|
|
647
|
-
files=file_to_upload,
|
|
704
|
+
files=file_to_upload,
|
|
648
705
|
headers={
|
|
649
706
|
"X-Atlassian-Token": "no-check",
|
|
650
707
|
"Accept": "application/json",
|
|
@@ -667,9 +724,7 @@ class ConfluenceSession:
|
|
|
667
724
|
# ensure path component is retained in attachment name
|
|
668
725
|
self._update_attachment(page_id, attachment_id, version, attachment_name)
|
|
669
726
|
|
|
670
|
-
def _update_attachment(
|
|
671
|
-
self, page_id: str, attachment_id: str, version: int, attachment_title: str
|
|
672
|
-
) -> None:
|
|
727
|
+
def _update_attachment(self, page_id: str, attachment_id: str, version: int, attachment_title: str) -> None:
|
|
673
728
|
id = attachment_id.removeprefix("att")
|
|
674
729
|
path = f"/content/{page_id}/child/attachment/{id}"
|
|
675
730
|
request = ConfluenceUpdateAttachmentRequest(
|
|
@@ -681,7 +736,7 @@ class ConfluenceSession:
|
|
|
681
736
|
)
|
|
682
737
|
|
|
683
738
|
LOGGER.info("Updating attachment: %s", attachment_id)
|
|
684
|
-
self.
|
|
739
|
+
self._put(ConfluenceVersion.VERSION_1, path, request, type(None))
|
|
685
740
|
|
|
686
741
|
def get_page_properties_by_title(
|
|
687
742
|
self,
|
|
@@ -708,8 +763,7 @@ class ConfluenceSession:
|
|
|
708
763
|
if space_id is not None:
|
|
709
764
|
query["space-id"] = space_id
|
|
710
765
|
|
|
711
|
-
|
|
712
|
-
data = typing.cast(dict[str, JsonType], payload)
|
|
766
|
+
data = self._get(ConfluenceVersion.VERSION_2, path, dict[str, JsonType], query=query)
|
|
713
767
|
results = typing.cast(list[JsonType], data["results"])
|
|
714
768
|
if len(results) != 1:
|
|
715
769
|
raise ConfluenceError(f"unique page not found with title: {title}")
|
|
@@ -726,11 +780,8 @@ class ConfluenceSession:
|
|
|
726
780
|
"""
|
|
727
781
|
|
|
728
782
|
path = f"/pages/{page_id}"
|
|
729
|
-
query
|
|
730
|
-
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
731
|
-
return _json_to_object(ConfluencePage, payload)
|
|
783
|
+
return self._get(ConfluenceVersion.VERSION_2, path, ConfluencePage, query={"body-format": "storage"})
|
|
732
784
|
|
|
733
|
-
@functools.cache
|
|
734
785
|
def get_page_properties(self, page_id: str) -> ConfluencePageProperties:
|
|
735
786
|
"""
|
|
736
787
|
Retrieves Confluence wiki page details.
|
|
@@ -740,8 +791,7 @@ class ConfluenceSession:
|
|
|
740
791
|
"""
|
|
741
792
|
|
|
742
793
|
path = f"/pages/{page_id}"
|
|
743
|
-
|
|
744
|
-
return _json_to_object(ConfluencePageProperties, payload)
|
|
794
|
+
return self._get(ConfluenceVersion.VERSION_2, path, ConfluencePageProperties)
|
|
745
795
|
|
|
746
796
|
def get_page_version(self, page_id: str) -> int:
|
|
747
797
|
"""
|
|
@@ -756,45 +806,30 @@ class ConfluenceSession:
|
|
|
756
806
|
def update_page(
|
|
757
807
|
self,
|
|
758
808
|
page_id: str,
|
|
759
|
-
|
|
809
|
+
content: str,
|
|
760
810
|
*,
|
|
761
|
-
title:
|
|
811
|
+
title: str,
|
|
812
|
+
version: int,
|
|
762
813
|
) -> None:
|
|
763
814
|
"""
|
|
764
815
|
Updates a page via the Confluence API.
|
|
765
816
|
|
|
766
817
|
:param page_id: The Confluence page ID.
|
|
767
|
-
:param
|
|
818
|
+
:param content: Confluence Storage Format XHTML.
|
|
768
819
|
:param title: New title to assign to the page. Needs to be unique within a space.
|
|
820
|
+
:param version: New version to assign to the page.
|
|
769
821
|
"""
|
|
770
822
|
|
|
771
|
-
page = self.get_page(page_id)
|
|
772
|
-
new_title = title or page.title
|
|
773
|
-
|
|
774
|
-
try:
|
|
775
|
-
old_content = sanitize_confluence(page.content)
|
|
776
|
-
if page.title == new_title and old_content == new_content:
|
|
777
|
-
LOGGER.info("Up-to-date page: %s", page_id)
|
|
778
|
-
return
|
|
779
|
-
except ParseError as exc:
|
|
780
|
-
LOGGER.warning(exc)
|
|
781
|
-
|
|
782
823
|
path = f"/pages/{page_id}"
|
|
783
824
|
request = ConfluenceUpdatePageRequest(
|
|
784
825
|
id=page_id,
|
|
785
826
|
status=ConfluenceStatus.CURRENT,
|
|
786
|
-
title=
|
|
787
|
-
body=ConfluencePageBody(
|
|
788
|
-
|
|
789
|
-
representation=ConfluenceRepresentation.STORAGE, value=new_content
|
|
790
|
-
)
|
|
791
|
-
),
|
|
792
|
-
version=ConfluenceContentVersion(
|
|
793
|
-
number=page.version.number + 1, minorEdit=True
|
|
794
|
-
),
|
|
827
|
+
title=title,
|
|
828
|
+
body=ConfluencePageBody(storage=ConfluencePageStorage(representation=ConfluenceRepresentation.STORAGE, value=content)),
|
|
829
|
+
version=ConfluenceContentVersion(number=version, minorEdit=True),
|
|
795
830
|
)
|
|
796
831
|
LOGGER.info("Updating page: %s", page_id)
|
|
797
|
-
self.
|
|
832
|
+
self._put(ConfluenceVersion.VERSION_2, path, request, type(None))
|
|
798
833
|
|
|
799
834
|
def create_page(
|
|
800
835
|
self,
|
|
@@ -827,9 +862,9 @@ class ConfluenceSession:
|
|
|
827
862
|
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
828
863
|
response = self.session.post(
|
|
829
864
|
url,
|
|
830
|
-
data=json_dump_string(object_to_json(request)),
|
|
865
|
+
data=json_dump_string(object_to_json(request)).encode("utf-8"),
|
|
831
866
|
headers={
|
|
832
|
-
"Content-Type": "application/json",
|
|
867
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
833
868
|
"Accept": "application/json",
|
|
834
869
|
},
|
|
835
870
|
)
|
|
@@ -889,7 +924,7 @@ class ConfluenceSession:
|
|
|
889
924
|
url,
|
|
890
925
|
params=query,
|
|
891
926
|
headers={
|
|
892
|
-
"Content-Type": "application/json",
|
|
927
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
893
928
|
"Accept": "application/json",
|
|
894
929
|
},
|
|
895
930
|
)
|
|
@@ -941,19 +976,7 @@ class ConfluenceSession:
|
|
|
941
976
|
"""
|
|
942
977
|
|
|
943
978
|
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()
|
|
979
|
+
self._post(ConfluenceVersion.VERSION_1, path, labels, type(None))
|
|
957
980
|
|
|
958
981
|
def remove_labels(self, page_id: str, labels: list[ConfluenceLabel]) -> None:
|
|
959
982
|
"""
|
|
@@ -973,7 +996,7 @@ class ConfluenceSession:
|
|
|
973
996
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
974
997
|
response.raise_for_status()
|
|
975
998
|
|
|
976
|
-
def update_labels(self, page_id: str, labels: list[ConfluenceLabel]) -> None:
|
|
999
|
+
def update_labels(self, page_id: str, labels: list[ConfluenceLabel], *, keep_existing: bool = False) -> None:
|
|
977
1000
|
"""
|
|
978
1001
|
Assigns the specified labels to a Confluence page. Existing labels are removed.
|
|
979
1002
|
|
|
@@ -982,10 +1005,7 @@ class ConfluenceSession:
|
|
|
982
1005
|
"""
|
|
983
1006
|
|
|
984
1007
|
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
|
-
)
|
|
1008
|
+
old_labels = set(ConfluenceLabel(name=label.name, prefix=label.prefix) for label in self.get_labels(page_id))
|
|
989
1009
|
|
|
990
1010
|
add_labels = list(new_labels - old_labels)
|
|
991
1011
|
remove_labels = list(old_labels - new_labels)
|
|
@@ -993,6 +1013,103 @@ class ConfluenceSession:
|
|
|
993
1013
|
if add_labels:
|
|
994
1014
|
add_labels.sort()
|
|
995
1015
|
self.add_labels(page_id, add_labels)
|
|
996
|
-
if remove_labels:
|
|
1016
|
+
if not keep_existing and remove_labels:
|
|
997
1017
|
remove_labels.sort()
|
|
998
1018
|
self.remove_labels(page_id, remove_labels)
|
|
1019
|
+
|
|
1020
|
+
def get_content_properties_for_page(self, page_id: str) -> list[ConfluenceIdentifiedContentProperty]:
|
|
1021
|
+
"""
|
|
1022
|
+
Retrieves content properties for a Confluence page.
|
|
1023
|
+
|
|
1024
|
+
:param page_id: The Confluence page ID.
|
|
1025
|
+
:returns: A list of content properties.
|
|
1026
|
+
"""
|
|
1027
|
+
|
|
1028
|
+
path = f"/pages/{page_id}/properties"
|
|
1029
|
+
results = self._fetch(path)
|
|
1030
|
+
return _json_to_object(list[ConfluenceIdentifiedContentProperty], results)
|
|
1031
|
+
|
|
1032
|
+
def add_content_property_to_page(self, page_id: str, property: ConfluenceContentProperty) -> ConfluenceIdentifiedContentProperty:
|
|
1033
|
+
"""
|
|
1034
|
+
Adds a new content property to a Confluence page.
|
|
1035
|
+
|
|
1036
|
+
:param page_id: The Confluence page ID.
|
|
1037
|
+
:param property: Content property to add.
|
|
1038
|
+
"""
|
|
1039
|
+
|
|
1040
|
+
path = f"/pages/{page_id}/properties"
|
|
1041
|
+
return self._post(ConfluenceVersion.VERSION_2, path, property, ConfluenceIdentifiedContentProperty)
|
|
1042
|
+
|
|
1043
|
+
def remove_content_property_from_page(self, page_id: str, property_id: str) -> None:
|
|
1044
|
+
"""
|
|
1045
|
+
Removes a content property from a Confluence page.
|
|
1046
|
+
|
|
1047
|
+
:param page_id: The Confluence page ID.
|
|
1048
|
+
:param property_id: Property ID, which uniquely identifies the property.
|
|
1049
|
+
"""
|
|
1050
|
+
|
|
1051
|
+
path = f"/pages/{page_id}/properties/{property_id}"
|
|
1052
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
1053
|
+
response = self.session.delete(url)
|
|
1054
|
+
response.raise_for_status()
|
|
1055
|
+
|
|
1056
|
+
def update_content_property_for_page(
|
|
1057
|
+
self, page_id: str, property_id: str, version: int, property: ConfluenceContentProperty
|
|
1058
|
+
) -> ConfluenceIdentifiedContentProperty:
|
|
1059
|
+
"""
|
|
1060
|
+
Updates an existing content property associated with a Confluence page.
|
|
1061
|
+
|
|
1062
|
+
:param page_id: The Confluence page ID.
|
|
1063
|
+
:param property_id: Property ID, which uniquely identifies the property.
|
|
1064
|
+
:param version: Version number to assign.
|
|
1065
|
+
:param property: Content property data to assign.
|
|
1066
|
+
:returns: Updated content property data.
|
|
1067
|
+
"""
|
|
1068
|
+
|
|
1069
|
+
path = f"/pages/{page_id}/properties/{property_id}"
|
|
1070
|
+
return self._put(
|
|
1071
|
+
ConfluenceVersion.VERSION_2,
|
|
1072
|
+
path,
|
|
1073
|
+
ConfluenceVersionedContentProperty(
|
|
1074
|
+
key=property.key,
|
|
1075
|
+
value=property.value,
|
|
1076
|
+
version=ConfluenceContentVersion(number=version),
|
|
1077
|
+
),
|
|
1078
|
+
ConfluenceIdentifiedContentProperty,
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
def update_content_properties_for_page(self, page_id: str, properties: list[ConfluenceContentProperty], *, keep_existing: bool = False) -> None:
|
|
1082
|
+
"""
|
|
1083
|
+
Updates content properties associated with a Confluence page.
|
|
1084
|
+
|
|
1085
|
+
:param page_id: The Confluence page ID.
|
|
1086
|
+
:param properties: A list of content property data to update.
|
|
1087
|
+
:param keep_existing: Whether to keep content property data whose key is not included in the list of properties passed as an argument.
|
|
1088
|
+
"""
|
|
1089
|
+
|
|
1090
|
+
old_mapping = {p.key: p for p in self.get_content_properties_for_page(page_id)}
|
|
1091
|
+
new_mapping = {p.key: p for p in properties}
|
|
1092
|
+
|
|
1093
|
+
new_props = set(p.key for p in properties)
|
|
1094
|
+
old_props = set(old_mapping.keys())
|
|
1095
|
+
|
|
1096
|
+
add_props = list(new_props - old_props)
|
|
1097
|
+
remove_props = list(old_props - new_props)
|
|
1098
|
+
update_props = list(old_props & new_props)
|
|
1099
|
+
|
|
1100
|
+
if add_props:
|
|
1101
|
+
add_props.sort()
|
|
1102
|
+
for key in add_props:
|
|
1103
|
+
self.add_content_property_to_page(page_id, new_mapping[key])
|
|
1104
|
+
if not keep_existing and remove_props:
|
|
1105
|
+
remove_props.sort()
|
|
1106
|
+
for key in remove_props:
|
|
1107
|
+
self.remove_content_property_from_page(page_id, old_mapping[key].id)
|
|
1108
|
+
if update_props:
|
|
1109
|
+
update_props.sort()
|
|
1110
|
+
for key in update_props:
|
|
1111
|
+
old_prop = old_mapping[key]
|
|
1112
|
+
new_prop = new_mapping[key]
|
|
1113
|
+
if old_prop.value == new_prop.value:
|
|
1114
|
+
continue
|
|
1115
|
+
self.update_content_property_for_page(page_id, old_prop.id, old_prop.version.number + 1, new_prop)
|