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.
- {markdown_to_confluence-0.4.7.dist-info → markdown_to_confluence-0.5.0.dist-info}/METADATA +30 -17
- 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 +43 -35
- 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.7.dist-info/RECORD +0 -34
- {markdown_to_confluence-0.4.7.dist-info → markdown_to_confluence-0.5.0.dist-info}/WHEEL +0 -0
- {markdown_to_confluence-0.4.7.dist-info → markdown_to_confluence-0.5.0.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.4.7.dist-info → markdown_to_confluence-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.4.7.dist-info → markdown_to_confluence-0.5.0.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.4.7.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
|
|
34
|
-
Requires-Dist: typing-extensions>=4.
|
|
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.
|
|
37
|
-
Requires-Dist: types-lxml>=2025.
|
|
38
|
-
Requires-Dist: types-markdown>=3.
|
|
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.
|
|
42
|
-
Requires-Dist: ruff>=0.
|
|
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.
|
|
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.
|
|
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]]:
|