markdown-to-confluence 0.3.2__py3-none-any.whl → 0.3.4__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/processor.py CHANGED
@@ -6,98 +6,96 @@ Copyright 2022-2025, Levente Hunyadi
6
6
  :see: https://github.com/hunyadi/md2conf
7
7
  """
8
8
 
9
- import hashlib
10
9
  import logging
11
10
  import os
11
+ from abc import abstractmethod
12
12
  from pathlib import Path
13
13
  from typing import Optional
14
14
 
15
- from .converter import (
16
- ConfluenceDocument,
17
- ConfluenceDocumentOptions,
18
- ConfluencePageMetadata,
19
- ConfluenceQualifiedID,
20
- extract_qualified_id,
21
- )
15
+ from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID
22
16
  from .matcher import Matcher, MatcherOptions
23
- from .properties import ConfluenceProperties
17
+ from .metadata import ConfluencePageMetadata, ConfluenceSiteMetadata
18
+ from .properties import ArgumentError
24
19
 
25
20
  LOGGER = logging.getLogger(__name__)
26
21
 
27
22
 
28
23
  class Processor:
24
+ """
25
+ Processes a single Markdown page or a directory of Markdown pages.
26
+ """
27
+
29
28
  options: ConfluenceDocumentOptions
30
- properties: ConfluenceProperties
29
+ site: ConfluenceSiteMetadata
30
+ root_dir: Path
31
+
32
+ page_metadata: dict[Path, ConfluencePageMetadata]
31
33
 
32
34
  def __init__(
33
- self, options: ConfluenceDocumentOptions, properties: ConfluenceProperties
35
+ self,
36
+ options: ConfluenceDocumentOptions,
37
+ site: ConfluenceSiteMetadata,
38
+ root_dir: Path,
34
39
  ) -> None:
35
40
  self.options = options
36
- self.properties = properties
37
-
38
- def process(self, path: Path) -> None:
39
- "Processes a single Markdown file or a directory of Markdown files."
41
+ self.site = site
42
+ self.root_dir = root_dir
40
43
 
41
- path = path.resolve(True)
42
- if path.is_dir():
43
- self.process_directory(path)
44
- elif path.is_file():
45
- self.process_page(path)
46
- else:
47
- raise ValueError(f"expected: valid file or directory path; got: {path}")
44
+ self.page_metadata = {}
48
45
 
49
- def process_directory(
50
- self, local_dir: Path, root_dir: Optional[Path] = None
51
- ) -> None:
52
- "Recursively scans a directory hierarchy for Markdown files."
46
+ def process_directory(self, local_dir: Path) -> None:
47
+ """
48
+ Recursively scans a directory hierarchy for Markdown files, and processes each, resolving cross-references.
49
+ """
53
50
 
54
51
  local_dir = local_dir.resolve(True)
55
- if root_dir is None:
56
- root_dir = local_dir
57
- else:
58
- root_dir = root_dir.resolve(True)
59
-
60
- LOGGER.info("Synchronizing directory: %s", local_dir)
52
+ LOGGER.info("Processing directory: %s", local_dir)
61
53
 
62
54
  # Step 1: build index of all page metadata
63
- page_metadata: dict[Path, ConfluencePageMetadata] = {}
64
- self._index_directory(local_dir, page_metadata)
65
- LOGGER.info("Indexed %d page(s)", len(page_metadata))
55
+ self._index_directory(local_dir, self.options.root_page_id)
56
+ LOGGER.info("Indexed %d page(s)", len(self.page_metadata))
66
57
 
67
58
  # Step 2: convert each page
68
- for page_path in page_metadata.keys():
69
- self._process_page(page_path, root_dir, page_metadata)
59
+ for page_path in self.page_metadata.keys():
60
+ self._process_page(page_path)
70
61
 
71
- def process_page(self, path: Path, root_dir: Optional[Path] = None) -> None:
72
- "Processes a single Markdown file."
62
+ def process_page(self, path: Path) -> None:
63
+ """
64
+ Processes a single Markdown file.
65
+ """
73
66
 
74
- path = path.resolve(True)
75
- if root_dir is None:
76
- root_dir = path.parent
77
- else:
78
- root_dir = root_dir.resolve(True)
67
+ LOGGER.info("Processing page: %s", path)
68
+ self._index_page(path, self.options.root_page_id)
69
+ self._process_page(path)
79
70
 
80
- self._process_page(path, root_dir, {})
71
+ def _process_page(self, path: Path) -> None:
72
+ document = ConfluenceDocument.create(
73
+ path, self.options, self.root_dir, self.site, self.page_metadata
74
+ )
75
+ self._save_document(document, path)
81
76
 
82
- def _process_page(
77
+ @abstractmethod
78
+ def _get_or_create_page(
83
79
  self,
84
- path: Path,
85
- root_dir: Path,
86
- page_metadata: dict[Path, ConfluencePageMetadata],
87
- ) -> None:
88
- "Processes a single Markdown file."
89
-
90
- document = ConfluenceDocument(path, self.options, root_dir, page_metadata)
91
- content = document.xhtml()
92
- with open(path.with_suffix(".csf"), "w", encoding="utf-8") as f:
93
- f.write(content)
80
+ absolute_path: Path,
81
+ parent_id: Optional[ConfluencePageID],
82
+ *,
83
+ title: Optional[str] = None,
84
+ ) -> ConfluencePageMetadata:
85
+ """
86
+ Creates a new Confluence page if no page is linked in the Markdown document.
87
+ """
88
+ ...
89
+
90
+ @abstractmethod
91
+ def _save_document(self, document: ConfluenceDocument, path: Path) -> None: ...
94
92
 
95
93
  def _index_directory(
96
- self,
97
- local_dir: Path,
98
- page_metadata: dict[Path, ConfluencePageMetadata],
94
+ self, local_dir: Path, parent_id: Optional[ConfluencePageID]
99
95
  ) -> None:
100
- "Indexes Markdown files in a directory recursively."
96
+ """
97
+ Indexes Markdown files in a directory hierarchy recursively.
98
+ """
101
99
 
102
100
  LOGGER.info("Indexing directory: %s", local_dir)
103
101
 
@@ -114,34 +112,107 @@ class Processor:
114
112
  elif entry.is_dir():
115
113
  directories.append(Path(local_dir) / entry.name)
116
114
 
115
+ # make page act as parent node
116
+ parent_doc: Optional[Path] = None
117
+ if (Path(local_dir) / "index.md") in files:
118
+ parent_doc = Path(local_dir) / "index.md"
119
+ elif (Path(local_dir) / "README.md") in files:
120
+ parent_doc = Path(local_dir) / "README.md"
121
+ elif (Path(local_dir) / f"{local_dir.name}.md") in files:
122
+ parent_doc = Path(local_dir) / f"{local_dir.name}.md"
123
+
124
+ if parent_doc is None and self.options.keep_hierarchy:
125
+ parent_doc = Path(local_dir) / "index.md"
126
+
127
+ # create a blank page for directory entry
128
+ with open(parent_doc, "w"):
129
+ pass
130
+
131
+ if parent_doc is not None:
132
+ if parent_doc in files:
133
+ files.remove(parent_doc)
134
+
135
+ # use latest parent as parent for index page
136
+ metadata = self._get_or_create_page(parent_doc, parent_id)
137
+ LOGGER.debug("Indexed parent %s with metadata: %s", parent_doc, metadata)
138
+ self.page_metadata[parent_doc] = metadata
139
+
140
+ # assign new index page as new parent
141
+ parent_id = ConfluencePageID(metadata.page_id)
142
+
117
143
  for doc in files:
118
- metadata = self._get_page(doc)
119
- LOGGER.debug("Indexed %s with metadata: %s", doc, metadata)
120
- page_metadata[doc] = metadata
144
+ self._index_page(doc, parent_id)
121
145
 
122
146
  for directory in directories:
123
- self._index_directory(directory, page_metadata)
124
-
125
- def _get_page(self, absolute_path: Path) -> ConfluencePageMetadata:
126
- "Extracts metadata from a Markdown file."
127
-
128
- with open(absolute_path, "r", encoding="utf-8") as f:
129
- document = f.read()
130
-
131
- qualified_id, document = extract_qualified_id(document)
132
- if qualified_id is None:
133
- if self.options.root_page_id is not None:
134
- hash = hashlib.md5(document.encode("utf-8"))
135
- digest = "".join(f"{c:x}" for c in hash.digest())
136
- LOGGER.info("Identifier %s assigned to page: %s", digest, absolute_path)
137
- qualified_id = ConfluenceQualifiedID(digest)
138
- else:
139
- raise ValueError("required: page ID for local output")
140
-
141
- return ConfluencePageMetadata(
142
- domain=self.properties.domain,
143
- base_path=self.properties.base_path,
144
- page_id=qualified_id.page_id,
145
- space_key=qualified_id.space_key or self.properties.space_key,
146
- title="",
147
- )
147
+ self._index_directory(directory, parent_id)
148
+
149
+ def _index_page(self, path: Path, parent_id: Optional[ConfluencePageID]) -> None:
150
+ """
151
+ Indexes a single Markdown file.
152
+ """
153
+
154
+ metadata = self._get_or_create_page(path, parent_id)
155
+ LOGGER.debug("Indexed %s with metadata: %s", path, metadata)
156
+ self.page_metadata[path] = metadata
157
+
158
+
159
+ class ProcessorFactory:
160
+ options: ConfluenceDocumentOptions
161
+ site: ConfluenceSiteMetadata
162
+
163
+ def __init__(
164
+ self, options: ConfluenceDocumentOptions, site: ConfluenceSiteMetadata
165
+ ) -> None:
166
+ self.options = options
167
+ self.site = site
168
+
169
+ @abstractmethod
170
+ def create(self, root_dir: Path) -> Processor: ...
171
+
172
+
173
+ class Converter:
174
+ factory: ProcessorFactory
175
+
176
+ def __init__(self, factory: ProcessorFactory) -> None:
177
+ self.factory = factory
178
+
179
+ def process(self, path: Path) -> None:
180
+ """
181
+ Processes a single Markdown file or a directory of Markdown files.
182
+ """
183
+
184
+ path = path.resolve(True)
185
+ if path.is_dir():
186
+ self.process_directory(path)
187
+ elif path.is_file():
188
+ self.process_page(path)
189
+ else:
190
+ raise ArgumentError(f"expected: valid file or directory path; got: {path}")
191
+
192
+ def process_directory(
193
+ self, local_dir: Path, root_dir: Optional[Path] = None
194
+ ) -> None:
195
+ """
196
+ Recursively scans a directory hierarchy for Markdown files, and processes each, resolving cross-references.
197
+ """
198
+
199
+ local_dir = local_dir.resolve(True)
200
+ if root_dir is None:
201
+ root_dir = local_dir
202
+ else:
203
+ root_dir = root_dir.resolve(True)
204
+
205
+ self.factory.create(root_dir).process_directory(local_dir)
206
+
207
+ def process_page(self, path: Path, root_dir: Optional[Path] = None) -> None:
208
+ """
209
+ Processes a single Markdown file.
210
+ """
211
+
212
+ path = path.resolve(True)
213
+ if root_dir is None:
214
+ root_dir = path.parent
215
+ else:
216
+ root_dir = root_dir.resolve(True)
217
+
218
+ self.factory.create(root_dir).process_page(path)
md2conf/properties.py CHANGED
@@ -10,50 +10,74 @@ import os
10
10
  from typing import Optional
11
11
 
12
12
 
13
+ class ArgumentError(ValueError):
14
+ "Raised when wrong arguments are passed to a function call."
15
+
16
+
17
+ class PageError(ValueError):
18
+ "Raised in case there is an issue with a Confluence page."
19
+
20
+
13
21
  class ConfluenceError(RuntimeError):
14
- pass
22
+ "Raised when a Confluence API call fails."
15
23
 
16
24
 
17
- class ConfluenceProperties:
25
+ class ConfluenceSiteProperties:
18
26
  domain: str
19
27
  base_path: str
20
28
  space_key: Optional[str]
21
- user_name: Optional[str]
22
- api_key: str
23
- headers: Optional[dict[str, str]]
24
29
 
25
30
  def __init__(
26
31
  self,
27
32
  domain: Optional[str] = None,
28
33
  base_path: Optional[str] = None,
29
- user_name: Optional[str] = None,
30
- api_key: Optional[str] = None,
31
34
  space_key: Optional[str] = None,
32
- headers: Optional[dict[str, str]] = None,
33
35
  ) -> None:
34
36
  opt_domain = domain or os.getenv("CONFLUENCE_DOMAIN")
35
37
  opt_base_path = base_path or os.getenv("CONFLUENCE_PATH")
36
- opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
37
- opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
38
38
  opt_space_key = space_key or os.getenv("CONFLUENCE_SPACE_KEY")
39
39
 
40
40
  if not opt_domain:
41
- raise ConfluenceError("Confluence domain not specified")
41
+ raise ArgumentError("Confluence domain not specified")
42
42
  if not opt_base_path:
43
43
  opt_base_path = "/wiki/"
44
- if not opt_api_key:
45
- raise ConfluenceError("Confluence API key not specified")
46
44
 
47
45
  if opt_domain.startswith(("http://", "https://")) or opt_domain.endswith("/"):
48
- raise ConfluenceError(
46
+ raise ArgumentError(
49
47
  "Confluence domain looks like a URL; only host name required"
50
48
  )
51
49
  if not opt_base_path.startswith("/") or not opt_base_path.endswith("/"):
52
- raise ConfluenceError("Confluence base path must start and end with a '/'")
50
+ raise ArgumentError("Confluence base path must start and end with a '/'")
53
51
 
54
52
  self.domain = opt_domain
55
53
  self.base_path = opt_base_path
54
+ self.space_key = opt_space_key
55
+
56
+
57
+ class ConfluenceConnectionProperties(ConfluenceSiteProperties):
58
+ "Properties related to connecting to Confluence."
59
+
60
+ user_name: Optional[str]
61
+ api_key: str
62
+ headers: Optional[dict[str, str]]
63
+
64
+ def __init__(
65
+ self,
66
+ domain: Optional[str] = None,
67
+ base_path: Optional[str] = None,
68
+ user_name: Optional[str] = None,
69
+ api_key: Optional[str] = None,
70
+ space_key: Optional[str] = None,
71
+ headers: Optional[dict[str, str]] = None,
72
+ ) -> None:
73
+ super().__init__(domain, base_path, space_key)
74
+
75
+ opt_user_name = user_name or os.getenv("CONFLUENCE_USER_NAME")
76
+ opt_api_key = api_key or os.getenv("CONFLUENCE_API_KEY")
77
+
78
+ if not opt_api_key:
79
+ raise ArgumentError("Confluence API key not specified")
80
+
56
81
  self.user_name = opt_user_name
57
82
  self.api_key = opt_api_key
58
- self.space_key = opt_space_key
59
83
  self.headers = headers
@@ -1,20 +0,0 @@
1
- markdown_to_confluence-0.3.2.dist-info/licenses/LICENSE,sha256=Pv43so2bPfmKhmsrmXFyAvS7M30-1i1tzjz6-dfhyOo,1077
2
- md2conf/__init__.py,sha256=IyztAgNkEXTxDlsTrxZOAkeNIzLSSzjNV0ZHg2SvDDE,402
3
- md2conf/__main__.py,sha256=ypjV_5mE0smlIRBFrpikgzXq18as2hY43HJxMLpzGp4,7145
4
- md2conf/api.py,sha256=-bneMbfpspfat4J63Z7bY0kdlW8yJguRRNbx6CaOkGY,20101
5
- md2conf/application.py,sha256=5K-nCPHJZfIahjubrLtXTwI-zsTiD140fdYXDnh3GSk,9161
6
- md2conf/converter.py,sha256=MoGbXqh5rE4qkdxxY8RHcnoZ5mz0aEuFz9nmUnt0WdM,36397
7
- md2conf/emoji.py,sha256=IZeguWqcboeOyJkGLTVONDMO4ZXfYXPgfkp56PTI-hE,1924
8
- md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
9
- md2conf/matcher.py,sha256=FgMFPvGiOqGezCs8OyerfsVo-iIHFoI6LRMzdcjM5UY,3693
10
- md2conf/mermaid.py,sha256=Alzkv0BY-lju4ojtBdW2qtCLZ59MO9kaS2RpQO6Kyfk,2304
11
- md2conf/processor.py,sha256=G-MIh1jGq9jjgogHnlnRUSrNgiV6_xO6Fy7ct9alqgM,4769
12
- md2conf/properties.py,sha256=WaVVOYSck7drVQfcBJmBMa7Mb0KVOZl9UZHvLS1Du8U,1892
13
- md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
14
- md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- markdown_to_confluence-0.3.2.dist-info/METADATA,sha256=2exvsuRCcD3L9CN7fl0gqki1gsqQgHDxGyjnKhDeYmw,14958
16
- markdown_to_confluence-0.3.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
17
- markdown_to_confluence-0.3.2.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
18
- markdown_to_confluence-0.3.2.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
19
- markdown_to_confluence-0.3.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
20
- markdown_to_confluence-0.3.2.dist-info/RECORD,,