markdown-to-confluence 0.4.8__py3-none-any.whl → 0.5.0__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.4.8.dist-info → markdown_to_confluence-0.5.0.dist-info}/METADATA +10 -11
- markdown_to_confluence-0.5.0.dist-info/RECORD +35 -0
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +13 -13
- md2conf/api.py +55 -71
- md2conf/collection.py +2 -2
- md2conf/converter.py +22 -22
- md2conf/domain.py +3 -3
- md2conf/drawio.py +1 -1
- md2conf/environment.py +22 -22
- md2conf/latex.py +9 -9
- md2conf/local.py +5 -6
- md2conf/markdown.py +7 -7
- md2conf/matcher.py +5 -5
- md2conf/mermaid.py +3 -3
- md2conf/metadata.py +1 -2
- md2conf/processor.py +12 -12
- md2conf/publisher.py +1 -2
- md2conf/scanner.py +33 -41
- md2conf/serializer.py +52 -0
- md2conf/toc.py +2 -3
- md2conf/xml.py +4 -6
- markdown_to_confluence-0.4.8.dist-info/RECORD +0 -34
- {markdown_to_confluence-0.4.8.dist-info → markdown_to_confluence-0.5.0.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.8.dist-info → markdown_to_confluence-0.5.0.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.8.dist-info → markdown_to_confluence-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.8.dist-info → markdown_to_confluence-0.5.0.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.8.dist-info → markdown_to_confluence-0.5.0.dist-info}/zip-safe +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-to-confluence
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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>
|
|
@@ -13,33 +13,32 @@ Classifier: Environment :: Console
|
|
|
13
13
|
Classifier: Intended Audience :: End Users/Desktop
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
21
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
|
-
Requires-Dist:
|
|
27
|
-
Requires-Dist: json_strong_typing>=0.4
|
|
26
|
+
Requires-Dist: cattrs>=25.3
|
|
28
27
|
Requires-Dist: lxml>=6.0
|
|
29
|
-
Requires-Dist: markdown>=3.
|
|
30
|
-
Requires-Dist: pymdown-extensions>=10.
|
|
28
|
+
Requires-Dist: markdown>=3.10
|
|
29
|
+
Requires-Dist: pymdown-extensions>=10.17
|
|
31
30
|
Requires-Dist: PyYAML>=6.0
|
|
32
31
|
Requires-Dist: requests>=2.32
|
|
33
|
-
Requires-Dist: truststore>=0.10
|
|
32
|
+
Requires-Dist: truststore>=0.10
|
|
34
33
|
Requires-Dist: typing-extensions>=4.15; python_version < "3.12"
|
|
35
34
|
Provides-Extra: dev
|
|
36
|
-
Requires-Dist: markdown_doc>=0.1.5;
|
|
35
|
+
Requires-Dist: markdown_doc>=0.1.5; extra == "dev"
|
|
37
36
|
Requires-Dist: types-lxml>=2025.8.25; extra == "dev"
|
|
38
|
-
Requires-Dist: types-markdown>=3.
|
|
37
|
+
Requires-Dist: types-markdown>=3.10; extra == "dev"
|
|
39
38
|
Requires-Dist: types-PyYAML>=6.0; extra == "dev"
|
|
40
39
|
Requires-Dist: types-requests>=2.32; extra == "dev"
|
|
41
40
|
Requires-Dist: mypy>=1.18; extra == "dev"
|
|
42
|
-
Requires-Dist: ruff>=0.
|
|
41
|
+
Requires-Dist: ruff>=0.14; extra == "dev"
|
|
43
42
|
Provides-Extra: formulas
|
|
44
43
|
Requires-Dist: matplotlib>=3.9; extra == "formulas"
|
|
45
44
|
Dynamic: license-file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
markdown_to_confluence-0.5.0.dist-info/licenses/LICENSE,sha256=56L-Y0dyZwyVlINRJRz3PNw-ka-oLVaAq-7d8zo6qlc,1077
|
|
2
|
+
md2conf/__init__.py,sha256=Ec01qZ3V1WkyzOh0MJegHd4kdvDCJhS5KjwuCfk_BRs,402
|
|
3
|
+
md2conf/__main__.py,sha256=ZAwZ2YqKUxKiVx8CQsrnso9z2deP5Xn80kqqf2o3AbY,11472
|
|
4
|
+
md2conf/api.py,sha256=yFDsE_5IpCXG4z24ZrxF8QjF07ep3HiBHqaWKcGKf1k,40731
|
|
5
|
+
md2conf/collection.py,sha256=nghFS5kK4kPbpLE7IHi4rprJK-Mu4KXNxjHYM9Rc5SQ,824
|
|
6
|
+
md2conf/converter.py,sha256=BM94de0CAQGXOTSve7_042y3VF_yu77NITX5FUUeJPQ,69446
|
|
7
|
+
md2conf/csf.py,sha256=rugs3qC2aJQCJSTczeBw9WhqSZZtMq14LjwT0V1b6Hc,6476
|
|
8
|
+
md2conf/domain.py,sha256=EsaAfUaT2qIrK91uRyxaPEY4kSq-nzhccErxVqHdooc,2205
|
|
9
|
+
md2conf/drawio.py,sha256=IqFlAegrKM5SQf5CqHD8STIzskH7Rpm9RtWwn_nXVTc,8581
|
|
10
|
+
md2conf/emoticon.py,sha256=P2L5oQvnRXeVifJQ3sJ2Ck-6ptbxumq2vsT-rM0W0Ms,484
|
|
11
|
+
md2conf/entities.dtd,sha256=M6NzqL5N7dPs_eUA_6sDsiSLzDaAacrx9LdttiufvYU,30215
|
|
12
|
+
md2conf/environment.py,sha256=BhI7YktY7G26HOhGlUvTkH2Vmfa4E_dhu2snzbBgMvE,3902
|
|
13
|
+
md2conf/extra.py,sha256=VuMxuOnnC2Qwy6y52ukIxsaYhrZArRqMmRHRE4QZl8g,687
|
|
14
|
+
md2conf/latex.py,sha256=3eFgsvaq6ROAc2skW1Wq21CX_pJai1Yc9t861Z3s5XA,7600
|
|
15
|
+
md2conf/local.py,sha256=Ou-j7kZWbHxC8Si8Yg7myqtTQ1He6mYQW1NpX3LLIcY,3704
|
|
16
|
+
md2conf/markdown.py,sha256=t-z19Zs_91_jzRvwmOsWqCDt0Tdghmk5bpNUON0YlKc,3148
|
|
17
|
+
md2conf/matcher.py,sha256=hkM49osFM9nrXRXe4pwcGCg0rrLsmKep7AYY_S01kNY,6774
|
|
18
|
+
md2conf/mermaid.py,sha256=9P4VV69dooaFBNUjdTIpzq7BFA8rDMqEif1O7XKWPdM,2617
|
|
19
|
+
md2conf/metadata.py,sha256=_kt_lh4gCzVRRhhrDk-cJCk9WMcX9ZDWB6hL0Lw3xoI,976
|
|
20
|
+
md2conf/processor.py,sha256=8Y-NSxAuqSHMSN9vhw_83HisGAmq87XAY98dis_xZ0Y,9690
|
|
21
|
+
md2conf/publisher.py,sha256=yI7gObPZLrNEXbiPKBJwkBPcGLI17UwzKd8FQe3U8bE,8634
|
|
22
|
+
md2conf/puppeteer-config.json,sha256=-dMTAN_7kNTGbDlfXzApl0KJpAWna9YKZdwMKbpOb60,159
|
|
23
|
+
md2conf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
md2conf/scanner.py,sha256=o46fTQXuTpbtvpnQPW3CW4ydIb5bM362K2TFpwO51P0,6782
|
|
25
|
+
md2conf/serializer.py,sha256=fRpbGUH_6liMDNeJl0LikgpSytle-7o4o3zE339592U,1343
|
|
26
|
+
md2conf/text.py,sha256=fHOrUaPXAjE4iRhHqFq-CiI-knpo4wvyHCWp0crewqA,1736
|
|
27
|
+
md2conf/toc.py,sha256=ZrfUfTv_Jiv27G4SBNjK3b-1ClYKoqN5yPRsEWp6IXk,2413
|
|
28
|
+
md2conf/uri.py,sha256=KbLBdRFtZTQTZd8b4j0LtE8Pb68Ly0WkemF4iW-EAB4,1158
|
|
29
|
+
md2conf/xml.py,sha256=Fu00Eg8c0VgMHIjRDBJBSNWtui8obEtowkiR7gHTduM,5526
|
|
30
|
+
markdown_to_confluence-0.5.0.dist-info/METADATA,sha256=1Dm2_wAu9FaYnxv5haqzq8XLND4QC3o823C5f6-0a_4,36435
|
|
31
|
+
markdown_to_confluence-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
32
|
+
markdown_to_confluence-0.5.0.dist-info/entry_points.txt,sha256=F1zxa1wtEObtbHS-qp46330WVFLHdMnV2wQ-ZorRmX0,50
|
|
33
|
+
markdown_to_confluence-0.5.0.dist-info/top_level.txt,sha256=_FJfl_kHrHNidyjUOuS01ngu_jDsfc-ZjSocNRJnTzU,8
|
|
34
|
+
markdown_to_confluence-0.5.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
35
|
+
markdown_to_confluence-0.5.0.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.
|
|
8
|
+
__version__ = "0.5.0"
|
|
9
9
|
__author__ = "Levente Hunyadi"
|
|
10
10
|
__copyright__ = "Copyright 2022-2025, Levente Hunyadi"
|
|
11
11
|
__license__ = "MIT"
|
md2conf/__main__.py
CHANGED
|
@@ -16,7 +16,7 @@ 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,
|
|
19
|
+
from typing import Any, Iterable, Literal, Sequence
|
|
20
20
|
|
|
21
21
|
from . import __version__
|
|
22
22
|
from .domain import ConfluenceDocumentOptions, ConfluencePageID
|
|
@@ -27,18 +27,18 @@ from .metadata import ConfluenceSiteMetadata
|
|
|
27
27
|
|
|
28
28
|
class Arguments(argparse.Namespace):
|
|
29
29
|
mdpath: Path
|
|
30
|
-
domain:
|
|
31
|
-
path:
|
|
32
|
-
api_url:
|
|
33
|
-
username:
|
|
34
|
-
api_key:
|
|
35
|
-
space:
|
|
30
|
+
domain: str | None
|
|
31
|
+
path: str | None
|
|
32
|
+
api_url: str | None
|
|
33
|
+
username: str | None
|
|
34
|
+
api_key: str | None
|
|
35
|
+
space: str | None
|
|
36
36
|
loglevel: str
|
|
37
37
|
ignore_invalid_url: bool
|
|
38
38
|
heading_anchors: bool
|
|
39
|
-
root_page:
|
|
39
|
+
root_page: str | None
|
|
40
40
|
keep_hierarchy: bool
|
|
41
|
-
generated_by:
|
|
41
|
+
generated_by: str | None
|
|
42
42
|
render_drawio: bool
|
|
43
43
|
render_mermaid: bool
|
|
44
44
|
render_latex: bool
|
|
@@ -58,8 +58,8 @@ class KwargsAppendAction(argparse.Action):
|
|
|
58
58
|
self,
|
|
59
59
|
parser: argparse.ArgumentParser,
|
|
60
60
|
namespace: argparse.Namespace,
|
|
61
|
-
values:
|
|
62
|
-
option_string:
|
|
61
|
+
values: str | Sequence[Any] | None,
|
|
62
|
+
option_string: str | None = None,
|
|
63
63
|
) -> None:
|
|
64
64
|
try:
|
|
65
65
|
d = dict(map(lambda x: x.split("="), typing.cast(Sequence[str], values)))
|
|
@@ -74,10 +74,10 @@ class KwargsAppendAction(argparse.Action):
|
|
|
74
74
|
class PositionalOnlyHelpFormatter(argparse.HelpFormatter):
|
|
75
75
|
def _format_usage(
|
|
76
76
|
self,
|
|
77
|
-
usage:
|
|
77
|
+
usage: str | None,
|
|
78
78
|
actions: Iterable[argparse.Action],
|
|
79
79
|
groups: Iterable[argparse._MutuallyExclusiveGroup], # pyright: ignore[reportPrivateUsage]
|
|
80
|
-
prefix:
|
|
80
|
+
prefix: str | None,
|
|
81
81
|
) -> str:
|
|
82
82
|
# filter only positional arguments
|
|
83
83
|
positional_actions = [a for a in actions if not a.option_strings]
|
md2conf/api.py
CHANGED
|
@@ -12,27 +12,21 @@ import io
|
|
|
12
12
|
import logging
|
|
13
13
|
import mimetypes
|
|
14
14
|
import ssl
|
|
15
|
-
import sys
|
|
16
15
|
import typing
|
|
17
16
|
from dataclasses import dataclass
|
|
18
17
|
from pathlib import Path
|
|
19
18
|
from types import TracebackType
|
|
20
|
-
from typing import Any,
|
|
19
|
+
from typing import Any, TypeVar, overload
|
|
21
20
|
from urllib.parse import urlencode, urlparse, urlunparse
|
|
22
21
|
|
|
23
22
|
import requests
|
|
23
|
+
import truststore
|
|
24
24
|
from requests.adapters import HTTPAdapter
|
|
25
|
-
from strong_typing.core import JsonType
|
|
26
|
-
from strong_typing.serialization import DeserializerOptions, json_dump_string, json_to_object, object_to_json
|
|
27
25
|
|
|
28
26
|
from .environment import ArgumentError, ConfluenceConnectionProperties, ConfluenceError, PageError
|
|
29
27
|
from .extra import override
|
|
30
28
|
from .metadata import ConfluenceSiteMetadata
|
|
31
|
-
|
|
32
|
-
if sys.version_info >= (3, 10):
|
|
33
|
-
import truststore
|
|
34
|
-
else:
|
|
35
|
-
import certifi
|
|
29
|
+
from .serializer import JsonType, json_to_object, object_to_json_payload
|
|
36
30
|
|
|
37
31
|
T = TypeVar("T")
|
|
38
32
|
|
|
@@ -45,14 +39,7 @@ mimetypes.add_type("application/vnd.openxmlformats-officedocument.presentationml
|
|
|
45
39
|
mimetypes.add_type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx", strict=True)
|
|
46
40
|
|
|
47
41
|
|
|
48
|
-
def
|
|
49
|
-
typ: type[T],
|
|
50
|
-
data: JsonType,
|
|
51
|
-
) -> T:
|
|
52
|
-
return json_to_object(typ, data, options=DeserializerOptions(skip_unassigned=True))
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def build_url(base_url: str, query: Optional[dict[str, str]] = None) -> str:
|
|
42
|
+
def build_url(base_url: str, query: dict[str, str] | None = None) -> str:
|
|
56
43
|
"Builds a URL with scheme, host, port, path and query string parameters."
|
|
57
44
|
|
|
58
45
|
scheme, netloc, path, params, query_str, fragment = urlparse(base_url)
|
|
@@ -79,7 +66,7 @@ def response_cast(response_type: None, response: requests.Response) -> None: ...
|
|
|
79
66
|
def response_cast(response_type: type[T], response: requests.Response) -> T: ...
|
|
80
67
|
|
|
81
68
|
|
|
82
|
-
def response_cast(response_type:
|
|
69
|
+
def response_cast(response_type: type[T] | None, response: requests.Response) -> T | None:
|
|
83
70
|
"Converts a response body into the expected type."
|
|
84
71
|
|
|
85
72
|
if response.text:
|
|
@@ -88,7 +75,7 @@ def response_cast(response_type: Optional[type[T]], response: requests.Response)
|
|
|
88
75
|
if response_type is None:
|
|
89
76
|
return None
|
|
90
77
|
else:
|
|
91
|
-
return
|
|
78
|
+
return json_to_object(response_type, response.json())
|
|
92
79
|
|
|
93
80
|
|
|
94
81
|
@enum.unique
|
|
@@ -155,9 +142,9 @@ class ConfluenceResultSet:
|
|
|
155
142
|
class ConfluenceContentVersion:
|
|
156
143
|
number: int
|
|
157
144
|
minorEdit: bool = False
|
|
158
|
-
createdAt:
|
|
159
|
-
message:
|
|
160
|
-
authorId:
|
|
145
|
+
createdAt: datetime.datetime | None = None
|
|
146
|
+
message: str | None = None
|
|
147
|
+
authorId: str | None = None
|
|
161
148
|
|
|
162
149
|
|
|
163
150
|
@dataclass(frozen=True)
|
|
@@ -182,12 +169,12 @@ class ConfluenceAttachment:
|
|
|
182
169
|
|
|
183
170
|
id: str
|
|
184
171
|
status: ConfluenceStatus
|
|
185
|
-
title:
|
|
172
|
+
title: str | None
|
|
186
173
|
createdAt: datetime.datetime
|
|
187
174
|
pageId: str
|
|
188
175
|
mediaType: str
|
|
189
|
-
mediaTypeDescription:
|
|
190
|
-
comment:
|
|
176
|
+
mediaTypeDescription: str | None
|
|
177
|
+
comment: str | None
|
|
191
178
|
fileId: str
|
|
192
179
|
fileSize: int
|
|
193
180
|
webuiLink: str
|
|
@@ -218,12 +205,12 @@ class ConfluencePageProperties:
|
|
|
218
205
|
status: ConfluenceStatus
|
|
219
206
|
title: str
|
|
220
207
|
spaceId: str
|
|
221
|
-
parentId:
|
|
222
|
-
parentType:
|
|
223
|
-
position:
|
|
208
|
+
parentId: str | None
|
|
209
|
+
parentType: ConfluencePageParentContentType | None
|
|
210
|
+
position: int | None
|
|
224
211
|
authorId: str
|
|
225
212
|
ownerId: str
|
|
226
|
-
lastOwnerId:
|
|
213
|
+
lastOwnerId: str | None
|
|
227
214
|
createdAt: datetime.datetime
|
|
228
215
|
version: ConfluenceContentVersion
|
|
229
216
|
|
|
@@ -329,9 +316,9 @@ class ConfluenceIdentifiedContentProperty(ConfluenceVersionedContentProperty):
|
|
|
329
316
|
@dataclass(frozen=True)
|
|
330
317
|
class ConfluenceCreatePageRequest:
|
|
331
318
|
spaceId: str
|
|
332
|
-
status:
|
|
333
|
-
title:
|
|
334
|
-
parentId:
|
|
319
|
+
status: ConfluenceStatus | None
|
|
320
|
+
title: str | None
|
|
321
|
+
parentId: str | None
|
|
335
322
|
body: ConfluencePageBody
|
|
336
323
|
|
|
337
324
|
|
|
@@ -368,10 +355,7 @@ class TruststoreAdapter(HTTPAdapter):
|
|
|
368
355
|
Adapts the pool manager to use the provided SSL context instead of the default.
|
|
369
356
|
"""
|
|
370
357
|
|
|
371
|
-
|
|
372
|
-
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
373
|
-
else:
|
|
374
|
-
ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
|
|
358
|
+
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
375
359
|
ctx.check_hostname = True
|
|
376
360
|
ctx.verify_mode = ssl.CERT_REQUIRED
|
|
377
361
|
super().init_poolmanager(connections, maxsize, block, ssl_context=ctx, **pool_kwargs) # type: ignore[no-untyped-call]
|
|
@@ -383,9 +367,9 @@ class ConfluenceAPI:
|
|
|
383
367
|
"""
|
|
384
368
|
|
|
385
369
|
properties: ConfluenceConnectionProperties
|
|
386
|
-
session:
|
|
370
|
+
session: "ConfluenceSession | None" = None
|
|
387
371
|
|
|
388
|
-
def __init__(self, properties:
|
|
372
|
+
def __init__(self, properties: ConfluenceConnectionProperties | None = None) -> None:
|
|
389
373
|
self.properties = properties or ConfluenceConnectionProperties()
|
|
390
374
|
|
|
391
375
|
def __enter__(self) -> "ConfluenceSession":
|
|
@@ -415,9 +399,9 @@ class ConfluenceAPI:
|
|
|
415
399
|
|
|
416
400
|
def __exit__(
|
|
417
401
|
self,
|
|
418
|
-
exc_type:
|
|
419
|
-
exc_val:
|
|
420
|
-
exc_tb:
|
|
402
|
+
exc_type: type[BaseException] | None,
|
|
403
|
+
exc_val: BaseException | None,
|
|
404
|
+
exc_tb: TracebackType | None,
|
|
421
405
|
) -> None:
|
|
422
406
|
"""
|
|
423
407
|
Closes an open connection.
|
|
@@ -444,10 +428,10 @@ class ConfluenceSession:
|
|
|
444
428
|
self,
|
|
445
429
|
session: requests.Session,
|
|
446
430
|
*,
|
|
447
|
-
api_url:
|
|
448
|
-
domain:
|
|
449
|
-
base_path:
|
|
450
|
-
space_key:
|
|
431
|
+
api_url: str | None,
|
|
432
|
+
domain: str | None,
|
|
433
|
+
base_path: str | None,
|
|
434
|
+
space_key: str | None,
|
|
451
435
|
) -> None:
|
|
452
436
|
self.session = session
|
|
453
437
|
self._space_id_to_key = {}
|
|
@@ -503,7 +487,7 @@ class ConfluenceSession:
|
|
|
503
487
|
self,
|
|
504
488
|
version: ConfluenceVersion,
|
|
505
489
|
path: str,
|
|
506
|
-
query:
|
|
490
|
+
query: dict[str, str] | None = None,
|
|
507
491
|
) -> str:
|
|
508
492
|
"""
|
|
509
493
|
Builds a full URL for invoking the Confluence API.
|
|
@@ -523,7 +507,7 @@ class ConfluenceSession:
|
|
|
523
507
|
path: str,
|
|
524
508
|
response_type: type[T],
|
|
525
509
|
*,
|
|
526
|
-
query:
|
|
510
|
+
query: dict[str, str] | None = None,
|
|
527
511
|
) -> T:
|
|
528
512
|
"Executes an HTTP request via Confluence API."
|
|
529
513
|
|
|
@@ -532,9 +516,9 @@ class ConfluenceSession:
|
|
|
532
516
|
if response.text:
|
|
533
517
|
LOGGER.debug("Received HTTP payload:\n%s", response.text)
|
|
534
518
|
response.raise_for_status()
|
|
535
|
-
return
|
|
519
|
+
return json_to_object(response_type, response.json())
|
|
536
520
|
|
|
537
|
-
def _fetch(self, path: str, query:
|
|
521
|
+
def _fetch(self, path: str, query: dict[str, str] | None = None) -> list[JsonType]:
|
|
538
522
|
"Retrieves all results of a REST API v2 paginated result-set."
|
|
539
523
|
|
|
540
524
|
items: list[JsonType] = []
|
|
@@ -556,14 +540,14 @@ class ConfluenceSession:
|
|
|
556
540
|
|
|
557
541
|
return items
|
|
558
542
|
|
|
559
|
-
def _build_request(self, version: ConfluenceVersion, path: str, body: Any, response_type:
|
|
543
|
+
def _build_request(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T] | None) -> tuple[str, dict[str, str], bytes]:
|
|
560
544
|
"Generates URL, headers and raw payload for a typed request/response."
|
|
561
545
|
|
|
562
546
|
url = self._build_url(version, path)
|
|
563
547
|
headers = {"Content-Type": "application/json"}
|
|
564
548
|
if response_type is not None:
|
|
565
549
|
headers["Accept"] = "application/json"
|
|
566
|
-
data =
|
|
550
|
+
data = object_to_json_payload(body)
|
|
567
551
|
return url, headers, data
|
|
568
552
|
|
|
569
553
|
@overload
|
|
@@ -572,7 +556,7 @@ class ConfluenceSession:
|
|
|
572
556
|
@overload
|
|
573
557
|
def _post(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T]) -> T: ...
|
|
574
558
|
|
|
575
|
-
def _post(self, version: ConfluenceVersion, path: str, body: Any, response_type:
|
|
559
|
+
def _post(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T] | None) -> T | None:
|
|
576
560
|
"Creates a new object via Confluence REST API."
|
|
577
561
|
|
|
578
562
|
url, headers, data = self._build_request(version, path, body, response_type)
|
|
@@ -586,7 +570,7 @@ class ConfluenceSession:
|
|
|
586
570
|
@overload
|
|
587
571
|
def _put(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T]) -> T: ...
|
|
588
572
|
|
|
589
|
-
def _put(self, version: ConfluenceVersion, path: str, body: Any, response_type:
|
|
573
|
+
def _put(self, version: ConfluenceVersion, path: str, body: Any, response_type: type[T] | None) -> T | None:
|
|
590
574
|
"Updates an existing object via Confluence REST API."
|
|
591
575
|
|
|
592
576
|
url, headers, data = self._build_request(version, path, body, response_type)
|
|
@@ -638,7 +622,7 @@ class ConfluenceSession:
|
|
|
638
622
|
|
|
639
623
|
return id
|
|
640
624
|
|
|
641
|
-
def get_space_id(self, *, space_id:
|
|
625
|
+
def get_space_id(self, *, space_id: str | None = None, space_key: str | None = None) -> str | None:
|
|
642
626
|
"""
|
|
643
627
|
Coalesces a space ID or space key into a space ID, accounting for site default.
|
|
644
628
|
|
|
@@ -671,17 +655,17 @@ class ConfluenceSession:
|
|
|
671
655
|
if len(results) != 1:
|
|
672
656
|
raise ConfluenceError(f"no such attachment on page {page_id}: {filename}")
|
|
673
657
|
result = typing.cast(dict[str, JsonType], results[0])
|
|
674
|
-
return
|
|
658
|
+
return json_to_object(ConfluenceAttachment, result)
|
|
675
659
|
|
|
676
660
|
def upload_attachment(
|
|
677
661
|
self,
|
|
678
662
|
page_id: str,
|
|
679
663
|
attachment_name: str,
|
|
680
664
|
*,
|
|
681
|
-
attachment_path:
|
|
682
|
-
raw_data:
|
|
683
|
-
content_type:
|
|
684
|
-
comment:
|
|
665
|
+
attachment_path: Path | None = None,
|
|
666
|
+
raw_data: bytes | None = None,
|
|
667
|
+
content_type: str | None = None,
|
|
668
|
+
comment: str | None = None,
|
|
685
669
|
force: bool = False,
|
|
686
670
|
) -> None:
|
|
687
671
|
"""
|
|
@@ -739,7 +723,7 @@ class ConfluenceSession:
|
|
|
739
723
|
|
|
740
724
|
if attachment_path is not None:
|
|
741
725
|
with open(attachment_path, "rb") as attachment_file:
|
|
742
|
-
file_to_upload: dict[str, tuple[
|
|
726
|
+
file_to_upload: dict[str, tuple[str | None, Any, str, dict[str, str]]] = {
|
|
743
727
|
"comment": (
|
|
744
728
|
None,
|
|
745
729
|
comment,
|
|
@@ -826,8 +810,8 @@ class ConfluenceSession:
|
|
|
826
810
|
self,
|
|
827
811
|
title: str,
|
|
828
812
|
*,
|
|
829
|
-
space_id:
|
|
830
|
-
space_key:
|
|
813
|
+
space_id: str | None = None,
|
|
814
|
+
space_key: str | None = None,
|
|
831
815
|
) -> ConfluencePageProperties:
|
|
832
816
|
"""
|
|
833
817
|
Looks up a Confluence wiki page ID by title.
|
|
@@ -852,7 +836,7 @@ class ConfluenceSession:
|
|
|
852
836
|
if len(results) != 1:
|
|
853
837
|
raise ConfluenceError(f"unique page not found with title: {title}")
|
|
854
838
|
|
|
855
|
-
page =
|
|
839
|
+
page = json_to_object(ConfluencePageProperties, results[0])
|
|
856
840
|
return page
|
|
857
841
|
|
|
858
842
|
def get_page(self, page_id: str) -> ConfluencePage:
|
|
@@ -946,7 +930,7 @@ class ConfluenceSession:
|
|
|
946
930
|
url = self._build_url(ConfluenceVersion.VERSION_2, path)
|
|
947
931
|
response = self.session.post(
|
|
948
932
|
url,
|
|
949
|
-
data=
|
|
933
|
+
data=object_to_json_payload(request),
|
|
950
934
|
headers={
|
|
951
935
|
"Content-Type": "application/json",
|
|
952
936
|
"Accept": "application/json",
|
|
@@ -954,7 +938,7 @@ class ConfluenceSession:
|
|
|
954
938
|
verify=True,
|
|
955
939
|
)
|
|
956
940
|
response.raise_for_status()
|
|
957
|
-
return
|
|
941
|
+
return json_to_object(ConfluencePage, response.json())
|
|
958
942
|
|
|
959
943
|
def delete_page(self, page_id: str, *, purge: bool = False) -> None:
|
|
960
944
|
"""
|
|
@@ -984,9 +968,9 @@ class ConfluenceSession:
|
|
|
984
968
|
self,
|
|
985
969
|
title: str,
|
|
986
970
|
*,
|
|
987
|
-
space_id:
|
|
988
|
-
space_key:
|
|
989
|
-
) ->
|
|
971
|
+
space_id: str | None = None,
|
|
972
|
+
space_key: str | None = None,
|
|
973
|
+
) -> str | None:
|
|
990
974
|
"""
|
|
991
975
|
Checks if a Confluence page exists with the given title.
|
|
992
976
|
|
|
@@ -1016,7 +1000,7 @@ class ConfluenceSession:
|
|
|
1016
1000
|
)
|
|
1017
1001
|
response.raise_for_status()
|
|
1018
1002
|
data = typing.cast(dict[str, JsonType], response.json())
|
|
1019
|
-
results =
|
|
1003
|
+
results = json_to_object(list[ConfluencePageProperties], data["results"])
|
|
1020
1004
|
|
|
1021
1005
|
if len(results) == 1:
|
|
1022
1006
|
return results[0].id
|
|
@@ -1051,7 +1035,7 @@ class ConfluenceSession:
|
|
|
1051
1035
|
|
|
1052
1036
|
path = f"/pages/{page_id}/labels"
|
|
1053
1037
|
results = self._fetch(path)
|
|
1054
|
-
return
|
|
1038
|
+
return json_to_object(list[ConfluenceIdentifiedLabel], results)
|
|
1055
1039
|
|
|
1056
1040
|
def add_labels(self, page_id: str, labels: list[ConfluenceLabel]) -> None:
|
|
1057
1041
|
"""
|
|
@@ -1113,7 +1097,7 @@ class ConfluenceSession:
|
|
|
1113
1097
|
|
|
1114
1098
|
path = f"/pages/{page_id}/properties"
|
|
1115
1099
|
results = self._fetch(path)
|
|
1116
|
-
return
|
|
1100
|
+
return json_to_object(list[ConfluenceIdentifiedContentProperty], results)
|
|
1117
1101
|
|
|
1118
1102
|
def add_content_property_to_page(self, page_id: str, property: ConfluenceContentProperty) -> ConfluenceIdentifiedContentProperty:
|
|
1119
1103
|
"""
|
md2conf/collection.py
CHANGED
|
@@ -7,7 +7,7 @@ Copyright 2022-2025, Levente Hunyadi
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from typing import Generic, Iterable,
|
|
10
|
+
from typing import Generic, Iterable, TypeVar
|
|
11
11
|
|
|
12
12
|
from .metadata import ConfluencePageMetadata
|
|
13
13
|
|
|
@@ -27,7 +27,7 @@ class KeyValueCollection(Generic[K, V]):
|
|
|
27
27
|
def add(self, key: K, data: V) -> None:
|
|
28
28
|
self._collection[key] = data
|
|
29
29
|
|
|
30
|
-
def get(self, key: K) ->
|
|
30
|
+
def get(self, key: K) -> V | None:
|
|
31
31
|
return self._collection.get(key)
|
|
32
32
|
|
|
33
33
|
def items(self) -> Iterable[tuple[K, V]]:
|
md2conf/converter.py
CHANGED
|
@@ -16,12 +16,11 @@ import uuid
|
|
|
16
16
|
from abc import ABC, abstractmethod
|
|
17
17
|
from dataclasses import dataclass
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import ClassVar, Literal
|
|
19
|
+
from typing import ClassVar, Literal
|
|
20
20
|
from urllib.parse import ParseResult, quote_plus, urlparse
|
|
21
21
|
|
|
22
22
|
import lxml.etree as ET
|
|
23
|
-
from
|
|
24
|
-
from strong_typing.exception import JsonTypeError
|
|
23
|
+
from cattrs import BaseValidationError
|
|
25
24
|
|
|
26
25
|
from . import drawio, mermaid
|
|
27
26
|
from .collection import ConfluencePageCollection
|
|
@@ -35,6 +34,7 @@ from .markdown import markdown_to_html
|
|
|
35
34
|
from .mermaid import MermaidConfigProperties
|
|
36
35
|
from .metadata import ConfluenceSiteMetadata
|
|
37
36
|
from .scanner import MermaidScanner, ScannedDocument, Scanner
|
|
37
|
+
from .serializer import JsonType
|
|
38
38
|
from .toc import TableOfContentsBuilder
|
|
39
39
|
from .uri import is_absolute_url, to_uuid_urn
|
|
40
40
|
from .xml import element_to_text
|
|
@@ -202,7 +202,7 @@ class NodeVisitor(ABC):
|
|
|
202
202
|
self.visit(source)
|
|
203
203
|
|
|
204
204
|
@abstractmethod
|
|
205
|
-
def transform(self, child: ElementType) ->
|
|
205
|
+
def transform(self, child: ElementType) -> ElementType | None: ...
|
|
206
206
|
|
|
207
207
|
|
|
208
208
|
def title_to_identifier(title: str) -> str:
|
|
@@ -273,11 +273,11 @@ class ImageAttributes:
|
|
|
273
273
|
"""
|
|
274
274
|
|
|
275
275
|
context: FormattingContext
|
|
276
|
-
width:
|
|
277
|
-
height:
|
|
278
|
-
alt:
|
|
279
|
-
title:
|
|
280
|
-
caption:
|
|
276
|
+
width: int | None
|
|
277
|
+
height: int | None
|
|
278
|
+
alt: str | None
|
|
279
|
+
title: str | None
|
|
280
|
+
caption: str | None
|
|
281
281
|
alignment: ImageAlignment = ImageAlignment.CENTER
|
|
282
282
|
|
|
283
283
|
def __post_init__(self) -> None:
|
|
@@ -374,13 +374,13 @@ class ConfluenceConverterOptions:
|
|
|
374
374
|
@dataclass
|
|
375
375
|
class ImageData:
|
|
376
376
|
path: Path
|
|
377
|
-
description:
|
|
377
|
+
description: str | None = None
|
|
378
378
|
|
|
379
379
|
|
|
380
380
|
@dataclass
|
|
381
381
|
class EmbeddedFileData:
|
|
382
382
|
data: bytes
|
|
383
|
-
description:
|
|
383
|
+
description: str | None = None
|
|
384
384
|
|
|
385
385
|
|
|
386
386
|
@dataclass
|
|
@@ -506,7 +506,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
506
506
|
else:
|
|
507
507
|
raise DocumentError(msg)
|
|
508
508
|
|
|
509
|
-
def _transform_link(self, anchor: ElementType) ->
|
|
509
|
+
def _transform_link(self, anchor: ElementType) -> ElementType | None:
|
|
510
510
|
"""
|
|
511
511
|
Transforms links (HTML anchor `<a>`).
|
|
512
512
|
|
|
@@ -559,7 +559,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
559
559
|
else:
|
|
560
560
|
return self._transform_attachment_link(anchor, absolute_path)
|
|
561
561
|
|
|
562
|
-
def _transform_page_link(self, anchor: ElementType, relative_url: ParseResult, absolute_path: Path) ->
|
|
562
|
+
def _transform_page_link(self, anchor: ElementType, relative_url: ParseResult, absolute_path: Path) -> ElementType | None:
|
|
563
563
|
"""
|
|
564
564
|
Transforms links to other Markdown documents (Confluence pages).
|
|
565
565
|
"""
|
|
@@ -596,7 +596,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
596
596
|
anchor.set("href", transformed_url.geturl())
|
|
597
597
|
return None
|
|
598
598
|
|
|
599
|
-
def _transform_attachment_link(self, anchor: ElementType, absolute_path: Path) ->
|
|
599
|
+
def _transform_attachment_link(self, anchor: ElementType, absolute_path: Path) -> ElementType | None:
|
|
600
600
|
"""
|
|
601
601
|
Transforms links to document binaries such as PDF, DOCX or XLSX.
|
|
602
602
|
"""
|
|
@@ -713,7 +713,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
713
713
|
else:
|
|
714
714
|
raise DocumentError(msg)
|
|
715
715
|
|
|
716
|
-
def _verify_image_path(self, path: Path) ->
|
|
716
|
+
def _verify_image_path(self, path: Path) -> Path | None:
|
|
717
717
|
"Checks whether an image path is safe to use."
|
|
718
718
|
|
|
719
719
|
# resolve relative path into absolute path w.r.t. base dir
|
|
@@ -905,12 +905,12 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
905
905
|
AC_ELEM("plain-text-body", ET.CDATA(content)),
|
|
906
906
|
)
|
|
907
907
|
|
|
908
|
-
def _extract_mermaid_config(self, content: str) ->
|
|
908
|
+
def _extract_mermaid_config(self, content: str) -> MermaidConfigProperties | None:
|
|
909
909
|
"""Extract scale from Mermaid YAML front matter configuration."""
|
|
910
910
|
try:
|
|
911
911
|
properties = MermaidScanner().read(content)
|
|
912
912
|
return properties.config
|
|
913
|
-
except
|
|
913
|
+
except BaseValidationError as ex:
|
|
914
914
|
LOGGER.warning("Failed to extract Mermaid properties: %s", ex)
|
|
915
915
|
return None
|
|
916
916
|
|
|
@@ -1558,7 +1558,7 @@ class ConfluenceStorageFormatConverter(NodeVisitor):
|
|
|
1558
1558
|
return AC_ELEM("task-list", {}, *tasks)
|
|
1559
1559
|
|
|
1560
1560
|
@override
|
|
1561
|
-
def transform(self, child: ElementType) ->
|
|
1561
|
+
def transform(self, child: ElementType) -> ElementType | None:
|
|
1562
1562
|
"""
|
|
1563
1563
|
Transforms an HTML element tree obtained from a Markdown document into a Confluence Storage Format element tree.
|
|
1564
1564
|
"""
|
|
@@ -1742,9 +1742,9 @@ class ConversionError(RuntimeError):
|
|
|
1742
1742
|
class ConfluenceDocument:
|
|
1743
1743
|
"Encapsulates an element tree for a Confluence document created by parsing a Markdown document."
|
|
1744
1744
|
|
|
1745
|
-
title:
|
|
1746
|
-
labels:
|
|
1747
|
-
properties:
|
|
1745
|
+
title: str | None
|
|
1746
|
+
labels: list[str] | None
|
|
1747
|
+
properties: dict[str, JsonType] | None
|
|
1748
1748
|
|
|
1749
1749
|
links: list[str]
|
|
1750
1750
|
images: list[ImageData]
|
|
@@ -1852,7 +1852,7 @@ class ConfluenceDocument:
|
|
|
1852
1852
|
return elements_to_string(self.root)
|
|
1853
1853
|
|
|
1854
1854
|
|
|
1855
|
-
def attachment_name(ref:
|
|
1855
|
+
def attachment_name(ref: Path | str) -> str:
|
|
1856
1856
|
"""
|
|
1857
1857
|
Safe name for use with attachment uploads.
|
|
1858
1858
|
|