markdown-to-confluence 0.5.2__py3-none-any.whl → 0.5.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.
- {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/METADATA +258 -157
- markdown_to_confluence-0.5.4.dist-info/RECORD +55 -0
- {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/licenses/LICENSE +1 -1
- md2conf/__init__.py +2 -2
- md2conf/__main__.py +83 -44
- md2conf/api.py +30 -10
- md2conf/attachment.py +72 -0
- md2conf/coalesce.py +43 -0
- md2conf/collection.py +1 -1
- md2conf/{extra.py → compatibility.py} +1 -1
- md2conf/converter.py +240 -657
- md2conf/csf.py +13 -11
- md2conf/drawio/__init__.py +0 -0
- md2conf/drawio/extension.py +116 -0
- md2conf/{drawio.py → drawio/render.py} +1 -1
- md2conf/emoticon.py +3 -3
- md2conf/environment.py +2 -2
- md2conf/extension.py +82 -0
- md2conf/external.py +66 -0
- md2conf/formatting.py +135 -0
- md2conf/frontmatter.py +70 -0
- md2conf/image.py +128 -0
- md2conf/latex.py +4 -183
- md2conf/local.py +8 -8
- md2conf/markdown.py +1 -1
- md2conf/matcher.py +1 -1
- md2conf/mermaid/__init__.py +0 -0
- md2conf/mermaid/config.py +20 -0
- md2conf/mermaid/extension.py +109 -0
- md2conf/{mermaid.py → mermaid/render.py} +10 -38
- md2conf/mermaid/scanner.py +55 -0
- md2conf/metadata.py +1 -1
- md2conf/{domain.py → options.py} +75 -16
- md2conf/plantuml/__init__.py +0 -0
- md2conf/plantuml/config.py +20 -0
- md2conf/plantuml/extension.py +158 -0
- md2conf/plantuml/render.py +138 -0
- md2conf/plantuml/scanner.py +56 -0
- md2conf/png.py +206 -0
- md2conf/processor.py +55 -13
- md2conf/publisher.py +127 -39
- md2conf/scanner.py +38 -129
- md2conf/serializer.py +2 -2
- md2conf/svg.py +144 -103
- md2conf/text.py +1 -1
- md2conf/toc.py +73 -1
- md2conf/uri.py +1 -1
- md2conf/xml.py +1 -1
- markdown_to_confluence-0.5.2.dist-info/RECORD +0 -36
- {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.5.2.dist-info → markdown_to_confluence-0.5.4.dist-info}/zip-safe +0 -0
- /md2conf/{puppeteer-config.json → mermaid/puppeteer-config.json} +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
markdown_to_confluence-0.5.4.dist-info/licenses/LICENSE,sha256=SEEBf2BMI1LUHnDvyHnV6L12A6zTAOQcsyMvaawAXWo,1077
|
|
2
|
+
md2conf/__init__.py,sha256=0xq0z3v7oQaJxJwZRPOd-Z4zgOOCnzgDJYa9nytHTD0,402
|
|
3
|
+
md2conf/__main__.py,sha256=BPUZd0uzCsAOH3Y5o7ydvV7Z77jSI8fHfo4AZPyYj2c,14639
|
|
4
|
+
md2conf/api.py,sha256=3TlUmiDU31dfL8raMwv2wQWV_IvVq6fu8j86cjKTz1A,42780
|
|
5
|
+
md2conf/attachment.py,sha256=Nc3qGDENWBnsI6OVwMLXnk0EyEITpvov9MluDFD90ZI,1689
|
|
6
|
+
md2conf/coalesce.py,sha256=YHnqFwow5wCj6OQ3oosig01D2lxWusAScMF4HAUO2-g,1305
|
|
7
|
+
md2conf/collection.py,sha256=ukN74VCa4HaGSh6tLXpLd0j_UNPywcnKI0X7usgdSCo,824
|
|
8
|
+
md2conf/compatibility.py,sha256=4ZNN6VLqxSbI1kowdsPproGZqwxBISys4Z22vBfe6Z8,687
|
|
9
|
+
md2conf/converter.py,sha256=7eP4sEPgmyjiD0PO3jjyS5TcDY3OSW9hYsriLd9rbek,63201
|
|
10
|
+
md2conf/csf.py,sha256=6H9G-5cZyyWMJr0tFskPNiWdQ2Ehq-V8EhlvvxhukWY,6582
|
|
11
|
+
md2conf/emoticon.py,sha256=0g4rkx3d58xU4nnLak5ms7i0FSDnq0WJrLVFRgGyLC8,542
|
|
12
|
+
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
13
|
+
md2conf/environment.py,sha256=TfNEz3Pyw9qe7f8i7e_kph16c09fhZ4cLNZZzIjmI18,3892
|
|
14
|
+
md2conf/extension.py,sha256=_IBf_yhYb6luQM3A-vAAtCpjHay33kE4Au_SGuC3kow,2274
|
|
15
|
+
md2conf/external.py,sha256=uY1G7bdqEMJW66vOvKsh5CS4oHY-YA7h2VVuaSdaqBo,2366
|
|
16
|
+
md2conf/formatting.py,sha256=ygL59VgpioX069axEX-7XjKs0sUjTfIZiBE5fWmITxc,4557
|
|
17
|
+
md2conf/frontmatter.py,sha256=iWtn_oXoLQxvCsdI3OXs1ylWGmB-gc7mMLpSGg113i4,1888
|
|
18
|
+
md2conf/image.py,sha256=YrtcE5KhzcbjiT-oQEkk--yKSiRSPlDUtMpekoepIdo,5289
|
|
19
|
+
md2conf/latex.py,sha256=haZKkUxSEcPj3fVmiIVZAwgszqNqGLk1GQ7i8KGHpo0,2226
|
|
20
|
+
md2conf/local.py,sha256=eY3WpY-lNzLZeAfxX1ACVEhuzz0HDYX_sNQogJfkqcM,3673
|
|
21
|
+
md2conf/markdown.py,sha256=4Km-AbQH04nDgPF0ijo-Ld7o8jTPXzENIMn7P1qIk0o,3148
|
|
22
|
+
md2conf/matcher.py,sha256=Xg4YSb87iPkCzhKuKytBut6NOkEab3IM-AjzXbwy64U,6774
|
|
23
|
+
md2conf/metadata.py,sha256=NOjbCIrwLgTIIeNgmo7w5JXuT-pxOXBGSg-irfdpokk,976
|
|
24
|
+
md2conf/options.py,sha256=DLxnQBhDmDJgfEDSYyMChJi_krS1nsquOHBKg82aGrY,4500
|
|
25
|
+
md2conf/png.py,sha256=GU3-0dG6HqwGjedJVUciaIdA-6CdPTy_clsOQGr6dGE,6251
|
|
26
|
+
md2conf/processor.py,sha256=xVLpvKg2FEO0tWsHQ8sm7YpimQepbZ07W0_yUzcvl6c,11116
|
|
27
|
+
md2conf/publisher.py,sha256=xRIig53b4-DLncL07XBgLN3ecmTsWwNEK3ckjyhqfU8,11574
|
|
28
|
+
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
|
+
md2conf/scanner.py,sha256=bupKNe47DRc8MyMLzYfgtzyVHV9osJSgnr7KCnKsMuM,3888
|
|
30
|
+
md2conf/serializer.py,sha256=W4_yLJfT3vLw0PUg88lpUEnvn64CjaX3ZaKgIrwcxfw,1786
|
|
31
|
+
md2conf/svg.py,sha256=fjr8sWe-tqdAKaIq2bsR9qPrhnCXUmoVRtezHZa86cg,11558
|
|
32
|
+
md2conf/text.py,sha256=cnYV_JQp_v91LbQHo3qvxcEuhIdaPjCjkmLOKINcNv4,1736
|
|
33
|
+
md2conf/toc.py,sha256=aJEH3fIzDr2RufxFbHJ8maEpezp8uXI_uw90k3-KNkA,4585
|
|
34
|
+
md2conf/uri.py,sha256=my0deyR5SlppJrYCbXF1Zz94QA1JT-HTWe9pKw7AJ_A,1158
|
|
35
|
+
md2conf/xml.py,sha256=uaaUDs0hfluNX74dfkY_Dxu1KmeNDGogpGRGpUVEfE4,5526
|
|
36
|
+
md2conf/drawio/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
+
md2conf/drawio/extension.py,sha256=HHLUriTfg82VCfOEyzU-6j2IM9rxR3I1UdSDdujWHgU,4409
|
|
38
|
+
md2conf/drawio/render.py,sha256=veSu5gjm5ggLnmaH7uvH9qNeOygBJpqhSKK_LJs0QTk,8581
|
|
39
|
+
md2conf/mermaid/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
|
+
md2conf/mermaid/config.py,sha256=5Dec2QcdB_GtnuXIW6nhJK8J5caduNZU1oz1mcmmb44,376
|
|
41
|
+
md2conf/mermaid/extension.py,sha256=1drXVM_KbS00dcjSCRru0wwbil4zq3aR81dHMhfe7zA,4021
|
|
42
|
+
md2conf/mermaid/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
43
|
+
md2conf/mermaid/render.py,sha256=zO6M5UWSKiezoxPojD8iwFnwrFEDw_P6liQi-C3LQgw,1817
|
|
44
|
+
md2conf/mermaid/scanner.py,sha256=oIpaNxiZBNcmggnjlyYGcIVOXcYQWjf1lEVdyIwE4xE,1379
|
|
45
|
+
md2conf/plantuml/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
|
+
md2conf/plantuml/config.py,sha256=j0ONhkzmAPagh00ltamTKlVEvXa6R284We9pDxRy-5U,378
|
|
47
|
+
md2conf/plantuml/extension.py,sha256=EQ-2O4d2cWBGcIHcFFXgaCNrfi357hS6IE_PsvwJ8_k,6256
|
|
48
|
+
md2conf/plantuml/render.py,sha256=Lf1It2KxHPKNGM1rhIDg9zdC3iqhRNCduByqa0_k_qw,3725
|
|
49
|
+
md2conf/plantuml/scanner.py,sha256=Oso6VbHVuMaPMKMazQc_bf4hhOT5WeJN5WiVPM8peyM,1347
|
|
50
|
+
markdown_to_confluence-0.5.4.dist-info/METADATA,sha256=bz_rRHiqV-EYCMcXCE9NMlUc8bk28SbFz9KO43YuW0E,45324
|
|
51
|
+
markdown_to_confluence-0.5.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
52
|
+
markdown_to_confluence-0.5.4.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
53
|
+
markdown_to_confluence-0.5.4.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
54
|
+
markdown_to_confluence-0.5.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
55
|
+
markdown_to_confluence-0.5.4.dist-info/RECORD,,
|
md2conf/__init__.py
CHANGED
|
@@ -5,9 +5,9 @@ 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.
|
|
8
|
+
__version__ = "0.5.4"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
|
-
__copyright__ = "Copyright 2022-
|
|
10
|
+
__copyright__ = "Copyright 2022-2026, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
|
12
12
|
__maintainer__ = "Levente Hunyadi"
|
|
13
13
|
__status__ = "Production"
|
md2conf/__main__.py
CHANGED
|
@@ -4,7 +4,7 @@ Publish Markdown files to Confluence wiki.
|
|
|
4
4
|
Parses Markdown files, converts Markdown content into the Confluence Storage Format (XHTML), and invokes
|
|
5
5
|
Confluence API endpoints to upload images and content.
|
|
6
6
|
|
|
7
|
-
Copyright 2022-
|
|
7
|
+
Copyright 2022-2026, Levente Hunyadi
|
|
8
8
|
|
|
9
9
|
:see: https://github.com/hunyadi/md2conf
|
|
10
10
|
"""
|
|
@@ -16,17 +16,22 @@ import sys
|
|
|
16
16
|
import typing
|
|
17
17
|
from io import StringIO
|
|
18
18
|
from pathlib import Path
|
|
19
|
+
from types import TracebackType
|
|
19
20
|
from typing import Any, Iterable, Literal, Sequence
|
|
20
21
|
|
|
22
|
+
from requests.exceptions import HTTPError, JSONDecodeError
|
|
23
|
+
|
|
21
24
|
from . import __version__
|
|
22
|
-
from .
|
|
23
|
-
from .environment import ArgumentError,
|
|
24
|
-
from .extra import override
|
|
25
|
+
from .compatibility import override
|
|
26
|
+
from .environment import ArgumentError, ConfluenceSiteProperties, ConnectionProperties
|
|
25
27
|
from .metadata import ConfluenceSiteMetadata
|
|
28
|
+
from .options import ConfluencePageID, ConverterOptions, DocumentOptions, ImageLayoutOptions, LayoutOptions
|
|
29
|
+
|
|
30
|
+
LOGGER = logging.getLogger(__name__)
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
class Arguments(argparse.Namespace):
|
|
29
|
-
mdpath: Path
|
|
34
|
+
mdpath: list[Path]
|
|
30
35
|
domain: str | None
|
|
31
36
|
path: str | None
|
|
32
37
|
api_url: str | None
|
|
@@ -41,9 +46,11 @@ class Arguments(argparse.Namespace):
|
|
|
41
46
|
skip_title_heading: bool
|
|
42
47
|
title_prefix: str | None
|
|
43
48
|
generated_by: str | None
|
|
49
|
+
skip_update: bool
|
|
44
50
|
prefer_raster: bool
|
|
45
51
|
render_drawio: bool
|
|
46
52
|
render_mermaid: bool
|
|
53
|
+
render_plantuml: bool
|
|
47
54
|
render_latex: bool
|
|
48
55
|
diagram_output_format: Literal["png", "svg"]
|
|
49
56
|
local: bool
|
|
@@ -99,7 +106,7 @@ def get_parser() -> argparse.ArgumentParser:
|
|
|
99
106
|
parser = argparse.ArgumentParser(formatter_class=PositionalOnlyHelpFormatter)
|
|
100
107
|
parser.prog = os.path.basename(os.path.dirname(__file__))
|
|
101
108
|
parser.add_argument("--version", action="version", version=__version__)
|
|
102
|
-
parser.add_argument("mdpath", help="Path to Markdown file or directory to convert and publish.")
|
|
109
|
+
parser.add_argument("mdpath", type=Path, nargs="+", help="Path to Markdown file or directory to convert and publish.")
|
|
103
110
|
parser.add_argument("-d", "--domain", help="Confluence organization domain.")
|
|
104
111
|
parser.add_argument("-p", "--path", help="Base path for Confluence (default: '/wiki/').")
|
|
105
112
|
parser.add_argument(
|
|
@@ -165,6 +172,12 @@ def get_parser() -> argparse.ArgumentParser:
|
|
|
165
172
|
const=None,
|
|
166
173
|
help="Do not add 'generated by a tool' prompt to pages.",
|
|
167
174
|
)
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"--skip-update",
|
|
177
|
+
action="store_true",
|
|
178
|
+
default=False,
|
|
179
|
+
help="Skip saving Confluence page ID in Markdown files.",
|
|
180
|
+
)
|
|
168
181
|
parser.add_argument(
|
|
169
182
|
"--render-drawio",
|
|
170
183
|
dest="render_drawio",
|
|
@@ -191,6 +204,19 @@ def get_parser() -> argparse.ArgumentParser:
|
|
|
191
204
|
action="store_false",
|
|
192
205
|
help="Upload Mermaid diagram sources as Confluence page attachments. (Marketplace app required to display.)",
|
|
193
206
|
)
|
|
207
|
+
parser.add_argument(
|
|
208
|
+
"--render-plantuml",
|
|
209
|
+
dest="render_plantuml",
|
|
210
|
+
action="store_true",
|
|
211
|
+
default=True,
|
|
212
|
+
help="Render PlantUML diagrams as image files. (Installed utility required to convert.)",
|
|
213
|
+
)
|
|
214
|
+
parser.add_argument(
|
|
215
|
+
"--no-render-plantuml",
|
|
216
|
+
dest="render_plantuml",
|
|
217
|
+
action="store_false",
|
|
218
|
+
help="Upload PlantUML diagram sources as Confluence page attachments. (Marketplace app required to display.)",
|
|
219
|
+
)
|
|
194
220
|
parser.add_argument(
|
|
195
221
|
"--render-latex",
|
|
196
222
|
dest="render_latex",
|
|
@@ -310,35 +336,61 @@ def get_help() -> str:
|
|
|
310
336
|
return buf.getvalue()
|
|
311
337
|
|
|
312
338
|
|
|
339
|
+
def _exception_hook(exc_type: type[BaseException], exc_value: BaseException, traceback: TracebackType | None) -> None:
|
|
340
|
+
LOGGER.exception("Exception raised: %s", exc_type.__name__, exc_info=exc_value)
|
|
341
|
+
ex: BaseException | None = exc_value
|
|
342
|
+
while ex is not None:
|
|
343
|
+
print(f"\033[95m{ex.__class__.__name__}\033[0m: {ex}")
|
|
344
|
+
|
|
345
|
+
if isinstance(ex, HTTPError):
|
|
346
|
+
# print details for a response with JSON body
|
|
347
|
+
if ex.response is not None:
|
|
348
|
+
try:
|
|
349
|
+
LOGGER.error(ex.response.json())
|
|
350
|
+
except JSONDecodeError:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
ex = ex.__cause__
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
sys.excepthook = _exception_hook
|
|
357
|
+
|
|
358
|
+
|
|
313
359
|
def main() -> None:
|
|
314
360
|
parser = get_parser()
|
|
315
361
|
args = Arguments()
|
|
316
362
|
parser.parse_args(namespace=args)
|
|
317
363
|
|
|
318
|
-
args.mdpath = Path(args.mdpath)
|
|
319
|
-
|
|
320
364
|
logging.basicConfig(
|
|
321
365
|
level=getattr(logging, args.loglevel.upper(), logging.INFO),
|
|
322
366
|
format="%(asctime)s - %(levelname)s - %(funcName)s [%(lineno)d] - %(message)s",
|
|
323
367
|
)
|
|
324
368
|
|
|
325
|
-
options =
|
|
326
|
-
heading_anchors=args.heading_anchors,
|
|
327
|
-
ignore_invalid_url=args.ignore_invalid_url,
|
|
328
|
-
skip_title_heading=args.skip_title_heading,
|
|
329
|
-
title_prefix=args.title_prefix,
|
|
330
|
-
generated_by=args.generated_by,
|
|
369
|
+
options = DocumentOptions(
|
|
331
370
|
root_page_id=ConfluencePageID(args.root_page) if args.root_page else None,
|
|
332
371
|
keep_hierarchy=args.keep_hierarchy,
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
372
|
+
title_prefix=args.title_prefix,
|
|
373
|
+
generated_by=args.generated_by,
|
|
374
|
+
skip_update=args.skip_update,
|
|
375
|
+
converter=ConverterOptions(
|
|
376
|
+
heading_anchors=args.heading_anchors,
|
|
377
|
+
ignore_invalid_url=args.ignore_invalid_url,
|
|
378
|
+
skip_title_heading=args.skip_title_heading,
|
|
379
|
+
prefer_raster=args.prefer_raster,
|
|
380
|
+
render_drawio=args.render_drawio,
|
|
381
|
+
render_mermaid=args.render_mermaid,
|
|
382
|
+
render_plantuml=args.render_plantuml,
|
|
383
|
+
render_latex=args.render_latex,
|
|
384
|
+
diagram_output_format=args.diagram_output_format,
|
|
385
|
+
webui_links=args.webui_links,
|
|
386
|
+
use_panel=args.use_panel,
|
|
387
|
+
layout=LayoutOptions(
|
|
388
|
+
image=ImageLayoutOptions(
|
|
389
|
+
alignment=args.alignment,
|
|
390
|
+
max_width=args.max_image_width,
|
|
391
|
+
),
|
|
392
|
+
),
|
|
393
|
+
),
|
|
342
394
|
)
|
|
343
395
|
if args.local:
|
|
344
396
|
from .local import LocalConverter
|
|
@@ -356,15 +408,15 @@ def main() -> None:
|
|
|
356
408
|
base_path=site_properties.base_path,
|
|
357
409
|
space_key=site_properties.space_key,
|
|
358
410
|
)
|
|
359
|
-
LocalConverter(options, site_metadata)
|
|
411
|
+
converter = LocalConverter(options, site_metadata)
|
|
412
|
+
for item in args.mdpath:
|
|
413
|
+
converter.process(item)
|
|
360
414
|
else:
|
|
361
|
-
from requests import HTTPError, JSONDecodeError
|
|
362
|
-
|
|
363
415
|
from .api import ConfluenceAPI
|
|
364
416
|
from .publisher import Publisher
|
|
365
417
|
|
|
366
418
|
try:
|
|
367
|
-
properties =
|
|
419
|
+
properties = ConnectionProperties(
|
|
368
420
|
api_url=args.api_url,
|
|
369
421
|
domain=args.domain,
|
|
370
422
|
base_path=args.path,
|
|
@@ -375,23 +427,10 @@ def main() -> None:
|
|
|
375
427
|
)
|
|
376
428
|
except ArgumentError as e:
|
|
377
429
|
parser.error(str(e))
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
options,
|
|
383
|
-
).process(args.mdpath)
|
|
384
|
-
except HTTPError as err:
|
|
385
|
-
logging.error(err)
|
|
386
|
-
|
|
387
|
-
# print details for a response with JSON body
|
|
388
|
-
if err.response is not None:
|
|
389
|
-
try:
|
|
390
|
-
logging.error(err.response.json())
|
|
391
|
-
except JSONDecodeError:
|
|
392
|
-
pass
|
|
393
|
-
|
|
394
|
-
sys.exit(1)
|
|
430
|
+
with ConfluenceAPI(properties) as api:
|
|
431
|
+
publisher = Publisher(api, options)
|
|
432
|
+
for item in args.mdpath:
|
|
433
|
+
publisher.process(item)
|
|
395
434
|
|
|
396
435
|
|
|
397
436
|
if __name__ == "__main__":
|
md2conf/api.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Publish Markdown files to Confluence wiki.
|
|
3
3
|
|
|
4
|
-
Copyright 2022-
|
|
4
|
+
Copyright 2022-2026, Levente Hunyadi
|
|
5
5
|
|
|
6
6
|
:see: https://github.com/hunyadi/md2conf
|
|
7
7
|
"""
|
|
@@ -25,8 +25,8 @@ import requests
|
|
|
25
25
|
import truststore
|
|
26
26
|
from requests.adapters import HTTPAdapter
|
|
27
27
|
|
|
28
|
-
from .
|
|
29
|
-
from .
|
|
28
|
+
from .compatibility import override
|
|
29
|
+
from .environment import ArgumentError, ConfluenceError, ConnectionProperties, PageError
|
|
30
30
|
from .metadata import ConfluenceSiteMetadata
|
|
31
31
|
from .serializer import JsonType, json_to_object, object_to_json_payload
|
|
32
32
|
|
|
@@ -35,6 +35,7 @@ T = TypeVar("T")
|
|
|
35
35
|
# spellchecker: disable
|
|
36
36
|
mimetypes.add_type("application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx", strict=True)
|
|
37
37
|
mimetypes.add_type("text/vnd.mermaid", ".mmd", strict=True)
|
|
38
|
+
mimetypes.add_type("text/vnd.plantuml", ".puml", strict=True)
|
|
38
39
|
mimetypes.add_type("application/vnd.oasis.opendocument.presentation", ".odp", strict=True)
|
|
39
40
|
mimetypes.add_type("application/vnd.oasis.opendocument.spreadsheet", ".ods", strict=True)
|
|
40
41
|
mimetypes.add_type("application/vnd.oasis.opendocument.text", ".odt", strict=True)
|
|
@@ -370,11 +371,11 @@ class ConfluenceAPI:
|
|
|
370
371
|
Encapsulates operations that can be invoked via the [Confluence REST API](https://developer.atlassian.com/cloud/confluence/rest/v2/).
|
|
371
372
|
"""
|
|
372
373
|
|
|
373
|
-
properties:
|
|
374
|
+
properties: ConnectionProperties
|
|
374
375
|
session: "ConfluenceSession | None" = None
|
|
375
376
|
|
|
376
|
-
def __init__(self, properties:
|
|
377
|
-
self.properties = properties or
|
|
377
|
+
def __init__(self, properties: ConnectionProperties | None = None) -> None:
|
|
378
|
+
self.properties = properties or ConnectionProperties()
|
|
378
379
|
|
|
379
380
|
def __enter__(self) -> "ConfluenceSession":
|
|
380
381
|
"""
|
|
@@ -626,7 +627,16 @@ class ConfluenceSession:
|
|
|
626
627
|
|
|
627
628
|
return id
|
|
628
629
|
|
|
630
|
+
@overload
|
|
631
|
+
def get_space_id(self, *, space_id: str | None = None) -> str | None: ...
|
|
632
|
+
|
|
633
|
+
@overload
|
|
634
|
+
def get_space_id(self, *, space_key: str | None = None) -> str | None: ...
|
|
635
|
+
|
|
629
636
|
def get_space_id(self, *, space_id: str | None = None, space_key: str | None = None) -> str | None:
|
|
637
|
+
return self._get_space_id(space_id=space_id, space_key=space_key)
|
|
638
|
+
|
|
639
|
+
def _get_space_id(self, *, space_id: str | None = None, space_key: str | None = None) -> str | None:
|
|
630
640
|
"""
|
|
631
641
|
Coalesces a space ID or space key into a space ID, accounting for site default.
|
|
632
642
|
|
|
@@ -647,6 +657,15 @@ class ConfluenceSession:
|
|
|
647
657
|
# space ID and key are unset, and no default space is configured
|
|
648
658
|
return None
|
|
649
659
|
|
|
660
|
+
def get_homepage_id(self, space_id: str) -> str:
|
|
661
|
+
"""
|
|
662
|
+
Returns the page ID corresponding to the space home page.
|
|
663
|
+
"""
|
|
664
|
+
|
|
665
|
+
path = f"/spaces/{space_id}"
|
|
666
|
+
data = self._get(ConfluenceVersion.VERSION_2, path, dict[str, JsonType])
|
|
667
|
+
return typing.cast(str, data["homepageId"])
|
|
668
|
+
|
|
650
669
|
def get_attachment_by_name(self, page_id: str, filename: str) -> ConfluenceAttachment:
|
|
651
670
|
"""
|
|
652
671
|
Retrieves a Confluence page attachment by an unprefixed file name.
|
|
@@ -831,7 +850,7 @@ class ConfluenceSession:
|
|
|
831
850
|
query = {
|
|
832
851
|
"title": title,
|
|
833
852
|
}
|
|
834
|
-
space_id = self.
|
|
853
|
+
space_id = self._get_space_id(space_id=space_id, space_key=space_key)
|
|
835
854
|
if space_id is not None:
|
|
836
855
|
query["space-id"] = space_id
|
|
837
856
|
|
|
@@ -871,10 +890,10 @@ class ConfluenceSession:
|
|
|
871
890
|
else:
|
|
872
891
|
raise
|
|
873
892
|
|
|
874
|
-
#
|
|
893
|
+
# this should not be reached, but satisfies type checker
|
|
875
894
|
if last_error is not None:
|
|
876
895
|
raise last_error
|
|
877
|
-
raise ConfluenceError(f"
|
|
896
|
+
raise ConfluenceError(f"failed to get page: {page_id}")
|
|
878
897
|
|
|
879
898
|
def get_page_properties(self, page_id: str) -> ConfluencePageProperties:
|
|
880
899
|
"""
|
|
@@ -1006,7 +1025,7 @@ class ConfluenceSession:
|
|
|
1006
1025
|
:returns: Confluence page ID of a matching page (if found), or `None`.
|
|
1007
1026
|
"""
|
|
1008
1027
|
|
|
1009
|
-
space_id = self.
|
|
1028
|
+
space_id = self._get_space_id(space_id=space_id, space_key=space_key)
|
|
1010
1029
|
path = "/pages"
|
|
1011
1030
|
query = {"title": title}
|
|
1012
1031
|
if space_id is not None:
|
|
@@ -1039,6 +1058,7 @@ class ConfluenceSession:
|
|
|
1039
1058
|
|
|
1040
1059
|
:param title: Page title. Pages in the same Confluence space must have a unique title.
|
|
1041
1060
|
:param parent_id: Identifies the parent page for a new child page.
|
|
1061
|
+
:returns: Confluence page info for the found or newly created page.
|
|
1042
1062
|
"""
|
|
1043
1063
|
|
|
1044
1064
|
parent_page = self.get_page_properties(parent_id)
|
md2conf/attachment.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Publish Markdown files to Confluence wiki.
|
|
3
|
+
|
|
4
|
+
Copyright 2022-2026, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/md2conf
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ImageData:
|
|
16
|
+
path: Path
|
|
17
|
+
description: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class EmbeddedFileData:
|
|
22
|
+
data: bytes
|
|
23
|
+
description: str | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AttachmentCatalog:
|
|
27
|
+
"Maintains a list of files and binary data to be uploaded to Confluence as attachments."
|
|
28
|
+
|
|
29
|
+
images: list[ImageData]
|
|
30
|
+
embedded_files: dict[str, EmbeddedFileData]
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self.images = []
|
|
34
|
+
self.embedded_files = {}
|
|
35
|
+
|
|
36
|
+
def add_image(self, data: ImageData) -> None:
|
|
37
|
+
self.images.append(data)
|
|
38
|
+
|
|
39
|
+
def add_embed(self, filename: str, data: EmbeddedFileData) -> None:
|
|
40
|
+
self.embedded_files[filename] = data
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def attachment_name(ref: Path | str) -> str:
|
|
44
|
+
"""
|
|
45
|
+
Safe name for use with attachment uploads.
|
|
46
|
+
|
|
47
|
+
Mutates a relative path such that it meets Confluence's attachment naming requirements.
|
|
48
|
+
|
|
49
|
+
Allowed characters:
|
|
50
|
+
|
|
51
|
+
* Alphanumeric characters: 0-9, a-z, A-Z
|
|
52
|
+
* Special characters: hyphen (-), underscore (_), period (.)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
if isinstance(ref, Path):
|
|
56
|
+
path = ref
|
|
57
|
+
else:
|
|
58
|
+
path = Path(ref)
|
|
59
|
+
|
|
60
|
+
if path.drive or path.root:
|
|
61
|
+
raise ValueError(f"required: relative path; got: {ref}")
|
|
62
|
+
|
|
63
|
+
regexp = re.compile(r"[^\-0-9A-Za-z_.]", re.UNICODE)
|
|
64
|
+
|
|
65
|
+
def replace_part(part: str) -> str:
|
|
66
|
+
if part == "..":
|
|
67
|
+
return "PAR"
|
|
68
|
+
else:
|
|
69
|
+
return regexp.sub("_", part)
|
|
70
|
+
|
|
71
|
+
parts = [replace_part(p) for p in path.parts]
|
|
72
|
+
return Path(*parts).as_posix().replace("/", "_")
|
md2conf/coalesce.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Publish Markdown files to Confluence wiki.
|
|
3
|
+
|
|
4
|
+
Copyright 2022-2026, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/md2conf
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
import dataclasses
|
|
11
|
+
from typing import Any, ClassVar, Protocol, TypeVar
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DataclassInstance(Protocol):
|
|
15
|
+
__dataclass_fields__: ClassVar[dict[str, dataclasses.Field[Any]]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
D = TypeVar("D", bound=DataclassInstance)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def coalesce(target: D, source: D) -> D:
|
|
22
|
+
"""
|
|
23
|
+
Implements nullish coalescing assignment on each field of a data-class.
|
|
24
|
+
|
|
25
|
+
Iterates over each field of the data-class, and evaluates the right operand and assigns it to the left only if
|
|
26
|
+
the left operand is `None`. Applies recursively when the field is a data-class.
|
|
27
|
+
|
|
28
|
+
:returns: A newly created data-class instance.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
updates: dict[str, Any] = {}
|
|
32
|
+
for field in dataclasses.fields(target):
|
|
33
|
+
target_field = getattr(target, field.name, None)
|
|
34
|
+
source_field = getattr(source, field.name, None)
|
|
35
|
+
|
|
36
|
+
if target_field is None:
|
|
37
|
+
if source_field is not None:
|
|
38
|
+
updates[field.name] = copy.deepcopy(source_field)
|
|
39
|
+
elif dataclasses.is_dataclass(field.type):
|
|
40
|
+
if source_field is not None:
|
|
41
|
+
updates[field.name] = coalesce(target_field, source_field)
|
|
42
|
+
|
|
43
|
+
return dataclasses.replace(target, **updates)
|
md2conf/collection.py
CHANGED