markdown-to-confluence 0.4.4__py3-none-any.whl → 0.4.6__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.4.dist-info → markdown_to_confluence-0.4.6.dist-info}/METADATA +83 -33
- markdown_to_confluence-0.4.6.dist-info/RECORD +34 -0
- {markdown_to_confluence-0.4.4.dist-info → markdown_to_confluence-0.4.6.dist-info}/licenses/LICENSE +1 -1
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +35 -39
- md2conf/api.py +90 -20
- md2conf/converter.py +585 -300
- md2conf/csf.py +66 -0
- md2conf/domain.py +2 -0
- md2conf/drawio.py +18 -14
- md2conf/emoticon.py +22 -0
- md2conf/latex.py +245 -0
- md2conf/local.py +2 -2
- md2conf/markdown.py +3 -1
- md2conf/mermaid.py +38 -29
- md2conf/processor.py +1 -1
- md2conf/{application.py → publisher.py} +28 -19
- md2conf/scanner.py +46 -0
- md2conf/text.py +54 -0
- md2conf/xml.py +37 -0
- markdown_to_confluence-0.4.4.dist-info/RECORD +0 -31
- {markdown_to_confluence-0.4.4.dist-info → markdown_to_confluence-0.4.6.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.4.dist-info → markdown_to_confluence-0.4.6.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.4.dist-info → markdown_to_confluence-0.4.6.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.4.dist-info → markdown_to_confluence-0.4.6.dist-info}/zip-safe +0 -0
- /md2conf/{properties.py → environment.py} +0 -0
md2conf/api.py
CHANGED
|
@@ -11,6 +11,8 @@ 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
|
|
@@ -19,15 +21,28 @@ from typing import Any, Optional, TypeVar
|
|
|
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
|
|
|
39
|
+
mimetypes.add_type("application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx", strict=True)
|
|
30
40
|
mimetypes.add_type("text/vnd.mermaid", ".mmd", strict=True)
|
|
41
|
+
mimetypes.add_type("application/vnd.oasis.opendocument.presentation", ".odp", strict=True)
|
|
42
|
+
mimetypes.add_type("application/vnd.oasis.opendocument.spreadsheet", ".ods", strict=True)
|
|
43
|
+
mimetypes.add_type("application/vnd.oasis.opendocument.text", ".odt", strict=True)
|
|
44
|
+
mimetypes.add_type("application/vnd.openxmlformats-officedocument.presentationml.presentation", ".pptx", strict=True)
|
|
45
|
+
mimetypes.add_type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx", strict=True)
|
|
31
46
|
|
|
32
47
|
|
|
33
48
|
def _json_to_object(
|
|
@@ -330,9 +345,33 @@ class ConfluenceUpdateAttachmentRequest:
|
|
|
330
345
|
version: ConfluenceContentVersion
|
|
331
346
|
|
|
332
347
|
|
|
348
|
+
class TruststoreAdapter(HTTPAdapter):
|
|
349
|
+
"""
|
|
350
|
+
Provides a general-case interface for HTTPS sessions to connect to HTTPS URLs.
|
|
351
|
+
|
|
352
|
+
This class implements the Transport Adapter interface in the Python library `requests`.
|
|
353
|
+
|
|
354
|
+
This class will usually be created by the :class:`requests.Session` class under the covers.
|
|
355
|
+
"""
|
|
356
|
+
|
|
357
|
+
@override
|
|
358
|
+
def init_poolmanager(self, connections: int, maxsize: int, block: bool = False, **pool_kwargs: Any) -> None:
|
|
359
|
+
"""
|
|
360
|
+
Adapts the pool manager to use the provided SSL context instead of the default.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
if sys.version_info >= (3, 10):
|
|
364
|
+
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
365
|
+
else:
|
|
366
|
+
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
|
|
367
|
+
ctx.check_hostname = True
|
|
368
|
+
ctx.verify_mode = ssl.CERT_REQUIRED
|
|
369
|
+
super().init_poolmanager(connections, maxsize, block, ssl_context=ctx, **pool_kwargs) # type: ignore[no-untyped-call]
|
|
370
|
+
|
|
371
|
+
|
|
333
372
|
class ConfluenceAPI:
|
|
334
373
|
"""
|
|
335
|
-
|
|
374
|
+
Encapsulates operations that can be invoked via the [Confluence REST API](https://developer.atlassian.com/cloud/confluence/rest/v2/).
|
|
336
375
|
"""
|
|
337
376
|
|
|
338
377
|
properties: ConfluenceConnectionProperties
|
|
@@ -342,7 +381,13 @@ class ConfluenceAPI:
|
|
|
342
381
|
self.properties = properties or ConfluenceConnectionProperties()
|
|
343
382
|
|
|
344
383
|
def __enter__(self) -> "ConfluenceSession":
|
|
384
|
+
"""
|
|
385
|
+
Opens a connection to a Confluence server.
|
|
386
|
+
"""
|
|
387
|
+
|
|
345
388
|
session = requests.Session()
|
|
389
|
+
session.mount("https://", TruststoreAdapter())
|
|
390
|
+
|
|
346
391
|
if self.properties.user_name:
|
|
347
392
|
session.auth = (self.properties.user_name, self.properties.api_key)
|
|
348
393
|
else:
|
|
@@ -366,6 +411,10 @@ class ConfluenceAPI:
|
|
|
366
411
|
exc_val: Optional[BaseException],
|
|
367
412
|
exc_tb: Optional[TracebackType],
|
|
368
413
|
) -> None:
|
|
414
|
+
"""
|
|
415
|
+
Closes an open connection.
|
|
416
|
+
"""
|
|
417
|
+
|
|
369
418
|
if self.session is not None:
|
|
370
419
|
self.session.close()
|
|
371
420
|
self.session = None
|
|
@@ -373,7 +422,7 @@ class ConfluenceAPI:
|
|
|
373
422
|
|
|
374
423
|
class ConfluenceSession:
|
|
375
424
|
"""
|
|
376
|
-
|
|
425
|
+
Represents an active connection to a Confluence server.
|
|
377
426
|
"""
|
|
378
427
|
|
|
379
428
|
session: requests.Session
|
|
@@ -412,8 +461,31 @@ class ConfluenceSession:
|
|
|
412
461
|
if not base_path:
|
|
413
462
|
raise ArgumentError("Confluence base path not specified and cannot be inferred")
|
|
414
463
|
self.site = ConfluenceSiteMetadata(domain, base_path, space_key)
|
|
464
|
+
|
|
415
465
|
if not api_url:
|
|
416
|
-
|
|
466
|
+
LOGGER.info("Discovering Confluence REST API URL")
|
|
467
|
+
try:
|
|
468
|
+
# obtain cloud ID to build URL for access with scoped token
|
|
469
|
+
response = self.session.get(f"https://{self.site.domain}/_edge/tenant_info", headers={"Accept": "application/json"}, verify=True)
|
|
470
|
+
if response.text:
|
|
471
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
472
|
+
response.raise_for_status()
|
|
473
|
+
cloud_id = response.json()["cloudId"]
|
|
474
|
+
|
|
475
|
+
# try next-generation REST API URL
|
|
476
|
+
LOGGER.info("Probing scoped Confluence REST API URL")
|
|
477
|
+
self.api_url = f"https://api.atlassian.com/ex/confluence/{cloud_id}/"
|
|
478
|
+
url = self._build_url(ConfluenceVersion.VERSION_2, "/spaces", {"limit": "1"})
|
|
479
|
+
response = self.session.get(url, headers={"Accept": "application/json"}, verify=True)
|
|
480
|
+
if response.text:
|
|
481
|
+
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
482
|
+
response.raise_for_status()
|
|
483
|
+
|
|
484
|
+
LOGGER.info("Configured scoped Confluence REST API URL: %s", self.api_url)
|
|
485
|
+
except requests.exceptions.HTTPError:
|
|
486
|
+
# fall back to classic REST API URL
|
|
487
|
+
self.api_url = f"https://{self.site.domain}{self.site.base_path}"
|
|
488
|
+
LOGGER.info("Configured classic Confluence REST API URL: %s", self.api_url)
|
|
417
489
|
|
|
418
490
|
def close(self) -> None:
|
|
419
491
|
self.session.close()
|
|
@@ -448,7 +520,7 @@ class ConfluenceSession:
|
|
|
448
520
|
"Executes an HTTP request via Confluence API."
|
|
449
521
|
|
|
450
522
|
url = self._build_url(version, path, query)
|
|
451
|
-
response = self.session.get(url, headers={"Accept": "application/json"})
|
|
523
|
+
response = self.session.get(url, headers={"Accept": "application/json"}, verify=True)
|
|
452
524
|
if response.text:
|
|
453
525
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
454
526
|
response.raise_for_status()
|
|
@@ -460,7 +532,7 @@ class ConfluenceSession:
|
|
|
460
532
|
items: list[JsonType] = []
|
|
461
533
|
url = self._build_url(ConfluenceVersion.VERSION_2, path, query)
|
|
462
534
|
while True:
|
|
463
|
-
response = self.session.get(url, headers={"Accept": "application/json"})
|
|
535
|
+
response = self.session.get(url, headers={"Accept": "application/json"}, verify=True)
|
|
464
536
|
response.raise_for_status()
|
|
465
537
|
|
|
466
538
|
payload = typing.cast(dict[str, JsonType], response.json())
|
|
@@ -496,22 +568,16 @@ class ConfluenceSession:
|
|
|
496
568
|
"Creates a new object via Confluence REST API."
|
|
497
569
|
|
|
498
570
|
url, headers, data = self._build_request(version, path, body, response_type)
|
|
499
|
-
response = self.session.post(
|
|
500
|
-
|
|
501
|
-
data=data,
|
|
502
|
-
headers=headers,
|
|
503
|
-
)
|
|
571
|
+
response = self.session.post(url, data=data, headers=headers, verify=True)
|
|
572
|
+
response.raise_for_status()
|
|
504
573
|
return response_cast(response_type, response)
|
|
505
574
|
|
|
506
575
|
def _put(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T]) -> T:
|
|
507
576
|
"Updates an existing object via Confluence REST API."
|
|
508
577
|
|
|
509
578
|
url, headers, data = self._build_request(version, path, body, response_type)
|
|
510
|
-
response = self.session.put(
|
|
511
|
-
|
|
512
|
-
data=data,
|
|
513
|
-
headers=headers,
|
|
514
|
-
)
|
|
579
|
+
response = self.session.put(url, data=data, headers=headers, verify=True)
|
|
580
|
+
response.raise_for_status()
|
|
515
581
|
return response_cast(response_type, response)
|
|
516
582
|
|
|
517
583
|
def space_id_to_key(self, id: str) -> str:
|
|
@@ -681,6 +747,7 @@ class ConfluenceSession:
|
|
|
681
747
|
"X-Atlassian-Token": "no-check",
|
|
682
748
|
"Accept": "application/json",
|
|
683
749
|
},
|
|
750
|
+
verify=True,
|
|
684
751
|
)
|
|
685
752
|
elif raw_data is not None:
|
|
686
753
|
LOGGER.info("Uploading raw data: %s", attachment_name)
|
|
@@ -708,6 +775,7 @@ class ConfluenceSession:
|
|
|
708
775
|
"X-Atlassian-Token": "no-check",
|
|
709
776
|
"Accept": "application/json",
|
|
710
777
|
},
|
|
778
|
+
verify=True,
|
|
711
779
|
)
|
|
712
780
|
else:
|
|
713
781
|
raise NotImplementedError("parameter match not exhaustive")
|
|
@@ -869,6 +937,7 @@ class ConfluenceSession:
|
|
|
869
937
|
"Content-Type": "application/json; charset=utf-8",
|
|
870
938
|
"Accept": "application/json",
|
|
871
939
|
},
|
|
940
|
+
verify=True,
|
|
872
941
|
)
|
|
873
942
|
response.raise_for_status()
|
|
874
943
|
return _json_to_object(ConfluencePage, response.json())
|
|
@@ -886,7 +955,7 @@ class ConfluenceSession:
|
|
|
886
955
|
# move to trash
|
|
887
956
|
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
888
957
|
LOGGER.info("Moving page to trash: %s", page_id)
|
|
889
|
-
response = self.session.delete(url)
|
|
958
|
+
response = self.session.delete(url, verify=True)
|
|
890
959
|
response.raise_for_status()
|
|
891
960
|
|
|
892
961
|
if purge:
|
|
@@ -894,7 +963,7 @@ class ConfluenceSession:
|
|
|
894
963
|
query = {"purge": "true"}
|
|
895
964
|
url = self._build_url(ConfluenceVersion.VERSION_2, path, query)
|
|
896
965
|
LOGGER.info("Permanently deleting page: %s", page_id)
|
|
897
|
-
response = self.session.delete(url)
|
|
966
|
+
response = self.session.delete(url, verify=True)
|
|
898
967
|
response.raise_for_status()
|
|
899
968
|
|
|
900
969
|
def page_exists(
|
|
@@ -929,6 +998,7 @@ class ConfluenceSession:
|
|
|
929
998
|
"Content-Type": "application/json; charset=utf-8",
|
|
930
999
|
"Accept": "application/json",
|
|
931
1000
|
},
|
|
1001
|
+
verify=True,
|
|
932
1002
|
)
|
|
933
1003
|
response.raise_for_status()
|
|
934
1004
|
data = typing.cast(dict[str, JsonType], response.json())
|
|
@@ -993,7 +1063,7 @@ class ConfluenceSession:
|
|
|
993
1063
|
query = {"name": label.name}
|
|
994
1064
|
|
|
995
1065
|
url = self._build_url(ConfluenceVersion.VERSION_1, path, query)
|
|
996
|
-
response = self.session.delete(url)
|
|
1066
|
+
response = self.session.delete(url, verify=True)
|
|
997
1067
|
if response.text:
|
|
998
1068
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
999
1069
|
response.raise_for_status()
|
|
@@ -1052,7 +1122,7 @@ class ConfluenceSession:
|
|
|
1052
1122
|
|
|
1053
1123
|
path = f"/pages/{page_id}/properties/{property_id}"
|
|
1054
1124
|
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
1055
|
-
response = self.session.delete(url)
|
|
1125
|
+
response = self.session.delete(url, verify=True)
|
|
1056
1126
|
response.raise_for_status()
|
|
1057
1127
|
|
|
1058
1128
|
def update_content_property_for_page(
|