markdown-to-confluence 0.5.1__py3-none-any.whl → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markdown-to-confluence
3
- Version: 0.5.1
3
+ Version: 0.5.2
4
4
  Summary: Publish Markdown files to Confluence wiki
5
5
  Author-email: Levente Hunyadi <hunyadi@gmail.com>
6
6
  Maintainer-email: Levente Hunyadi <hunyadi@gmail.com>
@@ -27,18 +27,18 @@ Requires-Dist: cattrs>=25.3
27
27
  Requires-Dist: lxml>=6.0
28
28
  Requires-Dist: markdown>=3.10
29
29
  Requires-Dist: orjson>=3.11
30
- Requires-Dist: pymdown-extensions>=10.17
30
+ Requires-Dist: pymdown-extensions>=10.19
31
31
  Requires-Dist: PyYAML>=6.0
32
32
  Requires-Dist: requests>=2.32
33
33
  Requires-Dist: truststore>=0.10
34
34
  Requires-Dist: typing-extensions>=4.15; python_version < "3.12"
35
35
  Provides-Extra: dev
36
- Requires-Dist: markdown_doc>=0.1.5; extra == "dev"
37
- Requires-Dist: types-lxml>=2025.8.25; extra == "dev"
36
+ Requires-Dist: markdown_doc>=0.1.6; extra == "dev"
37
+ Requires-Dist: types-lxml>=2025.11.25; extra == "dev"
38
38
  Requires-Dist: types-markdown>=3.10; extra == "dev"
39
39
  Requires-Dist: types-PyYAML>=6.0; extra == "dev"
40
40
  Requires-Dist: types-requests>=2.32; extra == "dev"
41
- Requires-Dist: mypy>=1.18; extra == "dev"
41
+ Requires-Dist: mypy>=1.19; extra == "dev"
42
42
  Requires-Dist: ruff>=0.14; extra == "dev"
43
43
  Provides-Extra: formulas
44
44
  Requires-Dist: matplotlib>=3.9; extra == "formulas"
@@ -215,6 +215,19 @@ Provide generated-by prompt text in the Markdown file with a tag:
215
215
 
216
216
  Alternatively, use the `--generated-by GENERATED_BY` option. The tag takes precedence.
217
217
 
218
+ The generated-by text can also be templated with the following variables:
219
+
220
+ - `%{filename}`: the name of the Markdown file
221
+ - `%{filepath}`: the path of the Markdown file relative to the *source root*
222
+
223
+ When publishing a directory hierarchy, the *source root* is the directory in which *md2conf* is launched. When publishing a single file, this is the directory in which the Markdown file resides.
224
+
225
+ It can be used with the CLI `--generated-by` option or directly in the files:
226
+
227
+ ```markdown
228
+ <!-- generated-by: Do not edit! Check out the file %{filepath} in the repo -->
229
+ ```
230
+
218
231
  ### Publishing a single page
219
232
 
220
233
  *md2conf* has two modes of operation: *single-page mode* and *directory mode*.
@@ -455,6 +468,58 @@ This is useful if you have a page in a hierarchy that participates in parent-chi
455
468
 
456
469
  If the `title` attribute (in the front-matter) or the topmost unique heading (in the document body) changes, the Confluence page title is updated. A warning is raised if the new title conflicts with the title of another page, and thus cannot be updated.
457
470
 
471
+ #### Avoiding duplicate titles
472
+
473
+ By default, when *md2conf* extracts a page title from the first unique heading in a Markdown document, the heading remains in the document body. This means the title appears twice on the Confluence page: once as the page title at the top, and once as the first heading in the content.
474
+
475
+ To avoid this duplication, use the `--skip-title-heading` option. When enabled, *md2conf* removes the first heading from the document body if it was used as the page title. This option only takes effect when:
476
+
477
+ 1. The title was extracted from the document's first unique heading (not from front-matter), AND
478
+ 2. There is exactly one top-level heading in the document.
479
+
480
+ If the title comes from the `title` attribute in front-matter, the heading is preserved in the document body regardless of this setting, as the heading and title are considered separate.
481
+
482
+ **Example without `--skip-title-heading` (default):**
483
+
484
+ Markdown:
485
+ ```markdown
486
+ # Installation Guide
487
+
488
+ Follow these steps...
489
+ ```
490
+
491
+ Confluence displays:
492
+ - Page title: "Installation Guide"
493
+ - Content: Starts with heading "Installation Guide", followed by "Follow these steps..."
494
+
495
+ **Example with `--skip-title-heading`:**
496
+
497
+ Same Markdown source, but Confluence displays:
498
+ - Page title: "Installation Guide"
499
+ - Content: Starts directly with "Follow these steps..." (heading removed)
500
+
501
+ **Edge case: Abstract or introductory text before the title:**
502
+
503
+ When a document has content before the first heading (like an abstract), removing the heading eliminates the visual separator between the introductory text and the main content:
504
+
505
+ ```markdown
506
+ This is an abstract paragraph providing context.
507
+
508
+ # Document Title
509
+
510
+ This is the main document content.
511
+ ```
512
+
513
+ With `--skip-title-heading`, the output becomes:
514
+ - Page title: "Document Title"
515
+ - Content: "This is an abstract paragraph..." flows directly into "This is the main document content..." (no heading separator)
516
+
517
+ While the structure remains semantically correct, the visual separation is lost. If you need to maintain separation, consider these workarounds:
518
+
519
+ 1. **Use a horizontal rule:** Add `---` after the abstract to create visual separation
520
+ 2. **Use an admonition block:** Wrap the abstract in an info/note block
521
+ 3. **Use front-matter title:** Set `title` in front-matter to keep the heading in the body
522
+
458
523
  ### Labels
459
524
 
460
525
  If a Markdown document has the front-matter attribute `tags`, *md2conf* assigns the specified tags to the Confluence page as labels.
@@ -571,7 +636,7 @@ options:
571
636
  -r ROOT_PAGE Root Confluence page to create new pages. If omitted, will raise exception when creating new pages.
572
637
  --keep-hierarchy Maintain source directory structure when exporting to Confluence.
573
638
  --flatten-hierarchy Flatten directories with no index.md or README.md when exporting to Confluence.
574
- --generated-by GENERATED_BY
639
+ --generated-by MARKDOWN
575
640
  Add prompt to pages (default: 'This page has been generated with a tool.').
576
641
  --no-generated-by Do not add 'generated by a tool' prompt to pages.
577
642
  --render-drawio Render draw.io diagrams as image files. (Installed utility required to covert.)
@@ -582,16 +647,24 @@ options:
582
647
  --no-render-latex Inline LaTeX formulas in Confluence page. (Marketplace app required to display.)
583
648
  --diagram-output-format {png,svg}
584
649
  Format for rendering Mermaid and draw.io diagrams (default: 'png').
650
+ --prefer-raster Prefer PNG over SVG when both exist (default: enabled).
651
+ --no-prefer-raster Use SVG files directly instead of preferring PNG equivalents.
585
652
  --heading-anchors Place an anchor at each section heading with GitHub-style same-page identifiers.
586
653
  --no-heading-anchors Don't place an anchor at each section heading.
587
654
  --ignore-invalid-url Emit a warning but otherwise ignore relative URLs that point to ill-specified locations.
588
- --local Write XHTML-based Confluence Storage Format files locally without invoking Confluence API.
589
- --headers KEY=VALUE [KEY=VALUE ...]
590
- Apply custom headers to all Confluence API requests.
655
+ --skip-title-heading Skip the first heading from document body when it is used as the page title (does not apply if title comes from front-matter).
656
+ --no-skip-title-heading
657
+ Keep the first heading in document body even when used as page title (default).
658
+ --title-prefix TEXT String to prepend to Confluence page title for each published page.
591
659
  --webui-links Enable Confluence Web UI links. (Typically required for on-prem versions of Confluence.)
592
660
  --alignment {center,left,right}
593
661
  Alignment for block-level images and formulas (default: 'center').
662
+ --max-image-width MAX_IMAGE_WIDTH
663
+ Maximum display width for images [px]. Wider images are scaled down for page display. Original size kept for full-size viewing.
594
664
  --use-panel Transform admonitions and alerts into a Confluence custom panel.
665
+ --local Write XHTML-based Confluence Storage Format files locally without invoking Confluence API.
666
+ --headers KEY=VALUE [KEY=VALUE ...]
667
+ Apply custom headers to all Confluence API requests.
595
668
  ```
596
669
 
597
670
  ### Confluence REST API v1 vs. v2
@@ -1,35 +1,36 @@
1
- markdown_to_confluence-0.5.1.dist-info/licenses/LICENSE,sha256=56L-Y0dyZwyVlINRJRz3PNw-ka-oLVaAq-7d8zo6qlc,1077
2
- md2conf/__init__.py,sha256=BhdWezYDER-ShxuHElVX_sLnP_NkQ7WoO5tr318SwgE,402
3
- md2conf/__main__.py,sha256=ZAwZ2YqKUxKiVx8CQsrnso9z2deP5Xn80kqqf2o3AbY,11472
4
- md2conf/api.py,sha256=yFDsE_5IpCXG4z24ZrxF8QjF07ep3HiBHqaWKcGKf1k,40731
1
+ markdown_to_confluence-0.5.2.dist-info/licenses/LICENSE,sha256=56L-Y0dyZwyVlINRJRz3PNw-ka-oLVaAq-7d8zo6qlc,1077
2
+ md2conf/__init__.py,sha256=TZU4q64xgFKmFudp0-NIfMbAgcFihMCih-5sjAybGKs,402
3
+ md2conf/__main__.py,sha256=GdYd7v6YpIbOcvaUNh_5TZXwnEGbaxlsIS0sJtMmyhE,13152
4
+ md2conf/api.py,sha256=v4QXiNFGHyIhYZWb36uG-AR4HBPoj2GZI939ao9SOIQ,41989
5
5
  md2conf/collection.py,sha256=nghFS5kK4kPbpLE7IHi4rprJK-Mu4KXNxjHYM9Rc5SQ,824
6
- md2conf/converter.py,sha256=BM94de0CAQGXOTSve7_042y3VF_yu77NITX5FUUeJPQ,69446
6
+ md2conf/converter.py,sha256=S6JiC-v5jqEVv4lcvCKxfLO-dZpHNLEfuocFBslIpaA,79924
7
7
  md2conf/csf.py,sha256=rugs3qC2aJQCJSTczeBw9WhqSZZtMq14LjwT0V1b6Hc,6476
8
- md2conf/domain.py,sha256=EsaAfUaT2qIrK91uRyxaPEY4kSq-nzhccErxVqHdooc,2205
8
+ md2conf/domain.py,sha256=opq_O_NOz097HC4Q8VA7aNba6Snq09euTCi-Ag-bvAo,2685
9
9
  md2conf/drawio.py,sha256=IqFlAegrKM5SQf5CqHD8STIzskH7Rpm9RtWwn_nXVTc,8581
10
10
  md2conf/emoticon.py,sha256=P2L5oQvnRXeVifJQ3sJ2Ck-6ptbxumq2vsT-rM0W0Ms,484
11
11
  md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
12
12
  md2conf/environment.py,sha256=BhI7YktY7G26HOhGlUvTkH2Vmfa4E_dhu2snzbBgMvE,3902
13
13
  md2conf/extra.py,sha256=VuMxuOnnC2Qwy6y52ukIxsaYhrZArRqMmRHRE4QZl8g,687
14
- md2conf/latex.py,sha256=3eFgsvaq6ROAc2skW1Wq21CX_pJai1Yc9t861Z3s5XA,7600
14
+ md2conf/latex.py,sha256=vZJakhwiSPkprz5IkJZOUw9H4FVXj_kksgMKoF8N_pI,7747
15
15
  md2conf/local.py,sha256=Ou-j7kZWbHxC8Si8Yg7myqtTQ1He6mYQW1NpX3LLIcY,3704
16
16
  md2conf/markdown.py,sha256=t-z19Zs_91_jzRvwmOsWqCDt0Tdghmk5bpNUON0YlKc,3148
17
17
  md2conf/matcher.py,sha256=hkM49osFM9nrXRXe4pwcGCg0rrLsmKep7AYY_S01kNY,6774
18
18
  md2conf/mermaid.py,sha256=9P4VV69dooaFBNUjdTIpzq7BFA8rDMqEif1O7XKWPdM,2617
19
19
  md2conf/metadata.py,sha256=_kt_lh4gCzVRRhhrDk-cJCk9WMcX9ZDWB6hL0Lw3xoI,976
20
20
  md2conf/processor.py,sha256=8Y-NSxAuqSHMSN9vhw_83HisGAmq87XAY98dis_xZ0Y,9690
21
- md2conf/publisher.py,sha256=yI7gObPZLrNEXbiPKBJwkBPcGLI17UwzKd8FQe3U8bE,8634
21
+ md2conf/publisher.py,sha256=ma0E_Kcxd86Qe_ywdhiTydkoJFRp6MI_Hy2ImFMKWsA,8752
22
22
  md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
23
23
  md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- md2conf/scanner.py,sha256=o46fTQXuTpbtvpnQPW3CW4ydIb5bM362K2TFpwO51P0,6782
24
+ md2conf/scanner.py,sha256=xxzNm3IRnlS0yAkStTHCigeLd6hUyvMYIBKFpTBEPf4,6776
25
25
  md2conf/serializer.py,sha256=JrBj8Z9zP8PjBeVAlRRqnMKDoz6IvkRbTd6K-JgFVow,1757
26
+ md2conf/svg.py,sha256=LImoEKerVdXGkTnR6SogKvsR7WJNDVvMN1Ju_usbrNs,10306
26
27
  md2conf/text.py,sha256=fHOrUaPXAjE4iRhHqFq-CiI-knpo4wvyHCWp0crewqA,1736
27
28
  md2conf/toc.py,sha256=ZrfUfTv_Jiv27G4SBNjK3b-1ClYKoqN5yPRsEWp6IXk,2413
28
29
  md2conf/uri.py,sha256=KbLBdRFtZTQTZd8b4j0LtE8Pb68Ly0WkemF4iW-EAB4,1158
29
30
  md2conf/xml.py,sha256=Fu00Eg8c0VgMHIjRDBJBSNWtui8obEtowkiR7gHTduM,5526
30
- markdown_to_confluence-0.5.1.dist-info/METADATA,sha256=Qttp8PjzAJnE6mJ_2-1ABYAVMCjEQeuL7QlaOcTmiEY,36463
31
- markdown_to_confluence-0.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
- markdown_to_confluence-0.5.1.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
33
- markdown_to_confluence-0.5.1.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
34
- markdown_to_confluence-0.5.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
35
- markdown_to_confluence-0.5.1.dist-info/RECORD,,
31
+ markdown_to_confluence-0.5.2.dist-info/METADATA,sha256=fts0cqy6A_o_QztqMmfVGWoY1kLEDftLNPKGhPsoMQo,40071
32
+ markdown_to_confluence-0.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
+ markdown_to_confluence-0.5.2.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
34
+ markdown_to_confluence-0.5.2.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
35
+ markdown_to_confluence-0.5.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
36
+ markdown_to_confluence-0.5.2.dist-info/RECORD,,
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.1"
8
+ __version__ = "0.5.2"
9
9
  __author__ = "Levente Hunyadi"
10
10
  __copyright__ = "Copyright 2022-2025, Levente Hunyadi"
11
11
  __license__ = "MIT"
md2conf/__main__.py CHANGED
@@ -34,11 +34,14 @@ class Arguments(argparse.Namespace):
34
34
  api_key: str | None
35
35
  space: str | None
36
36
  loglevel: str
37
- ignore_invalid_url: bool
38
37
  heading_anchors: bool
38
+ ignore_invalid_url: bool
39
39
  root_page: str | None
40
40
  keep_hierarchy: bool
41
+ skip_title_heading: bool
42
+ title_prefix: str | None
41
43
  generated_by: str | None
44
+ prefer_raster: bool
42
45
  render_drawio: bool
43
46
  render_mermaid: bool
44
47
  render_latex: bool
@@ -47,6 +50,7 @@ class Arguments(argparse.Namespace):
47
50
  headers: dict[str, str]
48
51
  webui_links: bool
49
52
  alignment: Literal["center", "left", "right"]
53
+ max_image_width: int | None
50
54
  use_panel: bool
51
55
 
52
56
 
@@ -151,6 +155,7 @@ def get_parser() -> argparse.ArgumentParser:
151
155
  parser.add_argument(
152
156
  "--generated-by",
153
157
  default="This page has been generated with a tool.",
158
+ metavar="MARKDOWN",
154
159
  help="Add prompt to pages (default: 'This page has been generated with a tool.').",
155
160
  )
156
161
  parser.add_argument(
@@ -206,6 +211,19 @@ def get_parser() -> argparse.ArgumentParser:
206
211
  default="png",
207
212
  help="Format for rendering Mermaid and draw.io diagrams (default: 'png').",
208
213
  )
214
+ parser.add_argument(
215
+ "--prefer-raster",
216
+ dest="prefer_raster",
217
+ action="store_true",
218
+ default=True,
219
+ help="Prefer PNG over SVG when both exist (default: enabled).",
220
+ )
221
+ parser.add_argument(
222
+ "--no-prefer-raster",
223
+ dest="prefer_raster",
224
+ action="store_false",
225
+ help="Use SVG files directly instead of preferring PNG equivalents.",
226
+ )
209
227
  parser.add_argument(
210
228
  "--heading-anchors",
211
229
  action="store_true",
@@ -225,18 +243,22 @@ def get_parser() -> argparse.ArgumentParser:
225
243
  help="Emit a warning but otherwise ignore relative URLs that point to ill-specified locations.",
226
244
  )
227
245
  parser.add_argument(
228
- "--local",
246
+ "--skip-title-heading",
229
247
  action="store_true",
230
248
  default=False,
231
- help="Write XHTML-based Confluence Storage Format files locally without invoking Confluence API.",
249
+ 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).",
232
250
  )
233
251
  parser.add_argument(
234
- "--headers",
235
- nargs="+",
236
- required=False,
237
- action=KwargsAppendAction,
238
- metavar="KEY=VALUE",
239
- help="Apply custom headers to all Confluence API requests.",
252
+ "--no-skip-title-heading",
253
+ dest="skip_title_heading",
254
+ action="store_false",
255
+ help="Keep the first heading in document body even when used as page title (default).",
256
+ )
257
+ parser.add_argument(
258
+ "--title-prefix",
259
+ default=None,
260
+ metavar="TEXT",
261
+ help="String to prepend to Confluence page title for each published page.",
240
262
  )
241
263
  parser.add_argument(
242
264
  "--webui-links",
@@ -251,12 +273,33 @@ def get_parser() -> argparse.ArgumentParser:
251
273
  default="center",
252
274
  help="Alignment for block-level images and formulas (default: 'center').",
253
275
  )
276
+ parser.add_argument(
277
+ "--max-image-width",
278
+ dest="max_image_width",
279
+ type=int,
280
+ default=None,
281
+ help="Maximum display width for images [px]. Wider images are scaled down for page display. Original size kept for full-size viewing.",
282
+ )
254
283
  parser.add_argument(
255
284
  "--use-panel",
256
285
  action="store_true",
257
286
  default=False,
258
287
  help="Transform admonitions and alerts into a Confluence custom panel.",
259
288
  )
289
+ parser.add_argument(
290
+ "--local",
291
+ action="store_true",
292
+ default=False,
293
+ help="Write XHTML-based Confluence Storage Format files locally without invoking Confluence API.",
294
+ )
295
+ parser.add_argument(
296
+ "--headers",
297
+ nargs="+",
298
+ required=False,
299
+ action=KwargsAppendAction,
300
+ metavar="KEY=VALUE",
301
+ help="Apply custom headers to all Confluence API requests.",
302
+ )
260
303
  return parser
261
304
 
262
305
 
@@ -282,15 +325,19 @@ def main() -> None:
282
325
  options = ConfluenceDocumentOptions(
283
326
  heading_anchors=args.heading_anchors,
284
327
  ignore_invalid_url=args.ignore_invalid_url,
328
+ skip_title_heading=args.skip_title_heading,
329
+ title_prefix=args.title_prefix,
285
330
  generated_by=args.generated_by,
286
331
  root_page_id=ConfluencePageID(args.root_page) if args.root_page else None,
287
332
  keep_hierarchy=args.keep_hierarchy,
333
+ prefer_raster=args.prefer_raster,
288
334
  render_drawio=args.render_drawio,
289
335
  render_mermaid=args.render_mermaid,
290
336
  render_latex=args.render_latex,
291
337
  diagram_output_format=args.diagram_output_format,
292
338
  webui_links=args.webui_links,
293
339
  alignment=args.alignment,
340
+ max_image_width=args.max_image_width,
294
341
  use_panel=args.use_panel,
295
342
  )
296
343
  if args.local:
md2conf/api.py CHANGED
@@ -11,7 +11,9 @@ import enum
11
11
  import io
12
12
  import logging
13
13
  import mimetypes
14
+ import random
14
15
  import ssl
16
+ import time
15
17
  import typing
16
18
  from dataclasses import dataclass
17
19
  from pathlib import Path
@@ -30,6 +32,7 @@ from .serializer import JsonType, json_to_object, object_to_json_payload
30
32
 
31
33
  T = TypeVar("T")
32
34
 
35
+ # spellchecker: disable
33
36
  mimetypes.add_type("application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx", strict=True)
34
37
  mimetypes.add_type("text/vnd.mermaid", ".mmd", strict=True)
35
38
  mimetypes.add_type("application/vnd.oasis.opendocument.presentation", ".odp", strict=True)
@@ -37,6 +40,7 @@ mimetypes.add_type("application/vnd.oasis.opendocument.spreadsheet", ".ods", str
37
40
  mimetypes.add_type("application/vnd.oasis.opendocument.text", ".odt", strict=True)
38
41
  mimetypes.add_type("application/vnd.openxmlformats-officedocument.presentationml.presentation", ".pptx", strict=True)
39
42
  mimetypes.add_type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx", strict=True)
43
+ # spellchecker: enable
40
44
 
41
45
 
42
46
  def build_url(base_url: str, query: dict[str, str] | None = None) -> str:
@@ -839,16 +843,38 @@ class ConfluenceSession:
839
843
  page = json_to_object(ConfluencePageProperties, results[0])
840
844
  return page
841
845
 
842
- def get_page(self, page_id: str) -> ConfluencePage:
846
+ def get_page(self, page_id: str, *, retries: int = 3, retry_delay: float = 1.0) -> ConfluencePage:
843
847
  """
844
848
  Retrieves Confluence wiki page details and content.
845
849
 
850
+ Includes retry logic to handle eventual consistency issues when fetching
851
+ a newly created page that may not be immediately available via the API.
852
+
846
853
  :param page_id: The Confluence page ID.
854
+ :param retries: Number of retry attempts for 404 errors (default: 3).
855
+ :param retry_delay: Initial delay in seconds between retries, doubles each attempt (default: 1.0).
847
856
  :returns: Confluence page info and content.
848
857
  """
849
858
 
850
859
  path = f"/pages/{page_id}"
851
- return self._get(ConfluenceVersion.VERSION_2, path, ConfluencePage, query={"body-format": "storage"})
860
+ last_error: requests.HTTPError | None = None
861
+
862
+ for attempt in range(retries + 1):
863
+ try:
864
+ return self._get(ConfluenceVersion.VERSION_2, path, ConfluencePage, query={"body-format": "storage"})
865
+ except requests.HTTPError as e:
866
+ if e.response is not None and e.response.status_code == 404 and attempt < retries:
867
+ delay = retry_delay * (2**attempt) + random.uniform(0, 1)
868
+ LOGGER.debug("Page %s not found, retrying in %.1f seconds (attempt %d/%d)", page_id, delay, attempt + 1, retries)
869
+ time.sleep(delay)
870
+ last_error = e
871
+ else:
872
+ raise
873
+
874
+ # This should not be reached, but satisfies type checker
875
+ if last_error is not None:
876
+ raise last_error
877
+ raise ConfluenceError(f"Failed to get page {page_id}")
852
878
 
853
879
  def get_page_properties(self, page_id: str) -> ConfluencePageProperties:
854
880
  """