markdown-to-confluence 0.4.1__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.1.dist-info → markdown_to_confluence-0.4.2.dist-info}/METADATA +101 -33
- markdown_to_confluence-0.4.2.dist-info/RECORD +27 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +53 -6
- md2conf/api.py +84 -94
- md2conf/application.py +17 -2
- md2conf/collection.py +17 -11
- md2conf/converter.py +477 -58
- md2conf/drawio.py +222 -0
- md2conf/local.py +1 -0
- md2conf/matcher.py +63 -4
- md2conf/metadata.py +2 -0
- md2conf/processor.py +37 -28
- md2conf/scanner.py +7 -0
- md2conf/xml.py +70 -0
- markdown_to_confluence-0.4.1.dist-info/RECORD +0 -25
- {markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.2.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.2.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.2.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.1.dist-info → markdown_to_confluence-0.4.2.dist-info}/zip-safe +0 -0
md2conf/api.py
CHANGED
|
@@ -22,7 +22,6 @@ import requests
|
|
|
22
22
|
from strong_typing.core import JsonType
|
|
23
23
|
from strong_typing.serialization import DeserializerOptions, json_dump_string, json_to_object, object_to_json
|
|
24
24
|
|
|
25
|
-
from .converter import ParseError, sanitize_confluence
|
|
26
25
|
from .metadata import ConfluenceSiteMetadata
|
|
27
26
|
from .properties import ArgumentError, ConfluenceConnectionProperties, ConfluenceError, PageError
|
|
28
27
|
|
|
@@ -55,6 +54,18 @@ def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
|
|
|
55
54
|
LOGGER = logging.getLogger(__name__)
|
|
56
55
|
|
|
57
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
|
+
|
|
58
69
|
@enum.unique
|
|
59
70
|
class ConfluenceVersion(enum.Enum):
|
|
60
71
|
"""
|
|
@@ -150,7 +161,7 @@ class ConfluenceAttachment:
|
|
|
150
161
|
createdAt: datetime.datetime
|
|
151
162
|
pageId: str
|
|
152
163
|
mediaType: str
|
|
153
|
-
mediaTypeDescription: str
|
|
164
|
+
mediaTypeDescription: Optional[str]
|
|
154
165
|
comment: Optional[str]
|
|
155
166
|
fileId: str
|
|
156
167
|
fileSize: int
|
|
@@ -387,8 +398,7 @@ class ConfluenceSession:
|
|
|
387
398
|
self.api_url = api_url
|
|
388
399
|
|
|
389
400
|
if not domain or not base_path:
|
|
390
|
-
|
|
391
|
-
data = json_to_object(ConfluenceResultSet, payload)
|
|
401
|
+
data = self._get(ConfluenceVersion.VERSION_2, "/spaces", ConfluenceResultSet, query={"limit": "1"})
|
|
392
402
|
base_url = data._links.base
|
|
393
403
|
|
|
394
404
|
_, domain, base_path, _, _, _ = urlparse(base_url)
|
|
@@ -425,12 +435,14 @@ class ConfluenceSession:
|
|
|
425
435
|
base_url = f"{self.api_url}{version.value}{path}"
|
|
426
436
|
return build_url(base_url, query)
|
|
427
437
|
|
|
428
|
-
def
|
|
438
|
+
def _get(
|
|
429
439
|
self,
|
|
430
440
|
version: ConfluenceVersion,
|
|
431
441
|
path: str,
|
|
442
|
+
response_type: type[T],
|
|
443
|
+
*,
|
|
432
444
|
query: Optional[dict[str, str]] = None,
|
|
433
|
-
) ->
|
|
445
|
+
) -> T:
|
|
434
446
|
"Executes an HTTP request via Confluence API."
|
|
435
447
|
|
|
436
448
|
url = self._build_url(version, path, query)
|
|
@@ -438,7 +450,7 @@ class ConfluenceSession:
|
|
|
438
450
|
if response.text:
|
|
439
451
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
440
452
|
response.raise_for_status()
|
|
441
|
-
return
|
|
453
|
+
return _json_to_object(response_type, response.json())
|
|
442
454
|
|
|
443
455
|
def _fetch(self, path: str, query: Optional[dict[str, str]] = None) -> list[JsonType]:
|
|
444
456
|
"Retrieves all results of a REST API v2 paginated result-set."
|
|
@@ -462,30 +474,55 @@ class ConfluenceSession:
|
|
|
462
474
|
|
|
463
475
|
return items
|
|
464
476
|
|
|
465
|
-
def
|
|
466
|
-
"
|
|
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."
|
|
467
479
|
|
|
468
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)
|
|
469
508
|
response = self.session.put(
|
|
470
509
|
url,
|
|
471
|
-
data=
|
|
472
|
-
headers=
|
|
510
|
+
data=data,
|
|
511
|
+
headers=headers,
|
|
473
512
|
)
|
|
474
|
-
|
|
475
|
-
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
476
|
-
response.raise_for_status()
|
|
513
|
+
return response_cast(response_type, response)
|
|
477
514
|
|
|
478
515
|
def space_id_to_key(self, id: str) -> str:
|
|
479
516
|
"Finds the Confluence space key for a space ID."
|
|
480
517
|
|
|
481
518
|
key = self._space_id_to_key.get(id)
|
|
482
519
|
if key is None:
|
|
483
|
-
|
|
520
|
+
data = self._get(
|
|
484
521
|
ConfluenceVersion.VERSION_2,
|
|
485
522
|
"/spaces",
|
|
486
|
-
|
|
523
|
+
dict[str, JsonType],
|
|
524
|
+
query={"ids": id, "status": "current"},
|
|
487
525
|
)
|
|
488
|
-
data = typing.cast(dict[str, JsonType], payload)
|
|
489
526
|
results = typing.cast(list[JsonType], data["results"])
|
|
490
527
|
if len(results) != 1:
|
|
491
528
|
raise ConfluenceError(f"unique space not found with id: {id}")
|
|
@@ -502,12 +539,12 @@ class ConfluenceSession:
|
|
|
502
539
|
|
|
503
540
|
id = self._space_key_to_id.get(key)
|
|
504
541
|
if id is None:
|
|
505
|
-
|
|
542
|
+
data = self._get(
|
|
506
543
|
ConfluenceVersion.VERSION_2,
|
|
507
544
|
"/spaces",
|
|
508
|
-
|
|
545
|
+
dict[str, JsonType],
|
|
546
|
+
query={"keys": key, "status": "current"},
|
|
509
547
|
)
|
|
510
|
-
data = typing.cast(dict[str, JsonType], payload)
|
|
511
548
|
results = typing.cast(list[JsonType], data["results"])
|
|
512
549
|
if len(results) != 1:
|
|
513
550
|
raise ConfluenceError(f"unique space not found with key: {key}")
|
|
@@ -546,9 +583,7 @@ class ConfluenceSession:
|
|
|
546
583
|
"""
|
|
547
584
|
|
|
548
585
|
path = f"/pages/{page_id}/attachments"
|
|
549
|
-
|
|
550
|
-
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
551
|
-
data = typing.cast(dict[str, JsonType], payload)
|
|
586
|
+
data = self._get(ConfluenceVersion.VERSION_2, path, dict[str, JsonType], query={"filename": filename})
|
|
552
587
|
|
|
553
588
|
results = typing.cast(list[JsonType], data["results"])
|
|
554
589
|
if len(results) != 1:
|
|
@@ -701,7 +736,7 @@ class ConfluenceSession:
|
|
|
701
736
|
)
|
|
702
737
|
|
|
703
738
|
LOGGER.info("Updating attachment: %s", attachment_id)
|
|
704
|
-
self.
|
|
739
|
+
self._put(ConfluenceVersion.VERSION_1, path, request, type(None))
|
|
705
740
|
|
|
706
741
|
def get_page_properties_by_title(
|
|
707
742
|
self,
|
|
@@ -728,8 +763,7 @@ class ConfluenceSession:
|
|
|
728
763
|
if space_id is not None:
|
|
729
764
|
query["space-id"] = space_id
|
|
730
765
|
|
|
731
|
-
|
|
732
|
-
data = typing.cast(dict[str, JsonType], payload)
|
|
766
|
+
data = self._get(ConfluenceVersion.VERSION_2, path, dict[str, JsonType], query=query)
|
|
733
767
|
results = typing.cast(list[JsonType], data["results"])
|
|
734
768
|
if len(results) != 1:
|
|
735
769
|
raise ConfluenceError(f"unique page not found with title: {title}")
|
|
@@ -746,9 +780,7 @@ class ConfluenceSession:
|
|
|
746
780
|
"""
|
|
747
781
|
|
|
748
782
|
path = f"/pages/{page_id}"
|
|
749
|
-
query
|
|
750
|
-
payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
|
|
751
|
-
return _json_to_object(ConfluencePage, payload)
|
|
783
|
+
return self._get(ConfluenceVersion.VERSION_2, path, ConfluencePage, query={"body-format": "storage"})
|
|
752
784
|
|
|
753
785
|
def get_page_properties(self, page_id: str) -> ConfluencePageProperties:
|
|
754
786
|
"""
|
|
@@ -759,8 +791,7 @@ class ConfluenceSession:
|
|
|
759
791
|
"""
|
|
760
792
|
|
|
761
793
|
path = f"/pages/{page_id}"
|
|
762
|
-
|
|
763
|
-
return _json_to_object(ConfluencePageProperties, payload)
|
|
794
|
+
return self._get(ConfluenceVersion.VERSION_2, path, ConfluencePageProperties)
|
|
764
795
|
|
|
765
796
|
def get_page_version(self, page_id: str) -> int:
|
|
766
797
|
"""
|
|
@@ -775,39 +806,30 @@ class ConfluenceSession:
|
|
|
775
806
|
def update_page(
|
|
776
807
|
self,
|
|
777
808
|
page_id: str,
|
|
778
|
-
|
|
809
|
+
content: str,
|
|
779
810
|
*,
|
|
780
|
-
title:
|
|
811
|
+
title: str,
|
|
812
|
+
version: int,
|
|
781
813
|
) -> None:
|
|
782
814
|
"""
|
|
783
815
|
Updates a page via the Confluence API.
|
|
784
816
|
|
|
785
817
|
:param page_id: The Confluence page ID.
|
|
786
|
-
:param
|
|
818
|
+
:param content: Confluence Storage Format XHTML.
|
|
787
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.
|
|
788
821
|
"""
|
|
789
822
|
|
|
790
|
-
page = self.get_page(page_id)
|
|
791
|
-
new_title = title or page.title
|
|
792
|
-
|
|
793
|
-
try:
|
|
794
|
-
old_content = sanitize_confluence(page.content)
|
|
795
|
-
if page.title == new_title and old_content == new_content:
|
|
796
|
-
LOGGER.info("Up-to-date page: %s", page_id)
|
|
797
|
-
return
|
|
798
|
-
except ParseError as exc:
|
|
799
|
-
LOGGER.warning(exc)
|
|
800
|
-
|
|
801
823
|
path = f"/pages/{page_id}"
|
|
802
824
|
request = ConfluenceUpdatePageRequest(
|
|
803
825
|
id=page_id,
|
|
804
826
|
status=ConfluenceStatus.CURRENT,
|
|
805
|
-
title=
|
|
806
|
-
body=ConfluencePageBody(storage=ConfluencePageStorage(representation=ConfluenceRepresentation.STORAGE, value=
|
|
807
|
-
version=ConfluenceContentVersion(number=
|
|
827
|
+
title=title,
|
|
828
|
+
body=ConfluencePageBody(storage=ConfluencePageStorage(representation=ConfluenceRepresentation.STORAGE, value=content)),
|
|
829
|
+
version=ConfluenceContentVersion(number=version, minorEdit=True),
|
|
808
830
|
)
|
|
809
831
|
LOGGER.info("Updating page: %s", page_id)
|
|
810
|
-
self.
|
|
832
|
+
self._put(ConfluenceVersion.VERSION_2, path, request, type(None))
|
|
811
833
|
|
|
812
834
|
def create_page(
|
|
813
835
|
self,
|
|
@@ -840,9 +862,9 @@ class ConfluenceSession:
|
|
|
840
862
|
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
841
863
|
response = self.session.post(
|
|
842
864
|
url,
|
|
843
|
-
data=json_dump_string(object_to_json(request)),
|
|
865
|
+
data=json_dump_string(object_to_json(request)).encode("utf-8"),
|
|
844
866
|
headers={
|
|
845
|
-
"Content-Type": "application/json",
|
|
867
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
846
868
|
"Accept": "application/json",
|
|
847
869
|
},
|
|
848
870
|
)
|
|
@@ -902,7 +924,7 @@ class ConfluenceSession:
|
|
|
902
924
|
url,
|
|
903
925
|
params=query,
|
|
904
926
|
headers={
|
|
905
|
-
"Content-Type": "application/json",
|
|
927
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
906
928
|
"Accept": "application/json",
|
|
907
929
|
},
|
|
908
930
|
)
|
|
@@ -954,19 +976,7 @@ class ConfluenceSession:
|
|
|
954
976
|
"""
|
|
955
977
|
|
|
956
978
|
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()
|
|
979
|
+
self._post(ConfluenceVersion.VERSION_1, path, labels, type(None))
|
|
970
980
|
|
|
971
981
|
def remove_labels(self, page_id: str, labels: list[ConfluenceLabel]) -> None:
|
|
972
982
|
"""
|
|
@@ -1028,19 +1038,7 @@ class ConfluenceSession:
|
|
|
1028
1038
|
"""
|
|
1029
1039
|
|
|
1030
1040
|
path = f"/pages/{page_id}/properties"
|
|
1031
|
-
|
|
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())
|
|
1041
|
+
return self._post(ConfluenceVersion.VERSION_2, path, property, ConfluenceIdentifiedContentProperty)
|
|
1044
1042
|
|
|
1045
1043
|
def remove_content_property_from_page(self, page_id: str, property_id: str) -> None:
|
|
1046
1044
|
"""
|
|
@@ -1069,24 +1067,16 @@ class ConfluenceSession:
|
|
|
1069
1067
|
"""
|
|
1070
1068
|
|
|
1071
1069
|
path = f"/pages/{page_id}/properties/{property_id}"
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
value=property.value,
|
|
1080
|
-
version=ConfluenceContentVersion(number=version),
|
|
1081
|
-
)
|
|
1082
|
-
)
|
|
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),
|
|
1083
1077
|
),
|
|
1084
|
-
|
|
1078
|
+
ConfluenceIdentifiedContentProperty,
|
|
1085
1079
|
)
|
|
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
1080
|
|
|
1091
1081
|
def update_content_properties_for_page(self, page_id: str, properties: list[ConfluenceContentProperty], *, keep_existing: bool = False) -> None:
|
|
1092
1082
|
"""
|
md2conf/application.py
CHANGED
|
@@ -11,11 +11,12 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Optional
|
|
12
12
|
|
|
13
13
|
from .api import ConfluenceContentProperty, ConfluenceLabel, ConfluenceSession, ConfluenceStatus
|
|
14
|
-
from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID, attachment_name
|
|
14
|
+
from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID, attachment_name, elements_from_string, get_volatile_attributes
|
|
15
15
|
from .extra import override, path_relative_to
|
|
16
16
|
from .metadata import ConfluencePageMetadata
|
|
17
17
|
from .processor import Converter, DocumentNode, Processor, ProcessorFactory
|
|
18
18
|
from .properties import PageError
|
|
19
|
+
from .xml import is_xml_equal
|
|
19
20
|
|
|
20
21
|
LOGGER = logging.getLogger(__name__)
|
|
21
22
|
|
|
@@ -97,6 +98,7 @@ class SynchronizingProcessor(Processor):
|
|
|
97
98
|
page_id=page.id,
|
|
98
99
|
space_key=space_key,
|
|
99
100
|
title=page.title,
|
|
101
|
+
synchronized=node.synchronized,
|
|
100
102
|
)
|
|
101
103
|
self.page_metadata.add(node.absolute_path, data)
|
|
102
104
|
|
|
@@ -143,7 +145,20 @@ class SynchronizingProcessor(Processor):
|
|
|
143
145
|
conflicting_page_id,
|
|
144
146
|
)
|
|
145
147
|
|
|
146
|
-
|
|
148
|
+
# fetch existing page
|
|
149
|
+
page = self.api.get_page(page_id.page_id)
|
|
150
|
+
if not title: # empty or `None`
|
|
151
|
+
title = page.title
|
|
152
|
+
|
|
153
|
+
# check if page has any changes
|
|
154
|
+
if page.title != title or not is_xml_equal(
|
|
155
|
+
document.root,
|
|
156
|
+
elements_from_string(page.content),
|
|
157
|
+
skip_attributes=get_volatile_attributes(),
|
|
158
|
+
):
|
|
159
|
+
self.api.update_page(page_id.page_id, content, title=title, version=page.version.number + 1)
|
|
160
|
+
else:
|
|
161
|
+
LOGGER.info("Up-to-date page: %s", page_id)
|
|
147
162
|
|
|
148
163
|
if document.labels is not None:
|
|
149
164
|
self.api.update_labels(
|
md2conf/collection.py
CHANGED
|
@@ -7,25 +7,31 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Iterable, Optional
|
|
10
|
+
from typing import Generic, Iterable, Optional, TypeVar
|
|
11
11
|
|
|
12
12
|
from .metadata import ConfluencePageMetadata
|
|
13
13
|
|
|
14
|
+
K = TypeVar("K")
|
|
15
|
+
V = TypeVar("V")
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
|
|
18
|
+
class KeyValueCollection(Generic[K, V]):
|
|
19
|
+
_collection: dict[K, V]
|
|
17
20
|
|
|
18
21
|
def __init__(self) -> None:
|
|
19
|
-
self.
|
|
22
|
+
self._collection = {}
|
|
20
23
|
|
|
21
24
|
def __len__(self) -> int:
|
|
22
|
-
return len(self.
|
|
25
|
+
return len(self._collection)
|
|
26
|
+
|
|
27
|
+
def add(self, key: K, data: V) -> None:
|
|
28
|
+
self._collection[key] = data
|
|
29
|
+
|
|
30
|
+
def get(self, key: K) -> Optional[V]:
|
|
31
|
+
return self._collection.get(key)
|
|
23
32
|
|
|
24
|
-
def
|
|
25
|
-
self.
|
|
33
|
+
def items(self) -> Iterable[tuple[K, V]]:
|
|
34
|
+
return self._collection.items()
|
|
26
35
|
|
|
27
|
-
def get(self, path: Path) -> Optional[ConfluencePageMetadata]:
|
|
28
|
-
return self._metadata.get(path)
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
return self._metadata.items()
|
|
37
|
+
class ConfluencePageCollection(KeyValueCollection[Path, ConfluencePageMetadata]): ...
|