markdown-to-confluence 0.5.3__py3-none-any.whl → 0.5.5__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.
Files changed (38) hide show
  1. {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/METADATA +275 -208
  2. markdown_to_confluence-0.5.5.dist-info/RECORD +57 -0
  3. {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/WHEEL +1 -1
  4. md2conf/__init__.py +1 -1
  5. md2conf/__main__.py +61 -189
  6. md2conf/api.py +35 -69
  7. md2conf/attachment.py +4 -3
  8. md2conf/clio.py +226 -0
  9. md2conf/compatibility.py +5 -0
  10. md2conf/converter.py +239 -147
  11. md2conf/csf.py +89 -9
  12. md2conf/drawio/extension.py +3 -3
  13. md2conf/drawio/render.py +2 -0
  14. md2conf/extension.py +4 -0
  15. md2conf/external.py +25 -8
  16. md2conf/frontmatter.py +18 -6
  17. md2conf/image.py +17 -14
  18. md2conf/latex.py +8 -1
  19. md2conf/markdown.py +68 -1
  20. md2conf/mermaid/render.py +1 -1
  21. md2conf/options.py +95 -24
  22. md2conf/plantuml/extension.py +7 -7
  23. md2conf/plantuml/render.py +6 -7
  24. md2conf/png.py +10 -6
  25. md2conf/processor.py +24 -3
  26. md2conf/publisher.py +193 -36
  27. md2conf/reflection.py +74 -0
  28. md2conf/scanner.py +16 -6
  29. md2conf/serializer.py +12 -1
  30. md2conf/svg.py +131 -109
  31. md2conf/toc.py +72 -0
  32. md2conf/xml.py +45 -0
  33. markdown_to_confluence-0.5.3.dist-info/RECORD +0 -55
  34. {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/entry_points.txt +0 -0
  35. {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/licenses/LICENSE +0 -0
  36. {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/top_level.txt +0 -0
  37. {markdown_to_confluence-0.5.3.dist-info → markdown_to_confluence-0.5.5.dist-info}/zip-safe +0 -0
  38. /md2conf/{puppeteer-config.json → mermaid/puppeteer-config.json} +0 -0
@@ -0,0 +1,57 @@
1
+ markdown_to_confluence-0.5.5.dist-info/licenses/LICENSE,sha256=SEEBf2BMI1LUHnDvyHnV6L12A6zTAOQcsyMvaawAXWo,1077
2
+ md2conf/__init__.py,sha256=TlQmZzCRIxd5nfwKqbjScEoeeE6okogDHEHRyXzmQBo,402
3
+ md2conf/__main__.py,sha256=4D4OiRJUY1m0UnhndIjg9bOFfAOsuyw12OzgxzypFnw,9480
4
+ md2conf/api.py,sha256=6lJnu-daNxDhOoJdTQ3GAyt6RkFSIP92iC4ptm8sNhs,43028
5
+ md2conf/attachment.py,sha256=3fGLXX3utOP9dZeZqfs9sFiwH5yFgk7ixfhKbTfUb8U,1720
6
+ md2conf/clio.py,sha256=pyiZKipOTQUqR6f0fblchPL1yAr6mM_GPCzS_-Kv1aQ,7851
7
+ md2conf/coalesce.py,sha256=YHnqFwow5wCj6OQ3oosig01D2lxWusAScMF4HAUO2-g,1305
8
+ md2conf/collection.py,sha256=ukN74VCa4HaGSh6tLXpLd0j_UNPywcnKI0X7usgdSCo,824
9
+ md2conf/compatibility.py,sha256=b6n_JFRlNU5Jx4RJqS7FgMp4tk0iYGFxb4--fkIsQcM,873
10
+ md2conf/converter.py,sha256=M7jILSSlY_G1WsyQ9tcZ3MsNmGH1T1GTUIiJQ0SSjlY,66627
11
+ md2conf/csf.py,sha256=w-ng621pWbECwlAwVOWrH0mszG_v9Ac0W39RtHyNa2w,8365
12
+ md2conf/emoticon.py,sha256=0g4rkx3d58xU4nnLak5ms7i0FSDnq0WJrLVFRgGyLC8,542
13
+ md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
14
+ md2conf/environment.py,sha256=TfNEz3Pyw9qe7f8i7e_kph16c09fhZ4cLNZZzIjmI18,3892
15
+ md2conf/extension.py,sha256=_IBf_yhYb6luQM3A-vAAtCpjHay33kE4Au_SGuC3kow,2274
16
+ md2conf/external.py,sha256=uY1G7bdqEMJW66vOvKsh5CS4oHY-YA7h2VVuaSdaqBo,2366
17
+ md2conf/formatting.py,sha256=ygL59VgpioX069axEX-7XjKs0sUjTfIZiBE5fWmITxc,4557
18
+ md2conf/frontmatter.py,sha256=3hUvrxvopI90UxJX7BLwLkjsC6LKLVr4mJ_ctB4r-po,2263
19
+ md2conf/image.py,sha256=E9sLj9xU41pKHBo_8saBcyOuJMwNPwVQFIMmW_7QTzA,5257
20
+ md2conf/latex.py,sha256=RukmO19gvmOFIfWTyBNth1r7FlnihvX56R1EWiaNRdU,2466
21
+ md2conf/local.py,sha256=eY3WpY-lNzLZeAfxX1ACVEhuzz0HDYX_sNQogJfkqcM,3673
22
+ md2conf/markdown.py,sha256=Okhzod811q5-AGn1dOdsYKeB8jBE0GXD2d4_k7Dk9y8,5939
23
+ md2conf/matcher.py,sha256=Xg4YSb87iPkCzhKuKytBut6NOkEab3IM-AjzXbwy64U,6774
24
+ md2conf/metadata.py,sha256=NOjbCIrwLgTIIeNgmo7w5JXuT-pxOXBGSg-irfdpokk,976
25
+ md2conf/options.py,sha256=G6J2l7JW__bIjpTJEQfiFkX3mdB9Q1XbJoN9xduzt3k,7993
26
+ md2conf/png.py,sha256=GU3-0dG6HqwGjedJVUciaIdA-6CdPTy_clsOQGr6dGE,6251
27
+ md2conf/processor.py,sha256=xVLpvKg2FEO0tWsHQ8sm7YpimQepbZ07W0_yUzcvl6c,11116
28
+ md2conf/publisher.py,sha256=n0YkMPw0YZQAUyj-PEW5RSes8YNxIEexFMSMtA7jrTY,14435
29
+ md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ md2conf/reflection.py,sha256=9b2X9BEisVHpZY5SWv89Z5ZucZ7bO1dIKmWGiooMS68,2638
31
+ md2conf/scanner.py,sha256=2OYBUxooIm8dJBzYMxB3YjdWam6QiNA6fFi5BocvFTo,4217
32
+ md2conf/serializer.py,sha256=fkSzSIPUMSOze4NrpYsqzfFrXQiRYROO9HrueQZoeSo,2144
33
+ md2conf/svg.py,sha256=dR4dnwbjoQq9iIM4nH9PQ4SUdTrZS0BKt0hMyO4C8qE,11607
34
+ md2conf/text.py,sha256=cnYV_JQp_v91LbQHo3qvxcEuhIdaPjCjkmLOKINcNv4,1736
35
+ md2conf/toc.py,sha256=g1mmfcK3c4qff-oQM9dr6s_6gF7WTMbNXoxsDAsiJBY,4614
36
+ md2conf/uri.py,sha256=my0deyR5SlppJrYCbXF1Zz94QA1JT-HTWe9pKw7AJ_A,1158
37
+ md2conf/xml.py,sha256=eR41FaqiketsCxTFBhwKEUW4g1ZQohvlh5J5WC3X-7Q,6690
38
+ md2conf/drawio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
+ md2conf/drawio/extension.py,sha256=HHLUriTfg82VCfOEyzU-6j2IM9rxR3I1UdSDdujWHgU,4409
40
+ md2conf/drawio/render.py,sha256=BgQRIScH_JpiVX7YTY543x7hQjThskqHf_w9YQ9Y5o0,8636
41
+ md2conf/mermaid/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
+ md2conf/mermaid/config.py,sha256=5Dec2QcdB_GtnuXIW6nhJK8J5caduNZU1oz1mcmmb44,376
43
+ md2conf/mermaid/extension.py,sha256=1drXVM_KbS00dcjSCRru0wwbil4zq3aR81dHMhfe7zA,4021
44
+ md2conf/mermaid/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
45
+ md2conf/mermaid/render.py,sha256=zO6M5UWSKiezoxPojD8iwFnwrFEDw_P6liQi-C3LQgw,1817
46
+ md2conf/mermaid/scanner.py,sha256=oIpaNxiZBNcmggnjlyYGcIVOXcYQWjf1lEVdyIwE4xE,1379
47
+ md2conf/plantuml/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
+ md2conf/plantuml/config.py,sha256=j0ONhkzmAPagh00ltamTKlVEvXa6R284We9pDxRy-5U,378
49
+ md2conf/plantuml/extension.py,sha256=TPwcQHw9s76GThVQVdgcPB0THmnDFPly2Zqei6pG63I,6285
50
+ md2conf/plantuml/render.py,sha256=Lf1It2KxHPKNGM1rhIDg9zdC3iqhRNCduByqa0_k_qw,3725
51
+ md2conf/plantuml/scanner.py,sha256=Oso6VbHVuMaPMKMazQc_bf4hhOT5WeJN5WiVPM8peyM,1347
52
+ markdown_to_confluence-0.5.5.dist-info/METADATA,sha256=zpy0fBKhzk-S2JcDLMZefDDTI5jrF1hTfIQkY7zyKg0,47514
53
+ markdown_to_confluence-0.5.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
54
+ markdown_to_confluence-0.5.5.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
55
+ markdown_to_confluence-0.5.5.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
56
+ markdown_to_confluence-0.5.5.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
57
+ markdown_to_confluence-0.5.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
md2conf/__init__.py CHANGED
@@ -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.5.3"
8
+ __version__ = "0.5.5"
9
9
  __author__ = "Levente Hunyadi"
10
10
  __copyright__ = "Copyright 2022-2026, Levente Hunyadi"
11
11
  __license__ = "MIT"
md2conf/__main__.py CHANGED
@@ -16,17 +16,23 @@ import sys
16
16
  import typing
17
17
  from io import StringIO
18
18
  from pathlib import Path
19
- from typing import Any, Iterable, Literal, Sequence
19
+ from types import TracebackType
20
+ from typing import Any, Iterable, Sequence
21
+
22
+ from requests.exceptions import HTTPError, JSONDecodeError
20
23
 
21
24
  from . import __version__
25
+ from .clio import add_arguments, get_options
22
26
  from .compatibility import override
23
27
  from .environment import ArgumentError, ConfluenceSiteProperties, ConnectionProperties
24
28
  from .metadata import ConfluenceSiteMetadata
25
- from .options import ConfluencePageID, ConverterOptions, DocumentOptions, ImageLayoutOptions, LayoutOptions
29
+ from .options import ConfluencePageID, ConverterOptions, DocumentOptions
30
+
31
+ LOGGER = logging.getLogger(__name__)
26
32
 
27
33
 
28
34
  class Arguments(argparse.Namespace):
29
- mdpath: Path
35
+ mdpath: list[Path]
30
36
  domain: str | None
31
37
  path: str | None
32
38
  api_url: str | None
@@ -34,25 +40,14 @@ class Arguments(argparse.Namespace):
34
40
  api_key: str | None
35
41
  space: str | None
36
42
  loglevel: str
37
- heading_anchors: bool
38
- ignore_invalid_url: bool
39
43
  root_page: str | None
40
44
  keep_hierarchy: bool
41
- skip_title_heading: bool
42
45
  title_prefix: str | None
43
46
  generated_by: str | None
44
- prefer_raster: bool
45
- render_drawio: bool
46
- render_mermaid: bool
47
- render_plantuml: bool
48
- render_latex: bool
49
- diagram_output_format: Literal["png", "svg"]
47
+ skip_update: bool
48
+ line_numbers: bool
50
49
  local: bool
51
50
  headers: dict[str, str]
52
- webui_links: bool
53
- alignment: Literal["center", "left", "right"]
54
- max_image_width: int | None
55
- use_panel: bool
56
51
 
57
52
 
58
53
  class KwargsAppendAction(argparse.Action):
@@ -100,7 +95,7 @@ def get_parser() -> argparse.ArgumentParser:
100
95
  parser = argparse.ArgumentParser(formatter_class=PositionalOnlyHelpFormatter)
101
96
  parser.prog = os.path.basename(os.path.dirname(__file__))
102
97
  parser.add_argument("--version", action="version", version=__version__)
103
- parser.add_argument("mdpath", help="Path to Markdown file or directory to convert and publish.")
98
+ parser.add_argument("mdpath", type=Path, nargs="+", help="Path to Markdown file or directory to convert and publish.")
104
99
  parser.add_argument("-d", "--domain", help="Confluence organization domain.")
105
100
  parser.add_argument("-p", "--path", help="Base path for Confluence (default: '/wiki/').")
106
101
  parser.add_argument(
@@ -123,16 +118,7 @@ def get_parser() -> argparse.ArgumentParser:
123
118
  parser.add_argument(
124
119
  "-l",
125
120
  "--loglevel",
126
- choices=[
127
- logging.getLevelName(level).lower()
128
- for level in (
129
- logging.DEBUG,
130
- logging.INFO,
131
- logging.WARN,
132
- logging.ERROR,
133
- logging.CRITICAL,
134
- )
135
- ],
121
+ choices=[logging.getLevelName(level).lower() for level in (logging.DEBUG, logging.INFO, logging.WARN, logging.ERROR, logging.CRITICAL)],
136
122
  default=logging.getLevelName(logging.INFO),
137
123
  help="Use this option to set the log verbosity.",
138
124
  )
@@ -148,16 +134,16 @@ def get_parser() -> argparse.ArgumentParser:
148
134
  help="Maintain source directory structure when exporting to Confluence.",
149
135
  )
150
136
  parser.add_argument(
151
- "--flatten-hierarchy",
137
+ "--skip-hierarchy",
152
138
  dest="keep_hierarchy",
153
139
  action="store_false",
154
- help="Flatten directories with no index.md or README.md when exporting to Confluence.",
140
+ help="Flatten directories with no `index.md` or `README.md` when exporting to Confluence.",
155
141
  )
156
142
  parser.add_argument(
157
143
  "--generated-by",
158
144
  default="This page has been generated with a tool.",
159
145
  metavar="MARKDOWN",
160
- help="Add prompt to pages (default: 'This page has been generated with a tool.').",
146
+ help="Add prompt to pages.",
161
147
  )
162
148
  parser.add_argument(
163
149
  "--no-generated-by",
@@ -167,107 +153,20 @@ def get_parser() -> argparse.ArgumentParser:
167
153
  help="Do not add 'generated by a tool' prompt to pages.",
168
154
  )
169
155
  parser.add_argument(
170
- "--render-drawio",
171
- dest="render_drawio",
172
- action="store_true",
173
- default=True,
174
- help="Render draw.io diagrams as image files. (Installed utility required to covert.)",
175
- )
176
- parser.add_argument(
177
- "--no-render-drawio",
178
- dest="render_drawio",
179
- action="store_false",
180
- help="Upload draw.io diagram sources as Confluence page attachments. (Marketplace app required to display.)",
181
- )
182
- parser.add_argument(
183
- "--render-mermaid",
184
- dest="render_mermaid",
185
- action="store_true",
186
- default=True,
187
- help="Render Mermaid diagrams as image files. (Installed utility required to convert.)",
188
- )
189
- parser.add_argument(
190
- "--no-render-mermaid",
191
- dest="render_mermaid",
192
- action="store_false",
193
- help="Upload Mermaid diagram sources as Confluence page attachments. (Marketplace app required to display.)",
194
- )
195
- parser.add_argument(
196
- "--render-plantuml",
197
- dest="render_plantuml",
198
- action="store_true",
199
- default=True,
200
- help="Render PlantUML diagrams as image files. (Installed utility required to convert.)",
201
- )
202
- parser.add_argument(
203
- "--no-render-plantuml",
204
- dest="render_plantuml",
205
- action="store_false",
206
- help="Upload PlantUML diagram sources as Confluence page attachments. (Marketplace app required to display.)",
207
- )
208
- parser.add_argument(
209
- "--render-latex",
210
- dest="render_latex",
211
- action="store_true",
212
- default=True,
213
- help="Render LaTeX formulas as image files. (Matplotlib required to convert.)",
214
- )
215
- parser.add_argument(
216
- "--no-render-latex",
217
- dest="render_latex",
218
- action="store_false",
219
- help="Inline LaTeX formulas in Confluence page. (Marketplace app required to display.)",
220
- )
221
- parser.add_argument(
222
- "--diagram-output-format",
223
- dest="diagram_output_format",
224
- choices=["png", "svg"],
225
- default="png",
226
- help="Format for rendering Mermaid and draw.io diagrams (default: 'png').",
227
- )
228
- parser.add_argument(
229
- "--prefer-raster",
230
- dest="prefer_raster",
231
- action="store_true",
232
- default=True,
233
- help="Prefer PNG over SVG when both exist (default: enabled).",
234
- )
235
- parser.add_argument(
236
- "--no-prefer-raster",
237
- dest="prefer_raster",
238
- action="store_false",
239
- help="Use SVG files directly instead of preferring PNG equivalents.",
240
- )
241
- parser.add_argument(
242
- "--heading-anchors",
156
+ "--skip-update",
243
157
  action="store_true",
244
158
  default=False,
245
- help="Place an anchor at each section heading with GitHub-style same-page identifiers.",
246
- )
247
- parser.add_argument(
248
- "--no-heading-anchors",
249
- action="store_false",
250
- dest="heading_anchors",
251
- help="Don't place an anchor at each section heading.",
252
- )
253
- parser.add_argument(
254
- "--ignore-invalid-url",
255
- action="store_true",
256
- default=False,
257
- help="Emit a warning but otherwise ignore relative URLs that point to ill-specified locations.",
258
- )
259
- parser.add_argument(
260
- "--skip-title-heading",
261
- action="store_true",
262
- default=False,
263
- help="Skip the first heading from document body when it is used as the page title (does not apply if title comes from front-matter).",
264
- )
265
- parser.add_argument(
266
- "--no-skip-title-heading",
267
- dest="skip_title_heading",
268
- action="store_false",
269
- help="Keep the first heading in document body even when used as page title (default).",
270
- )
159
+ help="Skip saving Confluence page ID in Markdown files.",
160
+ )
161
+ add_arguments(parser, ConverterOptions)
162
+ if sys.version_info >= (3, 13):
163
+ parser.add_argument(
164
+ "--ignore-invalid-url",
165
+ dest="force_valid_url",
166
+ action="store_false",
167
+ help="Emit a warning but otherwise ignore relative URLs that point to ill-specified locations.",
168
+ deprecated=True,
169
+ )
271
170
  parser.add_argument(
272
171
  "--title-prefix",
273
172
  default=None,
@@ -275,30 +174,11 @@ def get_parser() -> argparse.ArgumentParser:
275
174
  help="String to prepend to Confluence page title for each published page.",
276
175
  )
277
176
  parser.add_argument(
278
- "--webui-links",
279
- action="store_true",
280
- default=False,
281
- help="Enable Confluence Web UI links. (Typically required for on-prem versions of Confluence.)",
282
- )
283
- parser.add_argument(
284
- "--alignment",
285
- dest="alignment",
286
- choices=["center", "left", "right"],
287
- default="center",
288
- help="Alignment for block-level images and formulas (default: 'center').",
289
- )
290
- parser.add_argument(
291
- "--max-image-width",
292
- dest="max_image_width",
293
- type=int,
294
- default=None,
295
- help="Maximum display width for images [px]. Wider images are scaled down for page display. Original size kept for full-size viewing.",
296
- )
297
- parser.add_argument(
298
- "--use-panel",
177
+ "--line-numbers",
178
+ dest="line_numbers",
299
179
  action="store_true",
300
180
  default=False,
301
- help="Transform admonitions and alerts into a Confluence custom panel.",
181
+ help="Inject line numbers in Markdown source to help localize conversion errors.",
302
182
  )
303
183
  parser.add_argument(
304
184
  "--local",
@@ -324,13 +204,31 @@ def get_help() -> str:
324
204
  return buf.getvalue()
325
205
 
326
206
 
207
+ def _exception_hook(exc_type: type[BaseException], exc_value: BaseException, traceback: TracebackType | None) -> None:
208
+ LOGGER.exception("Exception raised: %s", exc_type.__name__, exc_info=exc_value)
209
+ ex: BaseException | None = exc_value
210
+ while ex is not None:
211
+ print(f"\033[95m{ex.__class__.__name__}\033[0m: {ex}")
212
+
213
+ if isinstance(ex, HTTPError):
214
+ # print details for a response with JSON body
215
+ if ex.response is not None:
216
+ try:
217
+ LOGGER.error(ex.response.json())
218
+ except JSONDecodeError:
219
+ pass
220
+
221
+ ex = ex.__cause__
222
+
223
+
224
+ sys.excepthook = _exception_hook # spellchecker:disable-line
225
+
226
+
327
227
  def main() -> None:
328
228
  parser = get_parser()
329
229
  args = Arguments()
330
230
  parser.parse_args(namespace=args)
331
231
 
332
- args.mdpath = Path(args.mdpath)
333
-
334
232
  logging.basicConfig(
335
233
  level=getattr(logging, args.loglevel.upper(), logging.INFO),
336
234
  format="%(asctime)s - %(levelname)s - %(funcName)s [%(lineno)d] - %(message)s",
@@ -341,25 +239,9 @@ def main() -> None:
341
239
  keep_hierarchy=args.keep_hierarchy,
342
240
  title_prefix=args.title_prefix,
343
241
  generated_by=args.generated_by,
344
- converter=ConverterOptions(
345
- heading_anchors=args.heading_anchors,
346
- ignore_invalid_url=args.ignore_invalid_url,
347
- skip_title_heading=args.skip_title_heading,
348
- prefer_raster=args.prefer_raster,
349
- render_drawio=args.render_drawio,
350
- render_mermaid=args.render_mermaid,
351
- render_plantuml=args.render_plantuml,
352
- render_latex=args.render_latex,
353
- diagram_output_format=args.diagram_output_format,
354
- webui_links=args.webui_links,
355
- use_panel=args.use_panel,
356
- layout=LayoutOptions(
357
- image=ImageLayoutOptions(
358
- alignment=args.alignment,
359
- max_width=args.max_image_width,
360
- ),
361
- ),
362
- ),
242
+ skip_update=args.skip_update,
243
+ converter=get_options(args, ConverterOptions),
244
+ line_numbers=args.line_numbers,
363
245
  )
364
246
  if args.local:
365
247
  from .local import LocalConverter
@@ -377,10 +259,10 @@ def main() -> None:
377
259
  base_path=site_properties.base_path,
378
260
  space_key=site_properties.space_key,
379
261
  )
380
- LocalConverter(options, site_metadata).process(args.mdpath)
262
+ converter = LocalConverter(options, site_metadata)
263
+ for item in args.mdpath:
264
+ converter.process(item)
381
265
  else:
382
- from requests import HTTPError, JSONDecodeError
383
-
384
266
  from .api import ConfluenceAPI
385
267
  from .publisher import Publisher
386
268
 
@@ -396,20 +278,10 @@ def main() -> None:
396
278
  )
397
279
  except ArgumentError as e:
398
280
  parser.error(str(e))
399
- try:
400
- with ConfluenceAPI(properties) as api:
401
- Publisher(api, options).process(args.mdpath)
402
- except HTTPError as err:
403
- logging.error(err)
404
-
405
- # print details for a response with JSON body
406
- if err.response is not None:
407
- try:
408
- logging.error(err.response.json())
409
- except JSONDecodeError:
410
- pass
411
-
412
- sys.exit(1)
281
+ with ConfluenceAPI(properties) as api:
282
+ publisher = Publisher(api, options)
283
+ for item in args.mdpath:
284
+ publisher.process(item)
413
285
 
414
286
 
415
287
  if __name__ == "__main__":
md2conf/api.py CHANGED
@@ -402,12 +402,7 @@ class ConfluenceAPI:
402
402
  )
403
403
  return self.session
404
404
 
405
- def __exit__(
406
- self,
407
- exc_type: type[BaseException] | None,
408
- exc_val: BaseException | None,
409
- exc_tb: TracebackType | None,
410
- ) -> None:
405
+ def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> None:
411
406
  """
412
407
  Closes an open connection.
413
408
  """
@@ -429,15 +424,7 @@ class ConfluenceSession:
429
424
  _space_id_to_key: dict[str, str]
430
425
  _space_key_to_id: dict[str, str]
431
426
 
432
- def __init__(
433
- self,
434
- session: requests.Session,
435
- *,
436
- api_url: str | None,
437
- domain: str | None,
438
- base_path: str | None,
439
- space_key: str | None,
440
- ) -> None:
427
+ def __init__(self, session: requests.Session, *, api_url: str | None, domain: str | None, base_path: str | None, space_key: str | None) -> None:
441
428
  self.session = session
442
429
  self._space_id_to_key = {}
443
430
  self._space_key_to_id = {}
@@ -488,12 +475,7 @@ class ConfluenceSession:
488
475
  self.session.close()
489
476
  self.session = requests.Session()
490
477
 
491
- def _build_url(
492
- self,
493
- version: ConfluenceVersion,
494
- path: str,
495
- query: dict[str, str] | None = None,
496
- ) -> str:
478
+ def _build_url(self, version: ConfluenceVersion, path: str, query: dict[str, str] | None = None) -> str:
497
479
  """
498
480
  Builds a full URL for invoking the Confluence API.
499
481
 
@@ -506,14 +488,7 @@ class ConfluenceSession:
506
488
  base_url = f"{self.api_url}{version.value}{path}"
507
489
  return build_url(base_url, query)
508
490
 
509
- def _get(
510
- self,
511
- version: ConfluenceVersion,
512
- path: str,
513
- response_type: type[T],
514
- *,
515
- query: dict[str, str] | None = None,
516
- ) -> T:
491
+ def _get(self, version: ConfluenceVersion, path: str, response_type: type[T], *, query: dict[str, str] | None = None) -> T:
517
492
  "Executes an HTTP request via Confluence API."
518
493
 
519
494
  url = self._build_url(version, path, query)
@@ -829,13 +804,7 @@ class ConfluenceSession:
829
804
  LOGGER.info("Updating attachment: %s", attachment_id)
830
805
  self._put(ConfluenceVersion.VERSION_1, path, request, None)
831
806
 
832
- def get_page_properties_by_title(
833
- self,
834
- title: str,
835
- *,
836
- space_id: str | None = None,
837
- space_key: str | None = None,
838
- ) -> ConfluencePageProperties:
807
+ def get_page_properties_by_title(self, title: str, *, space_id: str | None = None, space_key: str | None = None) -> ConfluencePageProperties:
839
808
  """
840
809
  Looks up a Confluence wiki page ID by title.
841
810
 
@@ -890,10 +859,10 @@ class ConfluenceSession:
890
859
  else:
891
860
  raise
892
861
 
893
- # This should not be reached, but satisfies type checker
862
+ # this should not be reached, but satisfies type checker
894
863
  if last_error is not None:
895
864
  raise last_error
896
- raise ConfluenceError(f"Failed to get page {page_id}")
865
+ raise ConfluenceError(f"failed to get page: {page_id}")
897
866
 
898
867
  def get_page_properties(self, page_id: str) -> ConfluencePageProperties:
899
868
  """
@@ -916,14 +885,7 @@ class ConfluenceSession:
916
885
 
917
886
  return self.get_page_properties(page_id).version.number
918
887
 
919
- def update_page(
920
- self,
921
- page_id: str,
922
- content: str,
923
- *,
924
- title: str,
925
- version: int,
926
- ) -> None:
888
+ def update_page(self, page_id: str, content: str, *, title: str, version: int, message: str) -> None:
927
889
  """
928
890
  Updates a page via the Confluence API.
929
891
 
@@ -939,35 +901,28 @@ class ConfluenceSession:
939
901
  status=ConfluenceStatus.CURRENT,
940
902
  title=title,
941
903
  body=ConfluencePageBody(storage=ConfluencePageStorage(representation=ConfluenceRepresentation.STORAGE, value=content)),
942
- version=ConfluenceContentVersion(number=version, minorEdit=True),
904
+ version=ConfluenceContentVersion(number=version, minorEdit=True, message=message),
943
905
  )
944
906
  LOGGER.info("Updating page: %s", page_id)
945
907
  self._put(ConfluenceVersion.VERSION_2, path, request, None)
946
908
 
947
- def create_page(
948
- self,
949
- parent_id: str,
950
- title: str,
951
- new_content: str,
952
- ) -> ConfluencePage:
909
+ def create_page(self, *, title: str, content: str, parent_id: str, space_id: str) -> ConfluencePage:
953
910
  """
954
911
  Creates a new page via Confluence API.
955
912
  """
956
913
 
957
914
  LOGGER.info("Creating page: %s", title)
958
915
 
959
- parent_page = self.get_page_properties(parent_id)
960
-
961
916
  path = "/pages/"
962
917
  request = ConfluenceCreatePageRequest(
963
- spaceId=parent_page.spaceId,
918
+ spaceId=space_id,
964
919
  status=ConfluenceStatus.CURRENT,
965
920
  title=title,
966
921
  parentId=parent_id,
967
922
  body=ConfluencePageBody(
968
923
  storage=ConfluencePageStorage(
969
924
  representation=ConfluenceRepresentation.STORAGE,
970
- value=new_content,
925
+ value=content,
971
926
  )
972
927
  ),
973
928
  )
@@ -1009,23 +964,15 @@ class ConfluenceSession:
1009
964
  response = self.session.delete(url, verify=True)
1010
965
  response.raise_for_status()
1011
966
 
1012
- def page_exists(
1013
- self,
1014
- title: str,
1015
- *,
1016
- space_id: str | None = None,
1017
- space_key: str | None = None,
1018
- ) -> str | None:
967
+ def page_exists(self, title: str, *, space_id: str | None = None) -> str | None:
1019
968
  """
1020
969
  Checks if a Confluence page exists with the given title.
1021
970
 
1022
971
  :param title: Page title. Pages in the same Confluence space must have a unique title.
1023
- :param space_key: Identifies the Confluence space.
1024
-
972
+ :param space_id: Identifies the Confluence space.
1025
973
  :returns: Confluence page ID of a matching page (if found), or `None`.
1026
974
  """
1027
975
 
1028
- space_id = self._get_space_id(space_id=space_id, space_key=space_key)
1029
976
  path = "/pages"
1030
977
  query = {"title": title}
1031
978
  if space_id is not None:
@@ -1058,17 +1005,19 @@ class ConfluenceSession:
1058
1005
 
1059
1006
  :param title: Page title. Pages in the same Confluence space must have a unique title.
1060
1007
  :param parent_id: Identifies the parent page for a new child page.
1008
+ :returns: Confluence page info for the found or newly created page.
1061
1009
  """
1062
1010
 
1063
1011
  parent_page = self.get_page_properties(parent_id)
1064
- page_id = self.page_exists(title, space_id=parent_page.spaceId)
1012
+ space_id = parent_page.spaceId
1013
+ page_id = self.page_exists(title, space_id=space_id)
1065
1014
 
1066
1015
  if page_id is not None:
1067
1016
  LOGGER.debug("Retrieving existing page: %s", page_id)
1068
1017
  return self.get_page(page_id)
1069
1018
  else:
1070
1019
  LOGGER.debug("Creating new page with title: %s", title)
1071
- return self.create_page(parent_id, title, "")
1020
+ return self.create_page(title=title, content="", parent_id=parent_id, space_id=space_id)
1072
1021
 
1073
1022
  def get_labels(self, page_id: str) -> list[ConfluenceIdentifiedLabel]:
1074
1023
  """
@@ -1132,6 +1081,23 @@ class ConfluenceSession:
1132
1081
  remove_labels.sort()
1133
1082
  self.remove_labels(page_id, remove_labels)
1134
1083
 
1084
+ def get_content_property_for_page(self, page_id: str, key: str) -> ConfluenceIdentifiedContentProperty | None:
1085
+ """
1086
+ Retrieves a content property for a Confluence page.
1087
+
1088
+ :param page_id: The Confluence page ID.
1089
+ :param key: The name of the property to fetch (with case-sensitive match).
1090
+ :returns: The content property value, or `None` if not found.
1091
+ """
1092
+
1093
+ path = f"/pages/{page_id}/properties"
1094
+ results = self._fetch(path, query={"key": key})
1095
+ properties = json_to_object(list[ConfluenceIdentifiedContentProperty], results)
1096
+ if len(properties) == 1:
1097
+ return properties.pop()
1098
+ else:
1099
+ return None
1100
+
1135
1101
  def get_content_properties_for_page(self, page_id: str) -> list[ConfluenceIdentifiedContentProperty]:
1136
1102
  """
1137
1103
  Retrieves content properties for a Confluence page.
md2conf/attachment.py CHANGED
@@ -40,6 +40,9 @@ class AttachmentCatalog:
40
40
  self.embedded_files[filename] = data
41
41
 
42
42
 
43
+ _DISALLOWED_CHAR_REGEXP = re.compile(r"[^\-0-9A-Za-z_.]", re.UNICODE)
44
+
45
+
43
46
  def attachment_name(ref: Path | str) -> str:
44
47
  """
45
48
  Safe name for use with attachment uploads.
@@ -60,13 +63,11 @@ def attachment_name(ref: Path | str) -> str:
60
63
  if path.drive or path.root:
61
64
  raise ValueError(f"required: relative path; got: {ref}")
62
65
 
63
- regexp = re.compile(r"[^\-0-9A-Za-z_.]", re.UNICODE)
64
-
65
66
  def replace_part(part: str) -> str:
66
67
  if part == "..":
67
68
  return "PAR"
68
69
  else:
69
- return regexp.sub("_", part)
70
+ return _DISALLOWED_CHAR_REGEXP.sub("_", part)
70
71
 
71
72
  parts = [replace_part(p) for p in path.parts]
72
73
  return Path(*parts).as_posix().replace("/", "_")