markdown-to-confluence 0.4.0__py3-none-any.whl → 0.4.2__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
@@ -10,17 +10,13 @@ import logging
10
10
  from pathlib import Path
11
11
  from typing import Optional
12
12
 
13
- from .api import ConfluenceLabel, ConfluenceSession
14
- from .converter import (
15
- ConfluenceDocument,
16
- ConfluenceDocumentOptions,
17
- ConfluencePageID,
18
- attachment_name,
19
- )
20
- from .extra import override
13
+ from .api import ConfluenceContentProperty, ConfluenceLabel, ConfluenceSession, ConfluenceStatus
14
+ from .converter import ConfluenceDocument, ConfluenceDocumentOptions, ConfluencePageID, attachment_name, elements_from_string, get_volatile_attributes
15
+ from .extra import override, path_relative_to
21
16
  from .metadata import ConfluencePageMetadata
22
17
  from .processor import Converter, DocumentNode, Processor, ProcessorFactory
23
18
  from .properties import PageError
19
+ from .xml import is_xml_equal
24
20
 
25
21
  LOGGER = logging.getLogger(__name__)
26
22
 
@@ -32,9 +28,7 @@ class SynchronizingProcessor(Processor):
32
28
 
33
29
  api: ConfluenceSession
34
30
 
35
- def __init__(
36
- self, api: ConfluenceSession, options: ConfluenceDocumentOptions, root_dir: Path
37
- ) -> None:
31
+ def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions, root_dir: Path) -> None:
38
32
  """
39
33
  Initializes a new processor instance.
40
34
 
@@ -47,9 +41,7 @@ class SynchronizingProcessor(Processor):
47
41
  self.api = api
48
42
 
49
43
  @override
50
- def _synchronize_tree(
51
- self, root: DocumentNode, root_id: Optional[ConfluencePageID]
52
- ) -> None:
44
+ def _synchronize_tree(self, root: DocumentNode, root_id: Optional[ConfluencePageID]) -> None:
53
45
  """
54
46
  Creates the cross-reference index and synchronizes the directory tree structure with the Confluence page hierarchy.
55
47
 
@@ -59,14 +51,10 @@ class SynchronizingProcessor(Processor):
59
51
  """
60
52
 
61
53
  if root.page_id is None and root_id is None:
62
- raise PageError(
63
- f"expected: root page ID in options, or explicit page ID in {root.absolute_path}"
64
- )
54
+ raise PageError(f"expected: root page ID in options, or explicit page ID in {root.absolute_path}")
65
55
  elif root.page_id is not None and root_id is not None:
66
56
  if root.page_id != root_id.page_id:
67
- raise PageError(
68
- f"mismatched inferred page ID of {root_id.page_id} and explicit page ID in {root.absolute_path}"
69
- )
57
+ raise PageError(f"mismatched inferred page ID of {root_id.page_id} and explicit page ID in {root.absolute_path}")
70
58
 
71
59
  real_id = root_id
72
60
  elif root_id is not None:
@@ -78,9 +66,7 @@ class SynchronizingProcessor(Processor):
78
66
 
79
67
  self._synchronize_subtree(root, real_id)
80
68
 
81
- def _synchronize_subtree(
82
- self, node: DocumentNode, parent_id: ConfluencePageID
83
- ) -> None:
69
+ def _synchronize_subtree(self, node: DocumentNode, parent_id: ConfluencePageID) -> None:
84
70
  if node.page_id is not None:
85
71
  # verify if page exists
86
72
  page = self.api.get_page_properties(node.page_id)
@@ -88,6 +74,10 @@ class SynchronizingProcessor(Processor):
88
74
  elif node.title is not None:
89
75
  # look up page by title
90
76
  page = self.api.get_or_create_page(node.title, parent_id.page_id)
77
+
78
+ if page.status is ConfluenceStatus.ARCHIVED:
79
+ raise PageError(f"unable to update archived page with ID {page.id}")
80
+
91
81
  update = True
92
82
  else:
93
83
  # always create a new page
@@ -108,6 +98,7 @@ class SynchronizingProcessor(Processor):
108
98
  page_id=page.id,
109
99
  space_key=space_key,
110
100
  title=page.title,
101
+ synchronized=node.synchronized,
111
102
  )
112
103
  self.page_metadata.add(node.absolute_path, data)
113
104
 
@@ -115,9 +106,7 @@ class SynchronizingProcessor(Processor):
115
106
  self._synchronize_subtree(child_node, ConfluencePageID(page.id))
116
107
 
117
108
  @override
118
- def _update_page(
119
- self, page_id: ConfluencePageID, document: ConfluenceDocument, path: Path
120
- ) -> None:
109
+ def _update_page(self, page_id: ConfluencePageID, document: ConfluenceDocument, path: Path) -> None:
121
110
  """
122
111
  Saves a new version of a Confluence document.
123
112
 
@@ -125,11 +114,11 @@ class SynchronizingProcessor(Processor):
125
114
  """
126
115
 
127
116
  base_path = path.parent
128
- for image in document.images:
117
+ for image_path in document.images:
129
118
  self.api.upload_attachment(
130
119
  page_id.page_id,
131
- attachment_name(image),
132
- attachment_path=base_path / image,
120
+ attachment_name(path_relative_to(image_path, base_path)),
121
+ attachment_path=image_path,
133
122
  )
134
123
 
135
124
  for name, data in document.embedded_images.items():
@@ -145,14 +134,8 @@ class SynchronizingProcessor(Processor):
145
134
  title = None
146
135
  if document.title is not None:
147
136
  meta = self.page_metadata.get(path)
148
- if (
149
- meta is not None
150
- and meta.space_key is not None
151
- and meta.title != document.title
152
- ):
153
- conflicting_page_id = self.api.page_exists(
154
- document.title, space_id=self.api.space_key_to_id(meta.space_key)
155
- )
137
+ if meta is not None and meta.space_key is not None and meta.title != document.title:
138
+ conflicting_page_id = self.api.page_exists(document.title, space_id=self.api.space_key_to_id(meta.space_key))
156
139
  if conflicting_page_id is None:
157
140
  title = document.title
158
141
  else:
@@ -162,17 +145,30 @@ class SynchronizingProcessor(Processor):
162
145
  conflicting_page_id,
163
146
  )
164
147
 
165
- self.api.update_page(page_id.page_id, content, title=title)
148
+ # fetch existing page
149
+ page = self.api.get_page(page_id.page_id)
150
+ if not title: # empty or `None`
151
+ title = page.title
152
+
153
+ # check if page has any changes
154
+ if page.title != title or not is_xml_equal(
155
+ document.root,
156
+ elements_from_string(page.content),
157
+ skip_attributes=get_volatile_attributes(),
158
+ ):
159
+ self.api.update_page(page_id.page_id, content, title=title, version=page.version.number + 1)
160
+ else:
161
+ LOGGER.info("Up-to-date page: %s", page_id)
166
162
 
167
163
  if document.labels is not None:
168
164
  self.api.update_labels(
169
165
  page_id.page_id,
170
- [
171
- ConfluenceLabel(name=label, prefix="global")
172
- for label in document.labels
173
- ],
166
+ [ConfluenceLabel(name=label, prefix="global") for label in document.labels],
174
167
  )
175
168
 
169
+ if document.properties is not None:
170
+ self.api.update_content_properties_for_page(page_id.page_id, [ConfluenceContentProperty(key, value) for key, value in document.properties.items()])
171
+
176
172
  def _update_markdown(self, path: Path, *, page_id: str, space_key: str) -> None:
177
173
  """
178
174
  Writes the Confluence page ID and space key at the beginning of the Markdown file.
@@ -202,9 +198,7 @@ class SynchronizingProcessor(Processor):
202
198
  class SynchronizingProcessorFactory(ProcessorFactory):
203
199
  api: ConfluenceSession
204
200
 
205
- def __init__(
206
- self, api: ConfluenceSession, options: ConfluenceDocumentOptions
207
- ) -> None:
201
+ def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions) -> None:
208
202
  super().__init__(options, api.site)
209
203
  self.api = api
210
204
 
@@ -219,7 +213,5 @@ class Application(Converter):
219
213
  This is the class instantiated by the command-line application.
220
214
  """
221
215
 
222
- def __init__(
223
- self, api: ConfluenceSession, options: ConfluenceDocumentOptions
224
- ) -> None:
216
+ def __init__(self, api: ConfluenceSession, options: ConfluenceDocumentOptions) -> None:
225
217
  super().__init__(SynchronizingProcessorFactory(api, options))
md2conf/collection.py CHANGED
@@ -7,25 +7,31 @@ Copyright 2022-2025, Levente Hunyadi
7
7
  """
8
8
 
9
9
  from pathlib import Path
10
- from typing import Iterable, Optional
10
+ from typing import Generic, Iterable, Optional, TypeVar
11
11
 
12
12
  from .metadata import ConfluencePageMetadata
13
13
 
14
+ K = TypeVar("K")
15
+ V = TypeVar("V")
14
16
 
15
- class ConfluencePageCollection:
16
- _metadata: dict[Path, ConfluencePageMetadata]
17
+
18
+ class KeyValueCollection(Generic[K, V]):
19
+ _collection: dict[K, V]
17
20
 
18
21
  def __init__(self) -> None:
19
- self._metadata = {}
22
+ self._collection = {}
20
23
 
21
24
  def __len__(self) -> int:
22
- return len(self._metadata)
25
+ return len(self._collection)
26
+
27
+ def add(self, key: K, data: V) -> None:
28
+ self._collection[key] = data
29
+
30
+ def get(self, key: K) -> Optional[V]:
31
+ return self._collection.get(key)
23
32
 
24
- def add(self, path: Path, data: ConfluencePageMetadata) -> None:
25
- self._metadata[path] = data
33
+ def items(self) -> Iterable[tuple[K, V]]:
34
+ return self._collection.items()
26
35
 
27
- def get(self, path: Path) -> Optional[ConfluencePageMetadata]:
28
- return self._metadata.get(path)
29
36
 
30
- def items(self) -> Iterable[tuple[Path, ConfluencePageMetadata]]:
31
- return self._metadata.items()
37
+ class ConfluencePageCollection(KeyValueCollection[Path, ConfluencePageMetadata]): ...