markdown-to-confluence 0.4.5__py3-none-any.whl → 0.4.7__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.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/METADATA +38 -13
- markdown_to_confluence-0.4.7.dist-info/RECORD +34 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +34 -36
- md2conf/api.py +121 -43
- md2conf/converter.py +312 -165
- md2conf/csf.py +10 -8
- md2conf/domain.py +4 -0
- md2conf/drawio.py +5 -3
- md2conf/emoticon.py +22 -0
- md2conf/latex.py +2 -2
- md2conf/matcher.py +1 -3
- md2conf/mermaid.py +18 -3
- md2conf/processor.py +2 -2
- md2conf/{application.py → publisher.py} +15 -12
- md2conf/scanner.py +57 -3
- md2conf/xml.py +8 -5
- markdown_to_confluence-0.4.5.dist-info/RECORD +0 -33
- {markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.5.dist-info → markdown_to_confluence-0.4.7.dist-info}/zip-safe +0 -0
- /md2conf/{properties.py → environment.py} +0 -0
md2conf/api.py
CHANGED
|
@@ -11,19 +11,28 @@ import enum
|
|
|
11
11
|
import io
|
|
12
12
|
import logging
|
|
13
13
|
import mimetypes
|
|
14
|
+
import ssl
|
|
15
|
+
import sys
|
|
14
16
|
import typing
|
|
15
17
|
from dataclasses import dataclass
|
|
16
18
|
from pathlib import Path
|
|
17
19
|
from types import TracebackType
|
|
18
|
-
from typing import Any, Optional, TypeVar
|
|
20
|
+
from typing import Any, Optional, TypeVar, overload
|
|
19
21
|
from urllib.parse import urlencode, urlparse, urlunparse
|
|
20
22
|
|
|
21
23
|
import requests
|
|
24
|
+
from requests.adapters import HTTPAdapter
|
|
22
25
|
from strong_typing.core import JsonType
|
|
23
26
|
from strong_typing.serialization import DeserializerOptions, json_dump_string, json_to_object, object_to_json
|
|
24
27
|
|
|
28
|
+
from .environment import ArgumentError, ConfluenceConnectionProperties, ConfluenceError, PageError
|
|
29
|
+
from .extra import override
|
|
25
30
|
from .metadata import ConfluenceSiteMetadata
|
|
26
|
-
|
|
31
|
+
|
|
32
|
+
if sys.version_info >= (3, 10):
|
|
33
|
+
import truststore
|
|
34
|
+
else:
|
|
35
|
+
import certifi
|
|
27
36
|
|
|
28
37
|
T = TypeVar("T")
|
|
29
38
|
|
|
@@ -62,16 +71,24 @@ def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
|
|
|
62
71
|
LOGGER = logging.getLogger(__name__)
|
|
63
72
|
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
@overload
|
|
75
|
+
def response_cast(response_type: None, response: requests.Response) -> None: ...
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@overload
|
|
79
|
+
def response_cast(response_type: type[T], response: requests.Response) -> T: ...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def response_cast(response_type: Optional[type[T]], response: requests.Response) -> Optional[T]:
|
|
66
83
|
"Converts a response body into the expected type."
|
|
67
84
|
|
|
68
85
|
if response.text:
|
|
69
86
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
70
87
|
response.raise_for_status()
|
|
71
|
-
if response_type is
|
|
72
|
-
return _json_to_object(response_type, response.json())
|
|
73
|
-
else:
|
|
88
|
+
if response_type is None:
|
|
74
89
|
return None
|
|
90
|
+
else:
|
|
91
|
+
return _json_to_object(response_type, response.json())
|
|
75
92
|
|
|
76
93
|
|
|
77
94
|
@enum.unique
|
|
@@ -336,9 +353,33 @@ class ConfluenceUpdateAttachmentRequest:
|
|
|
336
353
|
version: ConfluenceContentVersion
|
|
337
354
|
|
|
338
355
|
|
|
356
|
+
class TruststoreAdapter(HTTPAdapter):
|
|
357
|
+
"""
|
|
358
|
+
Provides a general-case interface for HTTPS sessions to connect to HTTPS URLs.
|
|
359
|
+
|
|
360
|
+
This class implements the Transport Adapter interface in the Python library `requests`.
|
|
361
|
+
|
|
362
|
+
This class will usually be created by the :class:`requests.Session` class under the covers.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
@override
|
|
366
|
+
def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **pool_kwargs: Any) -> None:
|
|
367
|
+
"""
|
|
368
|
+
Adapts the pool manager to use the provided SSL context instead of the default.
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
if sys.version_info >= (3, 10):
|
|
372
|
+
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
373
|
+
else:
|
|
374
|
+
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
|
|
375
|
+
ctx.check_hostname = True
|
|
376
|
+
ctx.verify_mode = ssl.CERT_REQUIRED
|
|
377
|
+
super().init_poolmanager(connections, maxsize, block, ssl_context=ctx, **pool_kwargs) # type: ignore[no-untyped-call]
|
|
378
|
+
|
|
379
|
+
|
|
339
380
|
class ConfluenceAPI:
|
|
340
381
|
"""
|
|
341
|
-
|
|
382
|
+
Encapsulates operations that can be invoked via the [Confluence REST API](https://developer.atlassian.com/cloud/confluence/rest/v2/).
|
|
342
383
|
"""
|
|
343
384
|
|
|
344
385
|
properties: ConfluenceConnectionProperties
|
|
@@ -348,7 +389,13 @@ class ConfluenceAPI:
|
|
|
348
389
|
self.properties = properties or ConfluenceConnectionProperties()
|
|
349
390
|
|
|
350
391
|
def __enter__(self) -> "ConfluenceSession":
|
|
392
|
+
"""
|
|
393
|
+
Opens a connection to a Confluence server.
|
|
394
|
+
"""
|
|
395
|
+
|
|
351
396
|
session = requests.Session()
|
|
397
|
+
session.mount("https://", TruststoreAdapter())
|
|
398
|
+
|
|
352
399
|
if self.properties.user_name:
|
|
353
400
|
session.auth = (self.properties.user_name, self.properties.api_key)
|
|
354
401
|
else:
|
|
@@ -372,6 +419,10 @@ class ConfluenceAPI:
|
|
|
372
419
|
exc_val: Optional[BaseException],
|
|
373
420
|
exc_tb: Optional[TracebackType],
|
|
374
421
|
) -> None:
|
|
422
|
+
"""
|
|
423
|
+
Closes an open connection.
|
|
424
|
+
"""
|
|
425
|
+
|
|
375
426
|
if self.session is not None:
|
|
376
427
|
self.session.close()
|
|
377
428
|
self.session = None
|
|
@@ -379,7 +430,7 @@ class ConfluenceAPI:
|
|
|
379
430
|
|
|
380
431
|
class ConfluenceSession:
|
|
381
432
|
"""
|
|
382
|
-
|
|
433
|
+
Represents an active connection to a Confluence server.
|
|
383
434
|
"""
|
|
384
435
|
|
|
385
436
|
session: requests.Session
|
|
@@ -407,7 +458,7 @@ class ConfluenceSession:
|
|
|
407
458
|
|
|
408
459
|
if not domain or not base_path:
|
|
409
460
|
data = self._get(ConfluenceVersion.VERSION_2, "/spaces", ConfluenceResultSet, query={"limit": "1"})
|
|
410
|
-
base_url = data._links.base
|
|
461
|
+
base_url = data._links.base # pyright: ignore[reportPrivateUsage]
|
|
411
462
|
|
|
412
463
|
_, domain, base_path, _, _, _ = urlparse(base_url)
|
|
413
464
|
if not base_path.endswith("/"):
|
|
@@ -418,8 +469,31 @@ class ConfluenceSession:
|
|
|
418
469
|
if not base_path:
|
|
419
470
|
raise ArgumentError("Confluence base path not specified and cannot be inferred")
|
|
420
471
|
self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
|
|
472
|
+
|
|
421
473
|
if not api_url:
|
|
422
|
-
|
|
474
|
+
LOGGER.info("Discovering Confluence REST API URL")
|
|
475
|
+
try:
|
|
476
|
+
# obtain cloud ID to build URL for access with scoped token
|
|
477
|
+
response = self.session.get(f"https://{self.site.domain}/_edge/tenant_info", headers={"Accept": "application/json"}, verify=True)
|
|
478
|
+
if response.text:
|
|
479
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
480
|
+
response.raise_for_status()
|
|
481
|
+
cloud_id = response.json()["cloudId"]
|
|
482
|
+
|
|
483
|
+
# try next-generation REST API URL
|
|
484
|
+
LOGGER.info("Probing scoped Confluence REST API URL")
|
|
485
|
+
self.api_url = f"https://api.atlassian.com/ex/confluence/{cloud_id}/"
|
|
486
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, "/spaces", {"limit": "1"})
|
|
487
|
+
response = self.session.get(url, headers={"Accept": "application/json"}, verify=True)
|
|
488
|
+
if response.text:
|
|
489
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
490
|
+
response.raise_for_status()
|
|
491
|
+
|
|
492
|
+
LOGGER.info("Configured scoped Confluence REST API URL: %s", self.api_url)
|
|
493
|
+
except requests.exceptions.HTTPError:
|
|
494
|
+
# fall back to classic REST API URL
|
|
495
|
+
self.api_url = f"https://{self.site.domain}{self.site.base_path}"
|
|
496
|
+
LOGGER.info("Configured classic Confluence REST API URL: %s", self.api_url)
|
|
423
497
|
|
|
424
498
|
def close(self) -> None:
|
|
425
499
|
self.session.close()
|
|
@@ -454,7 +528,7 @@ class ConfluenceSession:
|
|
|
454
528
|
"Executes an HTTP request via Confluence API."
|
|
455
529
|
|
|
456
530
|
url = self._build_url(version, path, query)
|
|
457
|
-
response = self.session.get(url, headers={"Accept": "application/json"})
|
|
531
|
+
response = self.session.get(url, headers={"Accept": "application/json"}, verify=True)
|
|
458
532
|
if response.text:
|
|
459
533
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
460
534
|
response.raise_for_status()
|
|
@@ -466,7 +540,7 @@ class ConfluenceSession:
|
|
|
466
540
|
items: list[JsonType] = []
|
|
467
541
|
url = self._build_url(ConfluenceVersion.VERSION_2, path, query)
|
|
468
542
|
while True:
|
|
469
|
-
response = self.session.get(url, headers={"Accept": "application/json"})
|
|
543
|
+
response = self.session.get(url, headers={"Accept": "application/json"}, verify=True)
|
|
470
544
|
response.raise_for_status()
|
|
471
545
|
|
|
472
546
|
payload = typing.cast(dict[str, JsonType], response.json())
|
|
@@ -482,42 +556,42 @@ class ConfluenceSession:
|
|
|
482
556
|
|
|
483
557
|
return items
|
|
484
558
|
|
|
485
|
-
def _build_request(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T]) -> tuple[str, dict[str, str], bytes]:
|
|
559
|
+
def _build_request(self, version: ConfluenceVersion, path: str, body: Any, response_type: Optional[type[T]]) -> tuple[str, dict[str, str], bytes]:
|
|
486
560
|
"Generates URL, headers and raw payload for a typed request/response."
|
|
487
561
|
|
|
488
562
|
url = self._build_url(version, path)
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
"Accept": "application/json",
|
|
493
|
-
}
|
|
494
|
-
else:
|
|
495
|
-
headers = {
|
|
496
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
497
|
-
}
|
|
563
|
+
headers = {"Content-Type": "application/json"}
|
|
564
|
+
if response_type is not None:
|
|
565
|
+
headers["Accept"] = "application/json"
|
|
498
566
|
data = json_dump_string(object_to_json(body)).encode("utf-8")
|
|
499
567
|
return url, headers, data
|
|
500
568
|
|
|
501
|
-
|
|
569
|
+
@overload
|
|
570
|
+
def _post(self, version: ConfluenceVersion, path: str, body: Any, response_type: None) -> None: ...
|
|
571
|
+
|
|
572
|
+
@overload
|
|
573
|
+
def _post(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T]) -> T: ...
|
|
574
|
+
|
|
575
|
+
def _post(self, version: ConfluenceVersion, path: str, body: Any, response_type: Optional[type[T]]) -> Optional[T]:
|
|
502
576
|
"Creates a new object via Confluence REST API."
|
|
503
577
|
|
|
504
578
|
url, headers, data = self._build_request(version, path, body, response_type)
|
|
505
|
-
response = self.session.post(
|
|
506
|
-
|
|
507
|
-
data=data,
|
|
508
|
-
headers=headers,
|
|
509
|
-
)
|
|
579
|
+
response = self.session.post(url, data=data, headers=headers, verify=True)
|
|
580
|
+
response.raise_for_status()
|
|
510
581
|
return response_cast(response_type, response)
|
|
511
582
|
|
|
512
|
-
|
|
583
|
+
@overload
|
|
584
|
+
def _put(self, version: ConfluenceVersion, path: str, body: Any, response_type: None) -> None: ...
|
|
585
|
+
|
|
586
|
+
@overload
|
|
587
|
+
def _put(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T]) -> T: ...
|
|
588
|
+
|
|
589
|
+
def _put(self, version: ConfluenceVersion, path: str, body: Any, response_type: Optional[type[T]]) -> Optional[T]:
|
|
513
590
|
"Updates an existing object via Confluence REST API."
|
|
514
591
|
|
|
515
592
|
url, headers, data = self._build_request(version, path, body, response_type)
|
|
516
|
-
response = self.session.put(
|
|
517
|
-
|
|
518
|
-
data=data,
|
|
519
|
-
headers=headers,
|
|
520
|
-
)
|
|
593
|
+
response = self.session.put(url, data=data, headers=headers, verify=True)
|
|
594
|
+
response.raise_for_status()
|
|
521
595
|
return response_cast(response_type, response)
|
|
522
596
|
|
|
523
597
|
def space_id_to_key(self, id: str) -> str:
|
|
@@ -687,6 +761,7 @@ class ConfluenceSession:
|
|
|
687
761
|
"X-Atlassian-Token": "no-check",
|
|
688
762
|
"Accept": "application/json",
|
|
689
763
|
},
|
|
764
|
+
verify=True,
|
|
690
765
|
)
|
|
691
766
|
elif raw_data is not None:
|
|
692
767
|
LOGGER.info("Uploading raw data: %s", attachment_name)
|
|
@@ -714,6 +789,7 @@ class ConfluenceSession:
|
|
|
714
789
|
"X-Atlassian-Token": "no-check",
|
|
715
790
|
"Accept": "application/json",
|
|
716
791
|
},
|
|
792
|
+
verify=True,
|
|
717
793
|
)
|
|
718
794
|
else:
|
|
719
795
|
raise NotImplementedError("parameter match not exhaustive")
|
|
@@ -744,7 +820,7 @@ class ConfluenceSession:
|
|
|
744
820
|
)
|
|
745
821
|
|
|
746
822
|
LOGGER.info("Updating attachment: %s", attachment_id)
|
|
747
|
-
self._put(ConfluenceVersion.VERSION_1, path, request,
|
|
823
|
+
self._put(ConfluenceVersion.VERSION_1, path, request, None)
|
|
748
824
|
|
|
749
825
|
def get_page_properties_by_title(
|
|
750
826
|
self,
|
|
@@ -837,7 +913,7 @@ class ConfluenceSession:
|
|
|
837
913
|
version=ConfluenceContentVersion(number=version, minorEdit=True),
|
|
838
914
|
)
|
|
839
915
|
LOGGER.info("Updating page: %s", page_id)
|
|
840
|
-
self._put(ConfluenceVersion.VERSION_2, path, request,
|
|
916
|
+
self._put(ConfluenceVersion.VERSION_2, path, request, None)
|
|
841
917
|
|
|
842
918
|
def create_page(
|
|
843
919
|
self,
|
|
@@ -872,9 +948,10 @@ class ConfluenceSession:
|
|
|
872
948
|
url,
|
|
873
949
|
data=json_dump_string(object_to_json(request)).encode("utf-8"),
|
|
874
950
|
headers={
|
|
875
|
-
"Content-Type": "application/json
|
|
951
|
+
"Content-Type": "application/json",
|
|
876
952
|
"Accept": "application/json",
|
|
877
953
|
},
|
|
954
|
+
verify=True,
|
|
878
955
|
)
|
|
879
956
|
response.raise_for_status()
|
|
880
957
|
return _json_to_object(ConfluencePage, response.json())
|
|
@@ -892,7 +969,7 @@ class ConfluenceSession:
|
|
|
892
969
|
# move to trash
|
|
893
970
|
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
894
971
|
LOGGER.info("Moving page to trash: %s", page_id)
|
|
895
|
-
response = self.session.delete(url)
|
|
972
|
+
response = self.session.delete(url, verify=True)
|
|
896
973
|
response.raise_for_status()
|
|
897
974
|
|
|
898
975
|
if purge:
|
|
@@ -900,7 +977,7 @@ class ConfluenceSession:
|
|
|
900
977
|
query = {"purge": "true"}
|
|
901
978
|
url = self._build_url(ConfluenceVersion.VERSION_2, path, query)
|
|
902
979
|
LOGGER.info("Permanently deleting page: %s", page_id)
|
|
903
|
-
response = self.session.delete(url)
|
|
980
|
+
response = self.session.delete(url, verify=True)
|
|
904
981
|
response.raise_for_status()
|
|
905
982
|
|
|
906
983
|
def page_exists(
|
|
@@ -932,9 +1009,10 @@ class ConfluenceSession:
|
|
|
932
1009
|
url,
|
|
933
1010
|
params=query,
|
|
934
1011
|
headers={
|
|
935
|
-
"Content-Type": "application/json
|
|
1012
|
+
"Content-Type": "application/json",
|
|
936
1013
|
"Accept": "application/json",
|
|
937
1014
|
},
|
|
1015
|
+
verify=True,
|
|
938
1016
|
)
|
|
939
1017
|
response.raise_for_status()
|
|
940
1018
|
data = typing.cast(dict[str, JsonType], response.json())
|
|
@@ -984,7 +1062,7 @@ class ConfluenceSession:
|
|
|
984
1062
|
"""
|
|
985
1063
|
|
|
986
1064
|
path = f"/content/{page_id}/label"
|
|
987
|
-
self._post(ConfluenceVersion.VERSION_1, path, labels,
|
|
1065
|
+
self._post(ConfluenceVersion.VERSION_1, path, labels, None)
|
|
988
1066
|
|
|
989
1067
|
def remove_labels(self, page_id: str, labels: list[ConfluenceLabel]) -> None:
|
|
990
1068
|
"""
|
|
@@ -999,7 +1077,7 @@ class ConfluenceSession:
|
|
|
999
1077
|
query = {"name": label.name}
|
|
1000
1078
|
|
|
1001
1079
|
url = self._build_url(ConfluenceVersion.VERSION_1, path, query)
|
|
1002
|
-
response = self.session.delete(url)
|
|
1080
|
+
response = self.session.delete(url, verify=True)
|
|
1003
1081
|
if response.text:
|
|
1004
1082
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
1005
1083
|
response.raise_for_status()
|
|
@@ -1058,7 +1136,7 @@ class ConfluenceSession:
|
|
|
1058
1136
|
|
|
1059
1137
|
path = f"/pages/{page_id}/properties/{property_id}"
|
|
1060
1138
|
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
1061
|
-
response = self.session.delete(url)
|
|
1139
|
+
response = self.session.delete(url, verify=True)
|
|
1062
1140
|
response.raise_for_status()
|
|
1063
1141
|
|
|
1064
1142
|
def update_content_property_for_page(
|