markdown-to-confluence 0.3.5__py3-none-any.whl → 0.4.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/application.py CHANGED
@@ -6,22 +6,16 @@ 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
  from pathlib import Path
12
11
  from typing import Optional
13
12
 
14
- from .api import ConfluencePage, ConfluenceSession
15
- from .converter import (
16
- ConfluenceDocument,
17
- ConfluenceDocumentOptions,
18
- ConfluencePageID,
19
- attachment_name,
20
- )
13
+ from .api import ConfluenceContentProperty, ConfluenceLabel, ConfluenceSession, ConfluenceStatus
14
+ from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID, attachment_name
15
+ from .extra import override, path_relative_to
21
16
  from .metadata import ConfluencePageMetadata
22
- from .processor import Converter, Processor, ProcessorFactory
17
+ from .processor import Converter, DocumentNode, Processor, ProcessorFactory
23
18
  from .properties import PageError
24
- from .scanner import Scanner
25
19
 
26
20
  LOGGER = logging.getLogger(__name__)
27
21
 
@@ -33,9 +27,7 @@ class SynchronizingProcessor(Processor):
33
27
 
34
28
  api: ConfluenceSession
35
29
 
36
- def __init__(
37
- self, api: ConfluenceSession, options: ConfluenceDocumentOptions, root_dir: Path
38
- ) -> None:
30
+ def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions, root_dir: Path) -> None:
39
31
  """
40
32
  Initializes a new processor instance.
41
33
 
@@ -47,71 +39,72 @@ class SynchronizingProcessor(Processor):
47
39
  super().__init__(options, api.site, root_dir)
48
40
  self.api = api
49
41
 
50
- def _get_or_create_page(
51
- self, absolute_path: Path, parent_id: Optional[ConfluencePageID]
52
- ) -> ConfluencePageMetadata:
42
+ @override
43
+ def _synchronize_tree(self, root: DocumentNode, root_id: Optional[ConfluencePageID]) -> None:
53
44
  """
54
- Creates a new Confluence page if no page is linked in the Markdown document.
45
+ Creates the cross-reference index and synchronizes the directory tree structure with the Confluence page hierarchy.
46
+
47
+ Creates new Confluence pages as necessary, e.g. if no page is linked in the Markdown document, or no page is found with lookup by page title.
48
+
49
+ Updates the original Markdown document to add tags to associate the document with its corresponding Confluence page.
55
50
  """
56
51
 
57
- # parse file
58
- document = Scanner().read(absolute_path)
59
-
60
- overwrite = False
61
- if document.page_id is None:
62
- # create new Confluence page
63
- if parent_id is None:
64
- raise PageError(
65
- f"expected: parent page ID for Markdown file with no linked Confluence page: {absolute_path}"
66
- )
67
-
68
- # use file name (without extension) and path hash if no title is supplied
69
- if document.title is not None:
70
- title = document.title
71
- else:
72
- overwrite = True
73
- relative_path = absolute_path.relative_to(self.root_dir)
74
- hash = hashlib.md5(relative_path.as_posix().encode("utf-8"))
75
- digest = "".join(f"{c:x}" for c in hash.digest())
76
- title = f"{absolute_path.stem} [{digest}]"
77
-
78
- confluence_page = self._create_page(
79
- absolute_path, document.text, title, parent_id
80
- )
52
+ if root.page_id is None and root_id is None:
53
+ raise PageError(f"expected: root page ID in options, or explicit page ID in {root.absolute_path}")
54
+ elif root.page_id is not None and root_id is not None:
55
+ if root.page_id != root_id.page_id:
56
+ raise PageError(f"mismatched inferred page ID of {root_id.page_id} and explicit page ID in {root.absolute_path}")
57
+
58
+ real_id = root_id
59
+ elif root_id is not None:
60
+ real_id = root_id
61
+ elif root.page_id is not None:
62
+ real_id = ConfluencePageID(root.page_id)
81
63
  else:
82
- # look up existing Confluence page
83
- confluence_page = self.api.get_page(document.page_id)
84
-
85
- return ConfluencePageMetadata(
86
- page_id=confluence_page.id,
87
- space_key=self.api.space_id_to_key(confluence_page.space_id),
88
- title=confluence_page.title,
89
- overwrite=overwrite,
90
- )
64
+ raise NotImplementedError("condition not exhaustive")
91
65
 
92
- def _create_page(
93
- self,
94
- absolute_path: Path,
95
- document: str,
96
- title: str,
97
- parent_id: ConfluencePageID,
98
- ) -> ConfluencePage:
99
- """
100
- Creates a new Confluence page when Markdown file doesn't have an embedded page ID yet.
101
- """
66
+ self._synchronize_subtree(root, real_id)
67
+
68
+ def _synchronize_subtree(self, node: DocumentNode, parent_id: ConfluencePageID) -> None:
69
+ if node.page_id is not None:
70
+ # verify if page exists
71
+ page = self.api.get_page_properties(node.page_id)
72
+ update = False
73
+ elif node.title is not None:
74
+ # look up page by title
75
+ page = self.api.get_or_create_page(node.title, parent_id.page_id)
76
+
77
+ if page.status is ConfluenceStatus.ARCHIVED:
78
+ raise PageError(f"unable to update archived page with ID {page.id}")
102
79
 
103
- confluence_page = self.api.get_or_create_page(title, parent_id.page_id)
104
- self._update_markdown(
105
- absolute_path,
106
- document,
107
- confluence_page.id,
108
- self.api.space_id_to_key(confluence_page.space_id),
80
+ update = True
81
+ else:
82
+ # always create a new page
83
+ digest = self._generate_hash(node.absolute_path)
84
+ title = f"{node.absolute_path.stem} [{digest}]"
85
+ page = self.api.create_page(parent_id.page_id, title, "")
86
+ update = True
87
+
88
+ space_key = self.api.space_id_to_key(page.spaceId)
89
+ if update:
90
+ self._update_markdown(
91
+ node.absolute_path,
92
+ page_id=page.id,
93
+ space_key=space_key,
94
+ )
95
+
96
+ data = ConfluencePageMetadata(
97
+ page_id=page.id,
98
+ space_key=space_key,
99
+ title=page.title,
109
100
  )
110
- return confluence_page
101
+ self.page_metadata.add(node.absolute_path, data)
102
+
103
+ for child_node in node.children():
104
+ self._synchronize_subtree(child_node, ConfluencePageID(page.id))
111
105
 
112
- def _save_document(
113
- self, page_id: ConfluencePageID, document: ConfluenceDocument, path: Path
114
- ) -> None:
106
+ @override
107
+ def _update_page(self, page_id: ConfluencePageID, document: ConfluenceDocument, path: Path) -> None:
115
108
  """
116
109
  Saves a new version of a Confluence document.
117
110
 
@@ -119,11 +112,11 @@ class SynchronizingProcessor(Processor):
119
112
  """
120
113
 
121
114
  base_path = path.parent
122
- for image in document.images:
115
+ for image_path in document.images:
123
116
  self.api.upload_attachment(
124
117
  page_id.page_id,
125
- attachment_name(image),
126
- attachment_path=base_path / image,
118
+ attachment_name(path_relative_to(image_path, base_path)),
119
+ attachment_path=image_path,
127
120
  )
128
121
 
129
122
  for name, data in document.embedded_images.items():
@@ -138,13 +131,9 @@ class SynchronizingProcessor(Processor):
138
131
 
139
132
  title = None
140
133
  if document.title is not None:
141
- meta = self.page_metadata[path]
142
-
143
- # update title only for pages with randomly assigned title
144
- if meta.overwrite:
145
- conflicting_page_id = self.api.page_exists(
146
- document.title, space_id=self.api.space_key_to_id(meta.space_key)
147
- )
134
+ meta = self.page_metadata.get(path)
135
+ if meta is not None and meta.space_key is not None and meta.title != document.title:
136
+ conflicting_page_id = self.api.page_exists(document.title, space_id=self.api.space_key_to_id(meta.space_key))
148
137
  if conflicting_page_id is None:
149
138
  title = document.title
150
139
  else:
@@ -156,17 +145,23 @@ class SynchronizingProcessor(Processor):
156
145
 
157
146
  self.api.update_page(page_id.page_id, content, title=title)
158
147
 
159
- def _update_markdown(
160
- self,
161
- path: Path,
162
- document: str,
163
- page_id: str,
164
- space_key: Optional[str],
165
- ) -> None:
148
+ if document.labels is not None:
149
+ self.api.update_labels(
150
+ page_id.page_id,
151
+ [ConfluenceLabel(name=label, prefix="global") for label in document.labels],
152
+ )
153
+
154
+ if document.properties is not None:
155
+ self.api.update_content_properties_for_page(page_id.page_id, [ConfluenceContentProperty(key, value) for key, value in document.properties.items()])
156
+
157
+ def _update_markdown(self, path: Path, *, page_id: str, space_key: str) -> None:
166
158
  """
167
159
  Writes the Confluence page ID and space key at the beginning of the Markdown file.
168
160
  """
169
161
 
162
+ with open(path, "r", encoding="utf-8") as file:
163
+ document = file.read()
164
+
170
165
  content: list[str] = []
171
166
 
172
167
  # check if the file has frontmatter
@@ -178,9 +173,7 @@ class SynchronizingProcessor(Processor):
178
173
  content.append(document[:index])
179
174
 
180
175
  content.append(f"<!-- confluence-page-id: {page_id} -->")
181
- if space_key:
182
- content.append(f"<!-- confluence-space-key: {space_key} -->")
183
-
176
+ content.append(f"<!-- confluence-space-key: {space_key} -->")
184
177
  content.append(document[index:])
185
178
 
186
179
  with open(path, "w", encoding="utf-8") as file:
@@ -190,9 +183,7 @@ class SynchronizingProcessor(Processor):
190
183
  class SynchronizingProcessorFactory(ProcessorFactory):
191
184
  api: ConfluenceSession
192
185
 
193
- def __init__(
194
- self, api: ConfluenceSession, options: ConfluenceDocumentOptions
195
- ) -> None:
186
+ def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions) -> None:
196
187
  super().__init__(options, api.site)
197
188
  self.api = api
198
189
 
@@ -207,7 +198,5 @@ class Application(Converter):
207
198
  This is the class instantiated by the command-line application.
208
199
  """
209
200
 
210
- def __init__(
211
- self, api: ConfluenceSession, options: ConfluenceDocumentOptions
212
- ) -> None:
201
+ def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions) -> None:
213
202
  super().__init__(SynchronizingProcessorFactory(api, options))
md2conf/collection.py ADDED
@@ -0,0 +1,31 @@
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
+ from pathlib import Path
10
+ from typing import Iterable, Optional
11
+
12
+ from .metadata import ConfluencePageMetadata
13
+
14
+
15
+ class ConfluencePageCollection:
16
+ _metadata: dict[Path, ConfluencePageMetadata]
17
+
18
+ def __init__(self) -> None:
19
+ self._metadata = {}
20
+
21
+ def __len__(self) -> int:
22
+ return len(self._metadata)
23
+
24
+ def add(self, path: Path, data: ConfluencePageMetadata) -> None:
25
+ self._metadata[path] = data
26
+
27
+ def get(self, path: Path) -> Optional[ConfluencePageMetadata]:
28
+ return self._metadata.get(path)
29
+
30
+ def items(self) -> Iterable[tuple[Path, ConfluencePageMetadata]]:
31
+ return self._metadata.items()