markdown-to-confluence 0.5.1__py3-none-any.whl → 0.5.3__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.
Files changed (54) hide show
  1. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/METADATA +160 -11
  2. markdown_to_confluence-0.5.3.dist-info/RECORD +55 -0
  3. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/licenses/LICENSE +1 -1
  4. md2conf/__init__.py +2 -2
  5. md2conf/__main__.py +94 -29
  6. md2conf/api.py +55 -10
  7. md2conf/attachment.py +72 -0
  8. md2conf/coalesce.py +43 -0
  9. md2conf/collection.py +1 -1
  10. md2conf/{extra.py → compatibility.py} +1 -1
  11. md2conf/converter.py +417 -590
  12. md2conf/csf.py +13 -11
  13. md2conf/drawio/__init__.py +0 -0
  14. md2conf/drawio/extension.py +116 -0
  15. md2conf/{drawio.py → drawio/render.py} +1 -1
  16. md2conf/emoticon.py +3 -3
  17. md2conf/environment.py +2 -2
  18. md2conf/extension.py +78 -0
  19. md2conf/external.py +49 -0
  20. md2conf/formatting.py +135 -0
  21. md2conf/frontmatter.py +70 -0
  22. md2conf/image.py +127 -0
  23. md2conf/latex.py +7 -186
  24. md2conf/local.py +8 -8
  25. md2conf/markdown.py +1 -1
  26. md2conf/matcher.py +1 -1
  27. md2conf/mermaid/__init__.py +0 -0
  28. md2conf/mermaid/config.py +20 -0
  29. md2conf/mermaid/extension.py +109 -0
  30. md2conf/{mermaid.py → mermaid/render.py} +10 -38
  31. md2conf/mermaid/scanner.py +55 -0
  32. md2conf/metadata.py +1 -1
  33. md2conf/options.py +116 -0
  34. md2conf/plantuml/__init__.py +0 -0
  35. md2conf/plantuml/config.py +20 -0
  36. md2conf/plantuml/extension.py +158 -0
  37. md2conf/plantuml/render.py +139 -0
  38. md2conf/plantuml/scanner.py +56 -0
  39. md2conf/png.py +202 -0
  40. md2conf/processor.py +32 -11
  41. md2conf/publisher.py +17 -18
  42. md2conf/scanner.py +31 -128
  43. md2conf/serializer.py +2 -2
  44. md2conf/svg.py +341 -0
  45. md2conf/text.py +1 -1
  46. md2conf/toc.py +1 -1
  47. md2conf/uri.py +1 -1
  48. md2conf/xml.py +1 -1
  49. markdown_to_confluence-0.5.1.dist-info/RECORD +0 -35
  50. md2conf/domain.py +0 -52
  51. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/WHEEL +0 -0
  52. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/entry_points.txt +0 -0
  53. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/top_level.txt +0 -0
  54. {markdown_to_confluence-0.5.1.dist-info → markdown_to_confluence-0.5.3.dist-info}/zip-safe +0 -0
md2conf/api.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2025, Levente Hunyadi
4
+ Copyright 2022-2026, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
@@ -11,7 +11,9 @@ import enum
11
11
  import io
12
12
  import logging
13
13
  import mimetypes
14
+ import random
14
15
  import ssl
16
+ import time
15
17
  import typing
16
18
  from dataclasses import dataclass
17
19
  from pathlib import Path
@@ -23,20 +25,23 @@ import requests
23
25
  import truststore
24
26
  from requests.adapters import HTTPAdapter
25
27
 
26
- from .environment import ArgumentError, ConfluenceConnectionProperties, ConfluenceError, PageError
27
- from .extra import override
28
+ from .compatibility import override
29
+ from .environment import ArgumentError, ConfluenceError, ConnectionProperties, PageError
28
30
  from .metadata import ConfluenceSiteMetadata
29
31
  from .serializer import JsonType, json_to_object, object_to_json_payload
30
32
 
31
33
  T = TypeVar("T")
32
34
 
35
+ # spellchecker: disable
33
36
  mimetypes.add_type("application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx", strict=True)
34
37
  mimetypes.add_type("text/vnd.mermaid", ".mmd", strict=True)
38
+ mimetypes.add_type("text/vnd.plantuml", ".puml", strict=True)
35
39
  mimetypes.add_type("application/vnd.oasis.opendocument.presentation", ".odp", strict=True)
36
40
  mimetypes.add_type("application/vnd.oasis.opendocument.spreadsheet", ".ods", strict=True)
37
41
  mimetypes.add_type("application/vnd.oasis.opendocument.text", ".odt", strict=True)
38
42
  mimetypes.add_type("application/vnd.openxmlformats-officedocument.presentationml.presentation", ".pptx", strict=True)
39
43
  mimetypes.add_type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx", strict=True)
44
+ # spellchecker: enable
40
45
 
41
46
 
42
47
  def build_url(base_url: str, query: dict[str, str] | None = None) -> str:
@@ -366,11 +371,11 @@ class ConfluenceAPI:
366
371
  Encapsulates operations that can be invoked via the [Confluence REST API](https://developer.atlassian.com/cloud/confluence/rest/v2/).
367
372
  """
368
373
 
369
- properties: ConfluenceConnectionProperties
374
+ properties: ConnectionProperties
370
375
  session: "ConfluenceSession | None" = None
371
376
 
372
- def __init__(self, properties: ConfluenceConnectionProperties | None = None) -> None:
373
- self.properties = properties or ConfluenceConnectionProperties()
377
+ def __init__(self, properties: ConnectionProperties | None = None) -> None:
378
+ self.properties = properties or ConnectionProperties()
374
379
 
375
380
  def __enter__(self) -> "ConfluenceSession":
376
381
  """
@@ -622,7 +627,16 @@ class ConfluenceSession:
622
627
 
623
628
  return id
624
629
 
630
+ @overload
631
+ def get_space_id(self, *, space_id: str | None = None) -> str | None: ...
632
+
633
+ @overload
634
+ def get_space_id(self, *, space_key: str | None = None) -> str | None: ...
635
+
625
636
  def get_space_id(self, *, space_id: str | None = None, space_key: str | None = None) -> str | None:
637
+ return self._get_space_id(space_id=space_id, space_key=space_key)
638
+
639
+ def _get_space_id(self, *, space_id: str | None = None, space_key: str | None = None) -> str | None:
626
640
  """
627
641
  Coalesces a space ID or space key into a space ID, accounting for site default.
628
642
 
@@ -643,6 +657,15 @@ class ConfluenceSession:
643
657
  # space ID and key are unset, and no default space is configured
644
658
  return None
645
659
 
660
+ def get_homepage_id(self, space_id: str) -> str:
661
+ """
662
+ Returns the page ID corresponding to the space home page.
663
+ """
664
+
665
+ path = f"/spaces/{space_id}"
666
+ data = self._get(ConfluenceVersion.VERSION_2, path, dict[str, JsonType])
667
+ return typing.cast(str, data["homepageId"])
668
+
646
669
  def get_attachment_by_name(self, page_id: str, filename: str) -> ConfluenceAttachment:
647
670
  """
648
671
  Retrieves a Confluence page attachment by an unprefixed file name.
@@ -827,7 +850,7 @@ class ConfluenceSession:
827
850
  query = {
828
851
  "title": title,
829
852
  }
830
- space_id = self.get_space_id(space_id=space_id, space_key=space_key)
853
+ space_id = self._get_space_id(space_id=space_id, space_key=space_key)
831
854
  if space_id is not None:
832
855
  query["space-id"] = space_id
833
856
 
@@ -839,16 +862,38 @@ class ConfluenceSession:
839
862
  page = json_to_object(ConfluencePageProperties, results[0])
840
863
  return page
841
864
 
842
- def get_page(self, page_id: str) -> ConfluencePage:
865
+ def get_page(self, page_id: str, *, retries: int = 3, retry_delay: float = 1.0) -> ConfluencePage:
843
866
  """
844
867
  Retrieves Confluence wiki page details and content.
845
868
 
869
+ Includes retry logic to handle eventual consistency issues when fetching
870
+ a newly created page that may not be immediately available via the API.
871
+
846
872
  :param page_id: The Confluence page ID.
873
+ :param retries: Number of retry attempts for 404 errors (default: 3).
874
+ :param retry_delay: Initial delay in seconds between retries, doubles each attempt (default: 1.0).
847
875
  :returns: Confluence page info and content.
848
876
  """
849
877
 
850
878
  path = f"/pages/{page_id}"
851
- return self._get(ConfluenceVersion.VERSION_2, path, ConfluencePage, query={"body-format": "storage"})
879
+ last_error: requests.HTTPError | None = None
880
+
881
+ for attempt in range(retries + 1):
882
+ try:
883
+ return self._get(ConfluenceVersion.VERSION_2, path, ConfluencePage, query={"body-format": "storage"})
884
+ except requests.HTTPError as e:
885
+ if e.response is not None and e.response.status_code == 404 and attempt < retries:
886
+ delay = retry_delay * (2**attempt) + random.uniform(0, 1)
887
+ LOGGER.debug("Page %s not found, retrying in %.1f seconds (attempt %d/%d)", page_id, delay, attempt + 1, retries)
888
+ time.sleep(delay)
889
+ last_error = e
890
+ else:
891
+ raise
892
+
893
+ # This should not be reached, but satisfies type checker
894
+ if last_error is not None:
895
+ raise last_error
896
+ raise ConfluenceError(f"Failed to get page {page_id}")
852
897
 
853
898
  def get_page_properties(self, page_id: str) -> ConfluencePageProperties:
854
899
  """
@@ -980,7 +1025,7 @@ class ConfluenceSession:
980
1025
  :returns: Confluence page ID of a matching page (if found), or `None`.
981
1026
  """
982
1027
 
983
- space_id = self.get_space_id(space_id=space_id, space_key=space_key)
1028
+ space_id = self._get_space_id(space_id=space_id, space_key=space_key)
984
1029
  path = "/pages"
985
1030
  query = {"title": title}
986
1031
  if space_id is not None:
md2conf/attachment.py ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+
14
+ @dataclass
15
+ class ImageData:
16
+ path: Path
17
+ description: str | None = None
18
+
19
+
20
+ @dataclass
21
+ class EmbeddedFileData:
22
+ data: bytes
23
+ description: str | None = None
24
+
25
+
26
+ class AttachmentCatalog:
27
+ "Maintains a list of files and binary data to be uploaded to Confluence as attachments."
28
+
29
+ images: list[ImageData]
30
+ embedded_files: dict[str, EmbeddedFileData]
31
+
32
+ def __init__(self) -> None:
33
+ self.images = []
34
+ self.embedded_files = {}
35
+
36
+ def add_image(self, data: ImageData) -> None:
37
+ self.images.append(data)
38
+
39
+ def add_embed(self, filename: str, data: EmbeddedFileData) -> None:
40
+ self.embedded_files[filename] = data
41
+
42
+
43
+ def attachment_name(ref: Path | str) -> str:
44
+ """
45
+ Safe name for use with attachment uploads.
46
+
47
+ Mutates a relative path such that it meets Confluence's attachment naming requirements.
48
+
49
+ Allowed characters:
50
+
51
+ * Alphanumeric characters: 0-9, a-z, A-Z
52
+ * Special characters: hyphen (-), underscore (_), period (.)
53
+ """
54
+
55
+ if isinstance(ref, Path):
56
+ path = ref
57
+ else:
58
+ path = Path(ref)
59
+
60
+ if path.drive or path.root:
61
+ raise ValueError(f"required: relative path; got: {ref}")
62
+
63
+ regexp = re.compile(r"[^\-0-9A-Za-z_.]", re.UNICODE)
64
+
65
+ def replace_part(part: str) -> str:
66
+ if part == "..":
67
+ return "PAR"
68
+ else:
69
+ return regexp.sub("_", part)
70
+
71
+ parts = [replace_part(p) for p in path.parts]
72
+ return Path(*parts).as_posix().replace("/", "_")
md2conf/coalesce.py ADDED
@@ -0,0 +1,43 @@
1
+ """
2
+ Publish Markdown files to Confluence wiki.
3
+
4
+ Copyright 2022-2026, Levente Hunyadi
5
+
6
+ :see: https://github.com/hunyadi/md2conf
7
+ """
8
+
9
+ import copy
10
+ import dataclasses
11
+ from typing import Any, ClassVar, Protocol, TypeVar
12
+
13
+
14
+ class DataclassInstance(Protocol):
15
+ __dataclass_fields__: ClassVar[dict[str, dataclasses.Field[Any]]]
16
+
17
+
18
+ D = TypeVar("D", bound=DataclassInstance)
19
+
20
+
21
+ def coalesce(target: D, source: D) -> D:
22
+ """
23
+ Implements nullish coalescing assignment on each field of a data-class.
24
+
25
+ Iterates over each field of the data-class, and evaluates the right operand and assigns it to the left only if
26
+ the left operand is `None`. Applies recursively when the field is a data-class.
27
+
28
+ :returns: A newly created data-class instance.
29
+ """
30
+
31
+ updates: dict[str, Any] = {}
32
+ for field in dataclasses.fields(target):
33
+ target_field = getattr(target, field.name, None)
34
+ source_field = getattr(source, field.name, None)
35
+
36
+ if target_field is None:
37
+ if source_field is not None:
38
+ updates[field.name] = copy.deepcopy(source_field)
39
+ elif dataclasses.is_dataclass(field.type):
40
+ if source_field is not None:
41
+ updates[field.name] = coalesce(target_field, source_field)
42
+
43
+ return dataclasses.replace(target, **updates)
md2conf/collection.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2025, Levente Hunyadi
4
+ Copyright 2022-2026, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Publish Markdown files to Confluence wiki.
3
3
 
4
- Copyright 2022-2025, Levente Hunyadi
4
+ Copyright 2022-2026, Levente Hunyadi
5
5
 
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """