markdown-to-confluence 0.4.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markdown-to-confluence
3
- Version: 0.4.7
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.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.8
30
- Requires-Dist: pymdown-extensions>=10.16
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; python_version >= "3.10"
34
- Requires-Dist: typing-extensions>=4.14; python_version < "3.12"
32
+ Requires-Dist: truststore>=0.10
33
+ Requires-Dist: typing-extensions>=4.15; python_version < "3.12"
35
34
  Provides-Extra: dev
36
- Requires-Dist: markdown_doc>=0.1.4; python_version >= "3.10" and extra == "dev"
37
- Requires-Dist: types-lxml>=2025.3.30; extra == "dev"
38
- Requires-Dist: types-markdown>=3.8; extra == "dev"
35
+ Requires-Dist: markdown_doc>=0.1.5; extra == "dev"
36
+ Requires-Dist: types-lxml>=2025.8.25; extra == "dev"
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
- Requires-Dist: mypy>=1.16; extra == "dev"
42
- Requires-Dist: ruff>=0.12; extra == "dev"
40
+ Requires-Dist: mypy>=1.18; extra == "dev"
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
@@ -425,9 +424,9 @@ Use the pseudo-language `csf` in a Markdown code block to pass content directly
425
424
 
426
425
  ### Ignoring files
427
426
 
428
- Skip files in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax (and constraints) of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
427
+ Skip files and subdirectories in a directory with rules defined in `.mdignore`. Each rule should occupy a single line. Rules follow the syntax (and constraints) of [fnmatch](https://docs.python.org/3/library/fnmatch.html#fnmatch.fnmatch). Specifically, `?` matches any single character, and `*` matches zero or more characters. For example, use `up-*.md` to exclude Markdown files that start with `up-`. Lines that start with `#` are treated as comments.
429
428
 
430
- Files that don't have the extension `*.md` are skipped automatically. Hidden directories (whose name starts with `.`) are not recursed into.
429
+ Files that don't have the extension `*.md` are skipped automatically. Hidden directories (whose name starts with `.`) are not recursed into. To skip an entire directory, add the name of the directory without a trailing `/`.
431
430
 
432
431
  Relative paths to items in a nested directory are not supported. You must put `.mdignore` in the same directory where the items to be skipped reside.
433
432
 
@@ -501,7 +500,7 @@ You can add [Mermaid diagrams](https://mermaid.js.org/) to your Markdown documen
501
500
 
502
501
  *md2conf* offers two options to publish the diagram:
503
502
 
504
- 1. Pre-render into an image (command-line option `--render-mermaid`). The source file or code block is interpreted by and converted into a PNG or SVG image with the Mermaid diagram utility [mermaid-cli](https://github.com/mermaid-js/mermaid-cli). The generated image is then uploaded to Confluence as an attachment to the page. This is the approach we use and support.
503
+ 1. Pre-render into an image (command-line option `--render-mermaid`). The source file or code block is interpreted by and converted into a PNG or SVG image with the Mermaid diagram utility [mermaid-cli](https://github.com/mermaid-js/mermaid-cli). The generated image is then uploaded to Confluence as an attachment to the page.
505
504
  2. Display on demand (command-line option `--no-render-mermaid`). The code block is transformed into a [diagram macro](https://stratus-addons.atlassian.net/wiki/spaces/MDFC/overview), which is processed by Confluence. You need a separate [marketplace app](https://marketplace.atlassian.com/apps/1226567/mermaid-diagrams-for-confluence) to turn macro definitions into images when a Confluence page is visited.
506
505
 
507
506
  If you are running into issues with the pre-rendering approach (e.g. misaligned labels in the generated image), verify if `mermaid-cli` can process the Mermaid source:
@@ -512,6 +511,14 @@ mmdc -i sample.mmd -o sample.png -b transparent --scale 2
512
511
 
513
512
  Ensure that `mermaid-cli` is set up, refer to *Installation* for instructions.
514
513
 
514
+ Note that `mermaid-cli` has some implicit dependencies (e.g. a headless browser) that may not be immediately available in a CI/CD environment such as GitHub Actions. Refer to the `Dockerfile` in the *md2conf* project root, or [mermaid-cli documentation](https://github.com/mermaid-js/mermaid-cli) on how to install these dependencies such as a `chromium-browser` and various fonts.
515
+
516
+ ### Alignment
517
+
518
+ You can configure diagram and image alignment using the JSON/YAML front-matter attribute `alignment` or the command-line argument of the same name. Possible values are `center` (default), `left` and `right`. The value configured in the Markdown file front-matter takes precedence.
519
+
520
+ Unfortunately, not every third-party app supports every alignment variant. For example, the draw\.io marketplace app supports left and center but not right alignment; and diagrams produced by the Mermaid marketplace app are always centered, ignoring the setting for alignment.
521
+
515
522
  ### Links to attachments
516
523
 
517
524
  If *md2conf* encounters a Markdown link that points to a file in the directory hierarchy being synchronized, it automatically uploads the file as an attachment to the Confluence page. Activating the link in Confluence downloads the file. Typical examples include PDFs (`*.pdf`), word processor documents (`*.docx`), spreadsheets (`*.xlsx`), plain text files (`*.txt`) or logs (`*.log`). The MIME type is set based on the file type.
@@ -586,6 +593,12 @@ options:
586
593
  --use-panel Transform admonitions and alerts into a Confluence custom panel.
587
594
  ```
588
595
 
596
+ ### Confluence REST API v1 vs. v2
597
+
598
+ *md2conf* version 0.3.0 has switched to using [Confluence REST API v2](https://developer.atlassian.com/cloud/confluence/rest/v2/) for API calls such as retrieving current page content. Earlier versions used [Confluence REST API v1](https://developer.atlassian.com/cloud/confluence/rest/v1/) exclusively. Unfortunately, Atlassian has decommissioned Confluence REST API v1 for several endpoints in Confluence Cloud as of due date March 31, 2025, and we don't have access to an environment where we could test retired v1 endpoints.
599
+
600
+ If you are restricted to an environment with Confluence REST API v1, we recommend *md2conf* [version 0.2.7](https://pypi.org/project/markdown-to-confluence/0.2.7/). Even though we don't actively support it, we are not aware of any major issues, making it a viable option in an on-premise environment with only Confluence REST API v1 support.
601
+
589
602
  ### Using the Docker container
590
603
 
591
604
  You can run the Docker container via `docker run` or via `Dockerfile`. Either can accept the environment variables or arguments similar to the Python options. The final argument `./` corresponds to `mdpath` in the command-line utility.
@@ -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.4.7"
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, 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]]: