markdown-to-confluence 0.3.0__tar.gz → 0.3.1__tar.gz

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 (31) hide show
  1. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/PKG-INFO +1 -1
  2. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/markdown_to_confluence.egg-info/PKG-INFO +1 -1
  3. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/markdown_to_confluence.egg-info/SOURCES.txt +0 -1
  4. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/__init__.py +1 -1
  5. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/__main__.py +8 -0
  6. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/api.py +29 -25
  7. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/application.py +9 -0
  8. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/converter.py +10 -17
  9. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/mermaid.py +5 -1
  10. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/properties.py +1 -3
  11. markdown_to_confluence-0.3.0/md2conf/util.py +0 -27
  12. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/LICENSE +0 -0
  13. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/README.md +0 -0
  14. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/markdown_to_confluence.egg-info/dependency_links.txt +0 -0
  15. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/markdown_to_confluence.egg-info/entry_points.txt +0 -0
  16. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/markdown_to_confluence.egg-info/requires.txt +0 -0
  17. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/markdown_to_confluence.egg-info/top_level.txt +0 -0
  18. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/markdown_to_confluence.egg-info/zip-safe +0 -0
  19. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/emoji.py +0 -0
  20. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/entities.dtd +0 -0
  21. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/matcher.py +0 -0
  22. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/processor.py +0 -0
  23. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/puppeteer-config.json +0 -0
  24. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/md2conf/py.typed +0 -0
  25. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/pyproject.toml +0 -0
  26. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/setup.cfg +0 -0
  27. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/setup.py +0 -0
  28. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/tests/test_conversion.py +0 -0
  29. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/tests/test_matcher.py +0 -0
  30. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/tests/test_mermaid.py +0 -0
  31. {markdown_to_confluence-0.3.0 → markdown_to_confluence-0.3.1}/tests/test_processor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: markdown-to-confluence
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Publish Markdown files to Confluence wiki
5
5
  Home-page: https://github.com/hunyadi/md2conf
6
6
  Author: Levente Hunyadi
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: markdown-to-confluence
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Publish Markdown files to Confluence wiki
5
5
  Home-page: https://github.com/hunyadi/md2conf
6
6
  Author: Levente Hunyadi
@@ -23,7 +23,6 @@ md2conf/processor.py
23
23
  md2conf/properties.py
24
24
  md2conf/puppeteer-config.json
25
25
  md2conf/py.typed
26
- md2conf/util.py
27
26
  tests/test_conversion.py
28
27
  tests/test_matcher.py
29
28
  tests/test_mermaid.py
@@ -5,7 +5,7 @@ Parses Markdown files, converts Markdown content into the Confluence Storage For
5
5
  Confluence API endpoints to upload images and content.
6
6
  """
7
7
 
8
- __version__ = "0.3.0"
8
+ __version__ = "0.3.1"
9
9
  __author__ = "Levente Hunyadi"
10
10
  __copyright__ = "Copyright 2022-2025, Levente Hunyadi"
11
11
  __license__ = "MIT"
@@ -38,6 +38,7 @@ class Arguments(argparse.Namespace):
38
38
  ignore_invalid_url: bool
39
39
  heading_anchors: bool
40
40
  root_page: Optional[str]
41
+ keep_hierarchy: bool
41
42
  generated_by: Optional[str]
42
43
  render_mermaid: bool
43
44
  diagram_output_format: Literal["png", "svg"]
@@ -109,6 +110,12 @@ def main() -> None:
109
110
  dest="root_page",
110
111
  help="Root Confluence page to create new pages. If omitted, will raise exception when creating new pages.",
111
112
  )
113
+ parser.add_argument(
114
+ "--keep-hierarchy",
115
+ action="store_true",
116
+ default=False,
117
+ help="Maintain source directory structure when exporting to Confluence.",
118
+ )
112
119
  parser.add_argument(
113
120
  "--generated-by",
114
121
  default="This page has been generated with a tool.",
@@ -189,6 +196,7 @@ def main() -> None:
189
196
  ignore_invalid_url=args.ignore_invalid_url,
190
197
  generated_by=args.generated_by,
191
198
  root_page_id=args.root_page,
199
+ keep_hierarchy=args.keep_hierarchy,
192
200
  render_mermaid=args.render_mermaid,
193
201
  diagram_output_format=args.diagram_output_format,
194
202
  webui_links=args.webui_links,
@@ -22,7 +22,6 @@ import requests
22
22
 
23
23
  from .converter import ParseError, sanitize_confluence
24
24
  from .properties import ConfluenceError, ConfluenceProperties
25
- from .util import removeprefix
26
25
 
27
26
  # a JSON type with possible `null` values
28
27
  JsonType = Union[
@@ -119,13 +118,17 @@ class ConfluenceSession:
119
118
  session: requests.Session
120
119
  domain: str
121
120
  base_path: str
122
- space_key: str
121
+ space_key: Optional[str]
123
122
 
124
123
  _space_id_to_key: dict[str, str]
125
124
  _space_key_to_id: dict[str, str]
126
125
 
127
126
  def __init__(
128
- self, session: requests.Session, domain: str, base_path: str, space_key: str
127
+ self,
128
+ session: requests.Session,
129
+ domain: str,
130
+ base_path: str,
131
+ space_key: Optional[str],
129
132
  ) -> None:
130
133
  self.session = session
131
134
  self.domain = domain
@@ -168,6 +171,10 @@ class ConfluenceSession:
168
171
  url = self._build_url(version, path, query)
169
172
  response = self.session.get(url)
170
173
  response.raise_for_status()
174
+ if len(response.text) > 240:
175
+ LOGGER.debug("Received HTTP payload (truncated):\n%.240s...", response.text)
176
+ else:
177
+ LOGGER.debug("Received HTTP payload:\n%s", response.text)
171
178
  return response.json()
172
179
 
173
180
  def _save(self, version: ConfluenceVersion, path: str, data: dict) -> None:
@@ -252,7 +259,6 @@ class ConfluenceSession:
252
259
  raw_data: Optional[bytes] = None,
253
260
  content_type: Optional[str] = None,
254
261
  comment: Optional[str] = None,
255
- space_key: Optional[str] = None,
256
262
  force: bool = False,
257
263
  ) -> None:
258
264
 
@@ -286,7 +292,7 @@ class ConfluenceSession:
286
292
  else:
287
293
  raise NotImplementedError("never occurs")
288
294
 
289
- id = removeprefix(attachment.id, "att")
295
+ id = attachment.id.removeprefix("att")
290
296
  path = f"/content/{page_id}/child/attachment/{id}/data"
291
297
 
292
298
  except ConfluenceError:
@@ -345,27 +351,18 @@ class ConfluenceSession:
345
351
  version = result["version"]["number"] + 1
346
352
 
347
353
  # ensure path component is retained in attachment name
348
- self._update_attachment(
349
- page_id, attachment_id, version, attachment_name, space_key=space_key
350
- )
354
+ self._update_attachment(page_id, attachment_id, version, attachment_name)
351
355
 
352
356
  def _update_attachment(
353
- self,
354
- page_id: str,
355
- attachment_id: str,
356
- version: int,
357
- attachment_title: str,
358
- *,
359
- space_key: Optional[str] = None,
357
+ self, page_id: str, attachment_id: str, version: int, attachment_title: str
360
358
  ) -> None:
361
- id = removeprefix(attachment_id, "att")
359
+ id = attachment_id.removeprefix("att")
362
360
  path = f"/content/{page_id}/child/attachment/{id}"
363
361
  data = {
364
362
  "id": attachment_id,
365
363
  "type": "attachment",
366
364
  "status": "current",
367
365
  "title": attachment_title,
368
- "space": {"key": space_key or self.space_key},
369
366
  "version": {"minorEdit": True, "number": version},
370
367
  }
371
368
 
@@ -389,9 +386,12 @@ class ConfluenceSession:
389
386
  LOGGER.info("Looking up page with title: %s", title)
390
387
  path = "/pages"
391
388
  query = {
392
- "space-id": self.space_key_to_id(space_key or self.space_key),
393
389
  "title": title,
394
390
  }
391
+ coalesced_space_key = space_key or self.space_key
392
+ if coalesced_space_key is not None:
393
+ query["space-id"] = self.space_key_to_id(coalesced_space_key)
394
+
395
395
  payload = self._invoke(ConfluenceVersion.VERSION_2, path, query)
396
396
  payload = typing.cast(dict[str, JsonType], payload)
397
397
 
@@ -432,7 +432,6 @@ class ConfluenceSession:
432
432
  Retrieve a Confluence wiki page version.
433
433
 
434
434
  :param page_id: The Confluence page ID.
435
- :param space_key: The Confluence space key (unless the default space is to be used).
436
435
  :returns: Confluence page version.
437
436
  """
438
437
 
@@ -454,7 +453,6 @@ class ConfluenceSession:
454
453
 
455
454
  :param page_id: The Confluence page ID.
456
455
  :param new_content: Confluence Storage Format XHTML.
457
- :param space_key: The Confluence space key (unless the default space is to be used).
458
456
  :param title: New title to assign to the page. Needs to be unique within a space.
459
457
  """
460
458
 
@@ -493,9 +491,15 @@ class ConfluenceSession:
493
491
  Create a new page via Confluence API.
494
492
  """
495
493
 
494
+ coalesced_space_key = space_key or self.space_key
495
+ if coalesced_space_key is None:
496
+ raise ConfluenceError(
497
+ "Confluence space key required for creating a new page"
498
+ )
499
+
496
500
  path = "/pages/"
497
501
  query = {
498
- "spaceId": self.space_key_to_id(space_key or self.space_key),
502
+ "spaceId": self.space_key_to_id(coalesced_space_key),
499
503
  "status": "current",
500
504
  "title": title,
501
505
  "parentId": parent_page_id,
@@ -553,10 +557,10 @@ class ConfluenceSession:
553
557
  self, title: str, *, space_key: Optional[str] = None
554
558
  ) -> Optional[str]:
555
559
  path = "/pages"
556
- query = {
557
- "title": title,
558
- "space-id": self.space_key_to_id(space_key or self.space_key),
559
- }
560
+ coalesced_space_key = space_key or self.space_key
561
+ query = {"title": title}
562
+ if coalesced_space_key is not None:
563
+ query["space-id"] = self.space_key_to_id(coalesced_space_key)
560
564
 
561
565
  LOGGER.info("Checking if page exists with title: %s", title)
562
566
 
@@ -131,6 +131,15 @@ class Application:
131
131
  parent_doc = Path(local_dir) / "index.md"
132
132
  elif (Path(local_dir) / "README.md") in files:
133
133
  parent_doc = Path(local_dir) / "README.md"
134
+ elif (Path(local_dir) / f"{local_dir.name}.md") in files:
135
+ parent_doc = Path(local_dir) / f"{local_dir.name}.md"
136
+
137
+ if parent_doc is None and self.options.keep_hierarchy:
138
+ parent_doc = Path(local_dir) / "index.md"
139
+
140
+ # create a blank page in Confluence for the directory entry
141
+ with open(parent_doc, "w"):
142
+ pass
134
143
 
135
144
  if parent_doc is not None:
136
145
  files.remove(parent_doc)
@@ -13,7 +13,6 @@ import importlib.resources as resources
13
13
  import logging
14
14
  import os.path
15
15
  import re
16
- import sys
17
16
  import uuid
18
17
  import xml.etree.ElementTree
19
18
  from dataclasses import dataclass
@@ -129,7 +128,7 @@ def _elements_from_strings(dtd_path: Path, items: list[str]) -> ET._Element:
129
128
 
130
129
  data = [
131
130
  '<?xml version="1.0"?>',
132
- f'<!DOCTYPE ac:confluence PUBLIC "-//Atlassian//Confluence 4 Page//EN" "{dtd_path}">'
131
+ f'<!DOCTYPE ac:confluence PUBLIC "-//Atlassian//Confluence 4 Page//EN" "{dtd_path.as_posix()}">'
133
132
  f"<root{ns_attr_list}>",
134
133
  ]
135
134
  data.extend(items)
@@ -144,13 +143,9 @@ def _elements_from_strings(dtd_path: Path, items: list[str]) -> ET._Element:
144
143
  def elements_from_strings(items: list[str]) -> ET._Element:
145
144
  "Creates a fragment of several XML nodes from their string representation wrapped in a root element."
146
145
 
147
- if sys.version_info >= (3, 9):
148
- resource_path = resources.files(__package__).joinpath("entities.dtd")
149
- with resources.as_file(resource_path) as dtd_path:
150
- return _elements_from_strings(dtd_path, items)
151
- else:
152
- with resources.path(__package__, "entities.dtd") as dtd_path:
153
- return _elements_from_strings(dtd_path, items)
146
+ resource_path = resources.files(__package__).joinpath("entities.dtd")
147
+ with resources.as_file(resource_path) as dtd_path:
148
+ return _elements_from_strings(dtd_path, items)
154
149
 
155
150
 
156
151
  def elements_from_string(content: str) -> ET._Element:
@@ -244,7 +239,7 @@ class ConfluencePageMetadata:
244
239
  domain: str
245
240
  base_path: str
246
241
  page_id: str
247
- space_key: str
242
+ space_key: Optional[str]
248
243
  title: str
249
244
 
250
245
 
@@ -976,6 +971,7 @@ class ConfluenceDocumentOptions:
976
971
  conversion rules for the identifier.
977
972
  :param generated_by: Text to use as the generated-by prompt (or `None` to omit a prompt).
978
973
  :param root_page_id: Confluence page to assume root page role for publishing a directory of Markdown files.
974
+ :param keep_hierarchy: Whether to maintain source directory structure when exporting to Confluence.
979
975
  :param render_mermaid: Whether to pre-render Mermaid diagrams into PNG/SVG images.
980
976
  :param diagram_output_format: Target image format for diagrams.
981
977
  :param webui_links: When true, convert relative URLs to Confluence Web UI links.
@@ -985,6 +981,7 @@ class ConfluenceDocumentOptions:
985
981
  heading_anchors: bool = False
986
982
  generated_by: Optional[str] = "This page has been generated with a tool."
987
983
  root_page_id: Optional[str] = None
984
+ keep_hierarchy: bool = False
988
985
  render_mermaid: bool = False
989
986
  diagram_output_format: Literal["png", "svg"] = "png"
990
987
  webui_links: bool = False
@@ -1132,10 +1129,6 @@ def _content_to_string(dtd_path: Path, content: str) -> str:
1132
1129
  def content_to_string(content: str) -> str:
1133
1130
  "Converts a Confluence Storage Format document returned by the API into a readable XML document."
1134
1131
 
1135
- if sys.version_info >= (3, 9):
1136
- resource_path = resources.files(__package__).joinpath("entities.dtd")
1137
- with resources.as_file(resource_path) as dtd_path:
1138
- return _content_to_string(dtd_path, content)
1139
- else:
1140
- with resources.path(__package__, "entities.dtd") as dtd_path:
1141
- return _content_to_string(dtd_path, content)
1132
+ resource_path = resources.files(__package__).joinpath("entities.dtd")
1133
+ with resources.as_file(resource_path) as dtd_path:
1134
+ return _content_to_string(dtd_path, content)
@@ -29,7 +29,11 @@ def get_mmdc() -> str:
29
29
  "Path to the Mermaid diagram converter."
30
30
 
31
31
  if is_docker():
32
- return "/home/md2conf/node_modules/.bin/mmdc"
32
+ full_path = "/home/md2conf/node_modules/.bin/mmdc"
33
+ if os.path.exists(full_path):
34
+ return full_path
35
+ else:
36
+ return "mmdc"
33
37
  elif os.name == "nt":
34
38
  return "mmdc.cmd"
35
39
  else:
@@ -17,7 +17,7 @@ class ConfluenceError(RuntimeError):
17
17
  class ConfluenceProperties:
18
18
  domain: str
19
19
  base_path: str
20
- space_key: str
20
+ space_key: Optional[str]
21
21
  user_name: Optional[str]
22
22
  api_key: str
23
23
  headers: Optional[dict[str, str]]
@@ -43,8 +43,6 @@ class ConfluenceProperties:
43
43
  opt_base_path = "/wiki/"
44
44
  if not opt_api_key:
45
45
  raise ConfluenceError("Confluence API key not specified")
46
- if not opt_space_key:
47
- raise ConfluenceError("Confluence space key not specified")
48
46
 
49
47
  if opt_domain.startswith(("http://", "https://")) or opt_domain.endswith("/"):
50
48
  raise ConfluenceError(
@@ -1,27 +0,0 @@
1
- """
2
- Publish Markdown files to Confluence wiki.
3
-
4
- Copyright 2022-2025, Levente Hunyadi
5
-
6
- :see: https://github.com/hunyadi/md2conf
7
- """
8
-
9
- import sys
10
-
11
- if sys.version_info >= (3, 9):
12
-
13
- def removeprefix(string: str, prefix: str) -> str:
14
- "If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
15
-
16
- return string.removeprefix(prefix)
17
-
18
- else:
19
-
20
- def removeprefix(string: str, prefix: str) -> str:
21
- "If the string starts with the prefix, return the string without the prefix; otherwise, return the original string."
22
-
23
- if string.startswith(prefix):
24
- prefix_len = len(prefix)
25
- return string[prefix_len:]
26
- else:
27
- return string