markdown-to-confluence 0.4.8__py3-none-any.whl → 0.5.1__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.4.8
3
+ Version: 0.5.1
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,33 @@ 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.9
23
+ Requires-Python: >=3.10
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
- Requires-Dist: certifi>=2025.8.3; python_version < "3.10"
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.9
30
- Requires-Dist: pymdown-extensions>=10.16
28
+ Requires-Dist: markdown>=3.10
29
+ Requires-Dist: orjson>=3.11
30
+ Requires-Dist: pymdown-extensions>=10.17
31
31
  Requires-Dist: PyYAML>=6.0
32
32
  Requires-Dist: requests>=2.32
33
- Requires-Dist: truststore>=0.10; python_version >= "3.10"
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; python_version >= "3.10" and extra == "dev"
36
+ Requires-Dist: markdown_doc>=0.1.5; extra == "dev"
37
37
  Requires-Dist: types-lxml>=2025.8.25; extra == "dev"
38
- Requires-Dist: types-markdown>=3.9; extra == "dev"
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
41
  Requires-Dist: mypy>=1.18; extra == "dev"
42
- Requires-Dist: ruff>=0.13; extra == "dev"
42
+ Requires-Dist: ruff>=0.14; extra == "dev"
43
43
  Provides-Extra: formulas
44
44
  Requires-Dist: matplotlib>=3.9; extra == "formulas"
45
45
  Dynamic: license-file
@@ -0,0 +1,35 @@
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
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=JrBj8Z9zP8PjBeVAlRRqnMKDoz6IvkRbTd6K-JgFVow,1757
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.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,,
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.4.8"
8
+ __version__ = "0.5.1"
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, Optional, Sequence, Union
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: Optional[str]
31
- path: Optional[str]
32
- api_url: Optional[str]
33
- username: Optional[str]
34
- api_key: Optional[str]
35
- space: Optional[str]
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: Optional[str]
39
+ root_page: str | None
40
40
  keep_hierarchy: bool
41
- generated_by: Optional[str]
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: Union[None, str, Sequence[Any]],
62
- option_string: Optional[str] = None,
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: Optional[str],
77
+ usage: str | None,
78
78
  actions: Iterable[argparse.Action],
79
79
  groups: Iterable[argparse._MutuallyExclusiveGroup], # pyright: ignore[reportPrivateUsage]
80
- prefix: Optional[str],
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, Optional, TypeVar, overload
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 _json_to_object(
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: Optional[type[T]], response: requests.Response) -> Optional[T]:
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 _json_to_object(response_type, response.json())
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: Optional[datetime.datetime] = None
159
- message: Optional[str] = None
160
- authorId: Optional[str] = None
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: Optional[str]
172
+ title: str | None
186
173
  createdAt: datetime.datetime
187
174
  pageId: str
188
175
  mediaType: str
189
- mediaTypeDescription: Optional[str]
190
- comment: Optional[str]
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: Optional[str]
222
- parentType: Optional[ConfluencePageParentContentType]
223
- position: Optional[int]
208
+ parentId: str | None
209
+ parentType: ConfluencePageParentContentType | None
210
+ position: int | None
224
211
  authorId: str
225
212
  ownerId: str
226
- lastOwnerId: Optional[str]
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: Optional[ConfluenceStatus]
333
- title: Optional[str]
334
- parentId: Optional[str]
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
- if sys.version_info >= (3, 10):
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: Optional["ConfluenceSession"] = None
370
+ session: "ConfluenceSession | None" = None
387
371
 
388
- def __init__(self, properties: Optional[ConfluenceConnectionProperties] = None) -> None:
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: Optional[type[BaseException]],
419
- exc_val: Optional[BaseException],
420
- exc_tb: Optional[TracebackType],
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: Optional[str],
448
- domain: Optional[str],
449
- base_path: Optional[str],
450
- space_key: Optional[str],
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: Optional[dict[str, str]] = None,
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: Optional[dict[str, str]] = None,
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 _json_to_object(response_type, response.json())
519
+ return json_to_object(response_type, response.json())
536
520
 
537
- def _fetch(self, path: str, query: Optional[dict[str, str]] = None) -> list[JsonType]:
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: Optional[type[T]]) -> tuple[str, dict[str, str], bytes]:
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 = json_dump_string(object_to_json(body)).encode("utf-8")
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: Optional[type[T]]) -> Optional[T]:
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: Optional[type[T]]) -> Optional[T]:
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: Optional[str] = None, space_key: Optional[str] = None) -> Optional[str]:
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 _json_to_object(ConfluenceAttachment, result)
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: Optional[Path] = None,
682
- raw_data: Optional[bytes] = None,
683
- content_type: Optional[str] = None,
684
- comment: Optional[str] = None,
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[Optional[str], Any, str, dict[str, str]]] = {
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: Optional[str] = None,
830
- space_key: Optional[str] = None,
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 = _json_to_object(ConfluencePageProperties, results[0])
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=json_dump_string(object_to_json(request)).encode("utf-8"),
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 _json_to_object(ConfluencePage, response.json())
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: Optional[str] = None,
988
- space_key: Optional[str] = None,
989
- ) -> Optional[str]:
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 = _json_to_object(list[ConfluencePageProperties], data["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 _json_to_object(list[ConfluenceIdentifiedLabel], results)
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 _json_to_object(list[ConfluenceIdentifiedContentProperty], results)
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, Optional, TypeVar
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) -> Optional[V]:
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, Optional, Union
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 strong_typing.core import JsonType
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) -> Optional[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: Optional[int]
277
- height: Optional[int]
278
- alt: Optional[str]
279
- title: Optional[str]
280
- caption: Optional[str]
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: Optional[str] = None
377
+ description: str | None = None
378
378
 
379
379
 
380
380
  @dataclass
381
381
  class EmbeddedFileData:
382
382
  data: bytes
383
- description: Optional[str] = None
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) -> Optional[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) -> Optional[ElementType]:
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) -> Optional[ElementType]:
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) -> Optional[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) -> Optional[MermaidConfigProperties]:
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 JsonTypeError as ex:
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) -> Optional[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: Optional[str]
1746
- labels: Optional[list[str]]
1747
- properties: Optional[dict[str, JsonType]]
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: Union[Path, str]) -> str:
1855
+ def attachment_name(ref: Path | str) -> str:
1856
1856
  """
1857
1857
  Safe name for use with attachment uploads.
1858
1858