markdown-to-confluence 0.1.13__py3-none-any.whl → 0.2.1__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.
md2conf/mermaid.py ADDED
@@ -0,0 +1,54 @@
1
+ import os
2
+ import os.path
3
+ import shutil
4
+ import subprocess
5
+ from typing import Literal
6
+
7
+
8
+ def has_mmdc() -> bool:
9
+ "True if Mermaid diagram converter is available on the OS."
10
+
11
+ if os.name == "nt":
12
+ executable = "mmdc.cmd"
13
+ else:
14
+ executable = "mmdc"
15
+ return shutil.which(executable) is not None
16
+
17
+
18
+ def render(source: str, output_format: Literal["png", "svg"] = "png") -> bytes:
19
+ "Generates a PNG or SVG image from a Mermaid diagram source."
20
+
21
+ filename = f"tmp_mermaid.{output_format}"
22
+
23
+ if os.name == "nt":
24
+ executable = "mmdc.cmd"
25
+ else:
26
+ executable = "mmdc"
27
+ try:
28
+ cmd = [
29
+ executable,
30
+ "--input",
31
+ "-",
32
+ "--output",
33
+ filename,
34
+ "--outputFormat",
35
+ output_format,
36
+ ]
37
+ proc = subprocess.Popen(
38
+ cmd,
39
+ stdout=subprocess.PIPE,
40
+ stdin=subprocess.PIPE,
41
+ stderr=subprocess.PIPE,
42
+ text=False,
43
+ )
44
+ proc.communicate(input=source.encode("utf-8"))
45
+ if proc.returncode:
46
+ raise RuntimeError(
47
+ f"failed to convert Mermaid diagram; exit code: {proc.returncode}"
48
+ )
49
+ with open(filename, "rb") as image:
50
+ return image.read()
51
+
52
+ finally:
53
+ if os.path.exists(filename):
54
+ os.remove(filename)
md2conf/processor.py CHANGED
@@ -1,91 +1,113 @@
1
- import logging
2
- import os
3
- from pathlib import Path
4
- from typing import Dict
5
-
6
- from .converter import (
7
- ConfluenceDocument,
8
- ConfluenceDocumentOptions,
9
- ConfluencePageMetadata,
10
- extract_qualified_id,
11
- )
12
- from .properties import ConfluenceProperties
13
-
14
- LOGGER = logging.getLogger(__name__)
15
-
16
-
17
- class Processor:
18
- options: ConfluenceDocumentOptions
19
- properties: ConfluenceProperties
20
-
21
- def __init__(
22
- self, options: ConfluenceDocumentOptions, properties: ConfluenceProperties
23
- ) -> None:
24
- self.options = options
25
- self.properties = properties
26
-
27
- def process(self, path: Path) -> None:
28
- "Processes a single Markdown file or a directory of Markdown files."
29
-
30
- if path.is_dir():
31
- self.process_directory(path)
32
- elif path.is_file():
33
- self.process_page(path, {})
34
- else:
35
- raise ValueError(f"expected: valid file or directory path; got: {path}")
36
-
37
- def process_directory(self, local_dir: Path) -> None:
38
- "Recursively scans a directory hierarchy for Markdown files."
39
-
40
- page_metadata: Dict[Path, ConfluencePageMetadata] = {}
41
- LOGGER.info(f"Synchronizing directory: {local_dir}")
42
-
43
- # Step 1: build index of all page metadata
44
- # NOTE: Pathlib.walk() is implemented only in Python 3.12+
45
- # so sticking for old os.walk
46
- for root, directories, files in os.walk(local_dir):
47
- for file_name in files:
48
- # Reconstitute Path object back
49
- docfile = (Path(root) / file_name).absolute()
50
-
51
- # Skip non-markdown files
52
- if docfile.suffix.lower() != ".md":
53
- continue
54
-
55
- metadata = self._get_page(docfile)
56
- LOGGER.debug(f"indexed {docfile} with metadata: {metadata}")
57
- page_metadata[docfile] = metadata
58
-
59
- LOGGER.info(f"indexed {len(page_metadata)} pages")
60
-
61
- # Step 2: Convert each page
62
- for page_path in page_metadata.keys():
63
- self.process_page(page_path, page_metadata)
64
-
65
- def process_page(
66
- self, path: Path, page_metadata: Dict[Path, ConfluencePageMetadata]
67
- ) -> None:
68
- "Processes a single Markdown file."
69
-
70
- document = ConfluenceDocument(path, self.options, page_metadata)
71
- content = document.xhtml()
72
- with open(path.with_suffix(".csf"), "w", encoding="utf-8") as f:
73
- f.write(content)
74
-
75
- def _get_page(self, absolute_path: Path) -> ConfluencePageMetadata:
76
- "Extracts metadata from a Markdown file."
77
-
78
- with open(absolute_path, "r", encoding="utf-8") as f:
79
- document = f.read()
80
-
81
- qualified_id, document = extract_qualified_id(document)
82
- if qualified_id is None:
83
- raise ValueError("required: page ID for local output")
84
-
85
- return ConfluencePageMetadata(
86
- domain=self.properties.domain,
87
- base_path=self.properties.base_path,
88
- page_id=qualified_id.page_id,
89
- space_key=qualified_id.space_key or self.properties.space_key,
90
- title="",
91
- )
1
+ import hashlib
2
+ import logging
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Dict, List
6
+
7
+ from .converter import (
8
+ ConfluenceDocument,
9
+ ConfluenceDocumentOptions,
10
+ ConfluencePageMetadata,
11
+ ConfluenceQualifiedID,
12
+ extract_qualified_id,
13
+ )
14
+ from .properties import ConfluenceProperties
15
+
16
+ LOGGER = logging.getLogger(__name__)
17
+
18
+
19
+ class Processor:
20
+ options: ConfluenceDocumentOptions
21
+ properties: ConfluenceProperties
22
+
23
+ def __init__(
24
+ self, options: ConfluenceDocumentOptions, properties: ConfluenceProperties
25
+ ) -> None:
26
+ self.options = options
27
+ self.properties = properties
28
+
29
+ def process(self, path: Path) -> None:
30
+ "Processes a single Markdown file or a directory of Markdown files."
31
+
32
+ if path.is_dir():
33
+ self.process_directory(path)
34
+ elif path.is_file():
35
+ self.process_page(path, {})
36
+ else:
37
+ raise ValueError(f"expected: valid file or directory path; got: {path}")
38
+
39
+ def process_directory(self, local_dir: Path) -> None:
40
+ "Recursively scans a directory hierarchy for Markdown files."
41
+
42
+ LOGGER.info(f"Synchronizing directory: {local_dir}")
43
+
44
+ # Step 1: build index of all page metadata
45
+ page_metadata: Dict[Path, ConfluencePageMetadata] = {}
46
+ self._index_directory(local_dir, page_metadata)
47
+ LOGGER.info(f"indexed {len(page_metadata)} page(s)")
48
+
49
+ # Step 2: convert each page
50
+ for page_path in page_metadata.keys():
51
+ self.process_page(page_path, page_metadata)
52
+
53
+ def process_page(
54
+ self, path: Path, page_metadata: Dict[Path, ConfluencePageMetadata]
55
+ ) -> None:
56
+ "Processes a single Markdown file."
57
+
58
+ document = ConfluenceDocument(path, self.options, page_metadata)
59
+ content = document.xhtml()
60
+ with open(path.with_suffix(".csf"), "w", encoding="utf-8") as f:
61
+ f.write(content)
62
+
63
+ def _index_directory(
64
+ self,
65
+ local_dir: Path,
66
+ page_metadata: Dict[Path, ConfluencePageMetadata],
67
+ ) -> None:
68
+ "Indexes Markdown files in a directory recursively."
69
+
70
+ LOGGER.info(f"Indexing directory: {local_dir}")
71
+
72
+ files: List[Path] = []
73
+ directories: List[Path] = []
74
+ for entry in os.scandir(local_dir):
75
+ if entry.is_file():
76
+ if entry.name.endswith(".md"):
77
+ # skip non-markdown files
78
+ files.append((Path(local_dir) / entry.name).absolute())
79
+ elif entry.is_dir():
80
+ if not entry.name.startswith("."):
81
+ directories.append((Path(local_dir) / entry.name).absolute())
82
+
83
+ for doc in files:
84
+ metadata = self._get_page(doc)
85
+ LOGGER.debug(f"indexed {doc} with metadata: {metadata}")
86
+ page_metadata[doc] = metadata
87
+
88
+ for directory in directories:
89
+ self._index_directory(Path(local_dir) / directory, page_metadata)
90
+
91
+ def _get_page(self, absolute_path: Path) -> ConfluencePageMetadata:
92
+ "Extracts metadata from a Markdown file."
93
+
94
+ with open(absolute_path, "r", encoding="utf-8") as f:
95
+ document = f.read()
96
+
97
+ qualified_id, document = extract_qualified_id(document)
98
+ if qualified_id is None:
99
+ if self.options.root_page_id is not None:
100
+ hash = hashlib.md5(document.encode("utf-8"))
101
+ digest = "".join(f"{c:x}" for c in hash.digest())
102
+ LOGGER.info(f"Identifier '{digest}' assigned to page: {absolute_path}")
103
+ qualified_id = ConfluenceQualifiedID(digest)
104
+ else:
105
+ raise ValueError("required: page ID for local output")
106
+
107
+ return ConfluencePageMetadata(
108
+ domain=self.properties.domain,
109
+ base_path=self.properties.base_path,
110
+ page_id=qualified_id.page_id,
111
+ space_key=qualified_id.space_key or self.properties.space_key,
112
+ title="",
113
+ )
md2conf/properties.py CHANGED
@@ -1,52 +1,53 @@
1
- import os
2
- from typing import Optional
3
-
4
-
5
- class ConfluenceError(RuntimeError):
6
- pass
7
-
8
-
9
- class ConfluenceProperties:
10
- domain: str
11
- base_path: str
12
- space_key: str
13
- user_name: Optional[str]
14
- api_key: str
15
-
16
- def __init__(
17
- self,
18
- domain: Optional[str] = None,
19
- base_path: Optional[str] = None,
20
- user_name: Optional[str] = None,
21
- api_key: Optional[str] = None,
22
- space_key: Optional[str] = None,
23
- ) -> None:
24
- opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
25
- opt_base_path = base_path or os.getenv("CONFLUENCE_PATH")
26
- opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
27
- opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
28
- opt_space_key = space_key or os.getenv("CONFLUENCE_SPACE_KEY")
29
-
30
- if not opt_domain:
31
- raise ConfluenceError("Confluence domain not specified")
32
- if not opt_base_path:
33
- opt_base_path = "/wiki/"
34
- if not opt_api_key:
35
- raise ConfluenceError("Confluence API key not specified")
36
- if not opt_space_key:
37
- raise ConfluenceError("Confluence space key not specified")
38
-
39
- if opt_domain.startswith(("http://", "https://")) or opt_domain.endswith("/"):
40
- raise ConfluenceError(
41
- "Confluence domain looks like a URL; only host name required"
42
- )
43
- if not opt_base_path.startswith("/") or not opt_base_path.endswith("/"):
44
- raise ConfluenceError("Confluence base path must start and end with a '/'")
45
-
46
- self.domain = opt_domain
47
- self.base_path = opt_base_path
48
- self.user_name = opt_user_name
49
- self.api_key = opt_api_key
50
- self.space_key = opt_space_key
51
- self.space_key = opt_space_key
52
- self.space_key = opt_space_key
1
+ import os
2
+ from typing import Dict, Optional
3
+
4
+
5
+ class ConfluenceError(RuntimeError):
6
+ pass
7
+
8
+
9
+ class ConfluenceProperties:
10
+ domain: str
11
+ base_path: str
12
+ space_key: str
13
+ user_name: Optional[str]
14
+ api_key: str
15
+ headers: Optional[Dict[str, str]]
16
+
17
+ def __init__(
18
+ self,
19
+ domain: Optional[str] = None,
20
+ base_path: Optional[str] = None,
21
+ user_name: Optional[str] = None,
22
+ api_key: Optional[str] = None,
23
+ space_key: Optional[str] = None,
24
+ headers: Optional[Dict[str, str]] = None,
25
+ ) -> None:
26
+ opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
27
+ opt_base_path = base_path or os.getenv("CONFLUENCE_PATH")
28
+ opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
29
+ opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
30
+ opt_space_key = space_key or os.getenv("CONFLUENCE_SPACE_KEY")
31
+
32
+ if not opt_domain:
33
+ raise ConfluenceError("Confluence domain not specified")
34
+ if not opt_base_path:
35
+ opt_base_path = "/wiki/"
36
+ if not opt_api_key:
37
+ raise ConfluenceError("Confluence API key not specified")
38
+ if not opt_space_key:
39
+ raise ConfluenceError("Confluence space key not specified")
40
+
41
+ if opt_domain.startswith(("http://", "https://")) or opt_domain.endswith("/"):
42
+ raise ConfluenceError(
43
+ "Confluence domain looks like a URL; only host name required"
44
+ )
45
+ if not opt_base_path.startswith("/") or not opt_base_path.endswith("/"):
46
+ raise ConfluenceError("Confluence base path must start and end with a '/'")
47
+
48
+ self.domain = opt_domain
49
+ self.base_path = opt_base_path
50
+ self.user_name = opt_user_name
51
+ self.api_key = opt_api_key
52
+ self.space_key = opt_space_key
53
+ self.headers = headers
@@ -1,16 +0,0 @@
1
- md2conf/__init__.py,sha256=yAKxptdEAiSXE55X3TxhUYa2iMkbW9fo1CWVRX8FNUY,416
2
- md2conf/__main__.py,sha256=-ZztZII6QYeuukxlOkVm8Mh9M1_ksA-5I2e2o5mvKLI,4353
3
- md2conf/api.py,sha256=ghLdaQtltRJ2-SUjhOZNkohL-RJ0Y1h6SfT7RiLPPZM,15231
4
- md2conf/application.py,sha256=BuIWLWrU8yaMbQhK0n3SUEPjxc7XJ0RIUdE3ALw9umE,5411
5
- md2conf/converter.py,sha256=_wfhDG-8Q3iAtR28Hp_ja-jyRMeb4HOvjCBsg1YMSZM,18926
6
- md2conf/entities.dtd,sha256=6and_fCiCHKhC7XXX0Fs-AaC0k_YN6csRQ7RiqbwyuM,30752
7
- md2conf/processor.py,sha256=TRsDbrNswNUjkdP7vXEUPYZvC07P6QN92ddwTGXVljg,3175
8
- md2conf/properties.py,sha256=zjtOQ73gQa5clPC6Ep4WHVjYIjTwPfrAtGDK7CUqbyc,1863
9
- md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- markdown_to_confluence-0.1.13.dist-info/LICENSE,sha256=d0Kt_fnS_LlDYrLPPI5xUSDGXRWRZNv81uKvmHa3lZM,1098
11
- markdown_to_confluence-0.1.13.dist-info/METADATA,sha256=NcsPNl4S4PsKF_lM8tbxNbOjdpf1Be3OFec9AmdJrSs,8035
12
- markdown_to_confluence-0.1.13.dist-info/WHEEL,sha256=nCVcAvsfA9TDtwGwhYaRrlPhTLV9m-Ga6mdyDtuwK18,91
13
- markdown_to_confluence-0.1.13.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
14
- markdown_to_confluence-0.1.13.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
15
- markdown_to_confluence-0.1.13.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
16
- markdown_to_confluence-0.1.13.dist-info/RECORD,,