markdown-to-confluence 0.5.4__py3-none-any.whl → 0.5.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {markdown_to_confluence-0.5.4.dist-info → markdown_to_confluence-0.5.5.dist-info}/METADATA +95 -53
- {markdown_to_confluence-0.5.4.dist-info → markdown_to_confluence-0.5.5.dist-info}/RECORD +29 -27
- {markdown_to_confluence-0.5.4.dist-info → markdown_to_confluence-0.5.5.dist-info}/WHEEL +1 -1
- md2conf/__init__.py +1 -1
- md2conf/__main__.py +23 -172
- md2conf/api.py +32 -67
- md2conf/attachment.py +4 -3
- md2conf/clio.py +226 -0
- md2conf/compatibility.py +5 -0
- md2conf/converter.py +235 -143
- md2conf/csf.py +89 -9
- md2conf/drawio/render.py +2 -0
- md2conf/frontmatter.py +18 -6
- md2conf/image.py +7 -5
- md2conf/latex.py +8 -1
- md2conf/markdown.py +68 -1
- md2conf/options.py +93 -24
- md2conf/plantuml/extension.py +1 -1
- md2conf/publisher.py +81 -16
- md2conf/reflection.py +74 -0
- md2conf/scanner.py +9 -5
- md2conf/serializer.py +12 -1
- md2conf/svg.py +5 -2
- md2conf/toc.py +1 -1
- md2conf/xml.py +45 -0
- {markdown_to_confluence-0.5.4.dist-info → markdown_to_confluence-0.5.5.dist-info}/entry_points.txt +0 -0
- {markdown_to_confluence-0.5.4.dist-info → markdown_to_confluence-0.5.5.dist-info}/licenses/LICENSE +0 -0
- {markdown_to_confluence-0.5.4.dist-info → markdown_to_confluence-0.5.5.dist-info}/top_level.txt +0 -0
- {markdown_to_confluence-0.5.4.dist-info → markdown_to_confluence-0.5.5.dist-info}/zip-safe +0 -0
md2conf/api.py
CHANGED
|
@@ -402,12 +402,7 @@ class ConfluenceAPI:
|
|
|
402
402
|
)
|
|
403
403
|
return self.session
|
|
404
404
|
|
|
405
|
-
def __exit__(
|
|
406
|
-
self,
|
|
407
|
-
exc_type: type[BaseException] | None,
|
|
408
|
-
exc_val: BaseException | None,
|
|
409
|
-
exc_tb: TracebackType | None,
|
|
410
|
-
) -> None:
|
|
405
|
+
def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> None:
|
|
411
406
|
"""
|
|
412
407
|
Closes an open connection.
|
|
413
408
|
"""
|
|
@@ -429,15 +424,7 @@ class ConfluenceSession:
|
|
|
429
424
|
_space_id_to_key: dict[str, str]
|
|
430
425
|
_space_key_to_id: dict[str, str]
|
|
431
426
|
|
|
432
|
-
def __init__(
|
|
433
|
-
self,
|
|
434
|
-
session: requests.Session,
|
|
435
|
-
*,
|
|
436
|
-
api_url: str | None,
|
|
437
|
-
domain: str | None,
|
|
438
|
-
base_path: str | None,
|
|
439
|
-
space_key: str | None,
|
|
440
|
-
) -> None:
|
|
427
|
+
def __init__(self, session: requests.Session, *, api_url: str | None, domain: str | None, base_path: str | None, space_key: str | None) -> None:
|
|
441
428
|
self.session = session
|
|
442
429
|
self._space_id_to_key = {}
|
|
443
430
|
self._space_key_to_id = {}
|
|
@@ -488,12 +475,7 @@ class ConfluenceSession:
|
|
|
488
475
|
self.session.close()
|
|
489
476
|
self.session = requests.Session()
|
|
490
477
|
|
|
491
|
-
def _build_url(
|
|
492
|
-
self,
|
|
493
|
-
version: ConfluenceVersion,
|
|
494
|
-
path: str,
|
|
495
|
-
query: dict[str, str] | None = None,
|
|
496
|
-
) -> str:
|
|
478
|
+
def _build_url(self, version: ConfluenceVersion, path: str, query: dict[str, str] | None = None) -> str:
|
|
497
479
|
"""
|
|
498
480
|
Builds a full URL for invoking the Confluence API.
|
|
499
481
|
|
|
@@ -506,14 +488,7 @@ class ConfluenceSession:
|
|
|
506
488
|
base_url = f"{self.api_url}{version.value}{path}"
|
|
507
489
|
return build_url(base_url, query)
|
|
508
490
|
|
|
509
|
-
def _get(
|
|
510
|
-
self,
|
|
511
|
-
version: ConfluenceVersion,
|
|
512
|
-
path: str,
|
|
513
|
-
response_type: type[T],
|
|
514
|
-
*,
|
|
515
|
-
query: dict[str, str] | None = None,
|
|
516
|
-
) -> T:
|
|
491
|
+
def _get(self, version: ConfluenceVersion, path: str, response_type: type[T], *, query: dict[str, str] | None = None) -> T:
|
|
517
492
|
"Executes an HTTP request via Confluence API."
|
|
518
493
|
|
|
519
494
|
url = self._build_url(version, path, query)
|
|
@@ -829,13 +804,7 @@ class ConfluenceSession:
|
|
|
829
804
|
LOGGER.info("Updating attachment: %s", attachment_id)
|
|
830
805
|
self._put(ConfluenceVersion.VERSION_1, path, request, None)
|
|
831
806
|
|
|
832
|
-
def get_page_properties_by_title(
|
|
833
|
-
self,
|
|
834
|
-
title: str,
|
|
835
|
-
*,
|
|
836
|
-
space_id: str | None = None,
|
|
837
|
-
space_key: str | None = None,
|
|
838
|
-
) -> ConfluencePageProperties:
|
|
807
|
+
def get_page_properties_by_title(self, title: str, *, space_id: str | None = None, space_key: str | None = None) -> ConfluencePageProperties:
|
|
839
808
|
"""
|
|
840
809
|
Looks up a Confluence wiki page ID by title.
|
|
841
810
|
|
|
@@ -916,14 +885,7 @@ class ConfluenceSession:
|
|
|
916
885
|
|
|
917
886
|
return self.get_page_properties(page_id).version.number
|
|
918
887
|
|
|
919
|
-
def update_page(
|
|
920
|
-
self,
|
|
921
|
-
page_id: str,
|
|
922
|
-
content: str,
|
|
923
|
-
*,
|
|
924
|
-
title: str,
|
|
925
|
-
version: int,
|
|
926
|
-
) -> None:
|
|
888
|
+
def update_page(self, page_id: str, content: str, *, title: str, version: int, message: str) -> None:
|
|
927
889
|
"""
|
|
928
890
|
Updates a page via the Confluence API.
|
|
929
891
|
|
|
@@ -939,35 +901,28 @@ class ConfluenceSession:
|
|
|
939
901
|
status=ConfluenceStatus.CURRENT,
|
|
940
902
|
title=title,
|
|
941
903
|
body=ConfluencePageBody(storage=ConfluencePageStorage(representation=ConfluenceRepresentation.STORAGE, value=content)),
|
|
942
|
-
version=ConfluenceContentVersion(number=version, minorEdit=True),
|
|
904
|
+
version=ConfluenceContentVersion(number=version, minorEdit=True, message=message),
|
|
943
905
|
)
|
|
944
906
|
LOGGER.info("Updating page: %s", page_id)
|
|
945
907
|
self._put(ConfluenceVersion.VERSION_2, path, request, None)
|
|
946
908
|
|
|
947
|
-
def create_page(
|
|
948
|
-
self,
|
|
949
|
-
parent_id: str,
|
|
950
|
-
title: str,
|
|
951
|
-
new_content: str,
|
|
952
|
-
) -> ConfluencePage:
|
|
909
|
+
def create_page(self, *, title: str, content: str, parent_id: str, space_id: str) -> ConfluencePage:
|
|
953
910
|
"""
|
|
954
911
|
Creates a new page via Confluence API.
|
|
955
912
|
"""
|
|
956
913
|
|
|
957
914
|
LOGGER.info("Creating page: %s", title)
|
|
958
915
|
|
|
959
|
-
parent_page = self.get_page_properties(parent_id)
|
|
960
|
-
|
|
961
916
|
path = "/pages/"
|
|
962
917
|
request = ConfluenceCreatePageRequest(
|
|
963
|
-
spaceId=
|
|
918
|
+
spaceId=space_id,
|
|
964
919
|
status=ConfluenceStatus.CURRENT,
|
|
965
920
|
title=title,
|
|
966
921
|
parentId=parent_id,
|
|
967
922
|
body=ConfluencePageBody(
|
|
968
923
|
storage=ConfluencePageStorage(
|
|
969
924
|
representation=ConfluenceRepresentation.STORAGE,
|
|
970
|
-
value=
|
|
925
|
+
value=content,
|
|
971
926
|
)
|
|
972
927
|
),
|
|
973
928
|
)
|
|
@@ -1009,23 +964,15 @@ class ConfluenceSession:
|
|
|
1009
964
|
response = self.session.delete(url, verify=True)
|
|
1010
965
|
response.raise_for_status()
|
|
1011
966
|
|
|
1012
|
-
def page_exists(
|
|
1013
|
-
self,
|
|
1014
|
-
title: str,
|
|
1015
|
-
*,
|
|
1016
|
-
space_id: str | None = None,
|
|
1017
|
-
space_key: str | None = None,
|
|
1018
|
-
) -> str | None:
|
|
967
|
+
def page_exists(self, title: str, *, space_id: str | None = None) -> str | None:
|
|
1019
968
|
"""
|
|
1020
969
|
Checks if a Confluence page exists with the given title.
|
|
1021
970
|
|
|
1022
971
|
:param title: Page title. Pages in the same Confluence space must have a unique title.
|
|
1023
|
-
:param
|
|
1024
|
-
|
|
972
|
+
:param space_id: Identifies the Confluence space.
|
|
1025
973
|
:returns: Confluence page ID of a matching page (if found), or `None`.
|
|
1026
974
|
"""
|
|
1027
975
|
|
|
1028
|
-
space_id = self._get_space_id(space_id=space_id, space_key=space_key)
|
|
1029
976
|
path = "/pages"
|
|
1030
977
|
query = {"title": title}
|
|
1031
978
|
if space_id is not None:
|
|
@@ -1062,14 +1009,15 @@ class ConfluenceSession:
|
|
|
1062
1009
|
"""
|
|
1063
1010
|
|
|
1064
1011
|
parent_page = self.get_page_properties(parent_id)
|
|
1065
|
-
|
|
1012
|
+
space_id = parent_page.spaceId
|
|
1013
|
+
page_id = self.page_exists(title, space_id=space_id)
|
|
1066
1014
|
|
|
1067
1015
|
if page_id is not None:
|
|
1068
1016
|
LOGGER.debug("Retrieving existing page: %s", page_id)
|
|
1069
1017
|
return self.get_page(page_id)
|
|
1070
1018
|
else:
|
|
1071
1019
|
LOGGER.debug("Creating new page with title: %s", title)
|
|
1072
|
-
return self.create_page(
|
|
1020
|
+
return self.create_page(title=title, content="", parent_id=parent_id, space_id=space_id)
|
|
1073
1021
|
|
|
1074
1022
|
def get_labels(self, page_id: str) -> list[ConfluenceIdentifiedLabel]:
|
|
1075
1023
|
"""
|
|
@@ -1133,6 +1081,23 @@ class ConfluenceSession:
|
|
|
1133
1081
|
remove_labels.sort()
|
|
1134
1082
|
self.remove_labels(page_id, remove_labels)
|
|
1135
1083
|
|
|
1084
|
+
def get_content_property_for_page(self, page_id: str, key: str) -> ConfluenceIdentifiedContentProperty | None:
|
|
1085
|
+
"""
|
|
1086
|
+
Retrieves a content property for a Confluence page.
|
|
1087
|
+
|
|
1088
|
+
:param page_id: The Confluence page ID.
|
|
1089
|
+
:param key: The name of the property to fetch (with case-sensitive match).
|
|
1090
|
+
:returns: The content property value, or `None` if not found.
|
|
1091
|
+
"""
|
|
1092
|
+
|
|
1093
|
+
path = f"/pages/{page_id}/properties"
|
|
1094
|
+
results = self._fetch(path, query={"key": key})
|
|
1095
|
+
properties = json_to_object(list[ConfluenceIdentifiedContentProperty], results)
|
|
1096
|
+
if len(properties) == 1:
|
|
1097
|
+
return properties.pop()
|
|
1098
|
+
else:
|
|
1099
|
+
return None
|
|
1100
|
+
|
|
1136
1101
|
def get_content_properties_for_page(self, page_id: str) -> list[ConfluenceIdentifiedContentProperty]:
|
|
1137
1102
|
"""
|
|
1138
1103
|
Retrieves content properties for a Confluence page.
|
md2conf/attachment.py
CHANGED
|
@@ -40,6 +40,9 @@ class AttachmentCatalog:
|
|
|
40
40
|
self.embedded_files[filename] = data
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
_DISALLOWED_CHAR_REGEXP = re.compile(r"[^\-0-9A-Za-z_.]", re.UNICODE)
|
|
44
|
+
|
|
45
|
+
|
|
43
46
|
def attachment_name(ref: Path | str) -> str:
|
|
44
47
|
"""
|
|
45
48
|
Safe name for use with attachment uploads.
|
|
@@ -60,13 +63,11 @@ def attachment_name(ref: Path | str) -> str:
|
|
|
60
63
|
if path.drive or path.root:
|
|
61
64
|
raise ValueError(f"required: relative path; got: {ref}")
|
|
62
65
|
|
|
63
|
-
regexp = re.compile(r"[^\-0-9A-Za-z_.]", re.UNICODE)
|
|
64
|
-
|
|
65
66
|
def replace_part(part: str) -> str:
|
|
66
67
|
if part == "..":
|
|
67
68
|
return "PAR"
|
|
68
69
|
else:
|
|
69
|
-
return
|
|
70
|
+
return _DISALLOWED_CHAR_REGEXP.sub("_", part)
|
|
70
71
|
|
|
71
72
|
parts = [replace_part(p) for p in path.parts]
|
|
72
73
|
return Path(*parts).as_posix().replace("/", "_")
|
md2conf/clio.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Publish Markdown files to Confluence wiki.
|
|
3
|
+
|
|
4
|
+
Copyright 2022-2026, Levente Hunyadi
|
|
5
|
+
|
|
6
|
+
:see: https://github.com/hunyadi/md2conf
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from argparse import ArgumentParser, Namespace
|
|
10
|
+
from dataclasses import MISSING, Field, dataclass, fields, is_dataclass
|
|
11
|
+
from types import NoneType, UnionType
|
|
12
|
+
from typing import Any, Literal, TypeVar, cast, get_args, get_origin
|
|
13
|
+
|
|
14
|
+
from .compatibility import LiteralString
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseOption:
|
|
18
|
+
@staticmethod
|
|
19
|
+
def field_name() -> str:
|
|
20
|
+
return "argument"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class BooleanOption(BaseOption):
|
|
25
|
+
true_text: LiteralString
|
|
26
|
+
false_text: LiteralString
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def boolean_option(true_text: LiteralString, false_text: LiteralString) -> dict[str, Any]:
|
|
30
|
+
"Identifies a command-line argument as a boolean (on/off) flag."
|
|
31
|
+
|
|
32
|
+
return {BaseOption.field_name(): BooleanOption(true_text, false_text)}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ValueOption(BaseOption):
|
|
37
|
+
text: LiteralString
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def value_option(text: LiteralString) -> dict[str, Any]:
|
|
41
|
+
"Identifies a command-line argument as an option that assigns a value."
|
|
42
|
+
|
|
43
|
+
return {BaseOption.field_name(): ValueOption(text)}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CompositeOption(BaseOption):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def composite_option() -> dict[str, Any]:
|
|
51
|
+
"Identifies a command-line argument as a data-class that needs to be unnested."
|
|
52
|
+
|
|
53
|
+
return {BaseOption.field_name(): CompositeOption()}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
T = TypeVar("T")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _has_metadata(field: Field[Any]) -> bool:
|
|
60
|
+
return field.metadata.get(BaseOption.field_name()) is not None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _get_metadata(field: Field[Any], tp: type[T]) -> T | None:
|
|
64
|
+
attrs = field.metadata.get(BaseOption.field_name())
|
|
65
|
+
if attrs is None:
|
|
66
|
+
return None
|
|
67
|
+
elif isinstance(attrs, tp):
|
|
68
|
+
return attrs
|
|
69
|
+
else:
|
|
70
|
+
raise TypeError(f"expected: {tp.__name__}; got: {type(attrs).__name__}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _OptionTreeVisitor:
|
|
74
|
+
"Adds arguments to a command-line argument parser by recursively visiting fields of a composite type."
|
|
75
|
+
|
|
76
|
+
parser: ArgumentParser
|
|
77
|
+
prefixes: list[str]
|
|
78
|
+
|
|
79
|
+
def __init__(self, parser: ArgumentParser) -> None:
|
|
80
|
+
self.parser = parser
|
|
81
|
+
self.prefixes = []
|
|
82
|
+
|
|
83
|
+
def _get_arg_name(self, arg_name: str) -> str:
|
|
84
|
+
return f"--{'-'.join([*self.prefixes, arg_name])}"
|
|
85
|
+
|
|
86
|
+
def _get_field_name(self, field_name: str) -> str:
|
|
87
|
+
return "_".join([*self.prefixes, field_name])
|
|
88
|
+
|
|
89
|
+
def _add_value_field(self, field: Field[Any], field_type: Any) -> None:
|
|
90
|
+
arg_name = field.name.replace("_", "-")
|
|
91
|
+
value_opt = _get_metadata(field, ValueOption)
|
|
92
|
+
if value_opt is None:
|
|
93
|
+
return
|
|
94
|
+
help_text = value_opt.text
|
|
95
|
+
if field.default is not MISSING and field.default is not None:
|
|
96
|
+
help_text += f" (default: {field.default!s})"
|
|
97
|
+
if isinstance(field_type, type):
|
|
98
|
+
metavar = field_type.__name__.upper()
|
|
99
|
+
else:
|
|
100
|
+
metavar = None
|
|
101
|
+
self.parser.add_argument(
|
|
102
|
+
self._get_arg_name(arg_name),
|
|
103
|
+
dest=self._get_field_name(field.name),
|
|
104
|
+
default=field.default,
|
|
105
|
+
type=field_type,
|
|
106
|
+
help=help_text,
|
|
107
|
+
metavar=metavar,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _add_field_as_argument(self, field: Field[Any]) -> None:
|
|
111
|
+
arg_name = field.name.replace("_", "-")
|
|
112
|
+
if field.type is bool:
|
|
113
|
+
bool_opt = _get_metadata(field, BooleanOption)
|
|
114
|
+
if bool_opt is None:
|
|
115
|
+
return
|
|
116
|
+
true_text = bool_opt.true_text
|
|
117
|
+
if field.default is True:
|
|
118
|
+
true_text += " (default)"
|
|
119
|
+
self.parser.add_argument(
|
|
120
|
+
self._get_arg_name(arg_name),
|
|
121
|
+
dest=self._get_field_name(field.name),
|
|
122
|
+
action="store_true",
|
|
123
|
+
default=field.default,
|
|
124
|
+
help=true_text,
|
|
125
|
+
)
|
|
126
|
+
if arg_name.startswith("skip-"):
|
|
127
|
+
inverse_arg_name = "keep-" + arg_name.removeprefix("skip-")
|
|
128
|
+
elif arg_name.startswith("keep-"):
|
|
129
|
+
inverse_arg_name = "skip-" + arg_name.removeprefix("keep-")
|
|
130
|
+
else:
|
|
131
|
+
inverse_arg_name = f"no-{arg_name}"
|
|
132
|
+
false_text = bool_opt.false_text
|
|
133
|
+
if field.default is False:
|
|
134
|
+
false_text += " (default)"
|
|
135
|
+
self.parser.add_argument(
|
|
136
|
+
self._get_arg_name(inverse_arg_name),
|
|
137
|
+
dest=self._get_field_name(field.name),
|
|
138
|
+
action="store_false",
|
|
139
|
+
help=false_text,
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
origin = get_origin(field.type)
|
|
144
|
+
if origin is Literal:
|
|
145
|
+
value_opt = _get_metadata(field, ValueOption)
|
|
146
|
+
if value_opt is None:
|
|
147
|
+
return
|
|
148
|
+
value_text = value_opt.text
|
|
149
|
+
if field.default is not MISSING and field.default is not None:
|
|
150
|
+
value_text += f" (default: {field.default!s})"
|
|
151
|
+
self.parser.add_argument(
|
|
152
|
+
self._get_arg_name(arg_name),
|
|
153
|
+
dest=self._get_field_name(field.name),
|
|
154
|
+
choices=get_args(field.type),
|
|
155
|
+
default=field.default,
|
|
156
|
+
help=value_text,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
elif origin is UnionType:
|
|
160
|
+
union_types = list(get_args(field.type))
|
|
161
|
+
if len(union_types) != 2 or NoneType not in union_types:
|
|
162
|
+
raise TypeError(f"expected: `T` or `T | None` as argument type; got: {field.type}")
|
|
163
|
+
union_types.remove(NoneType)
|
|
164
|
+
required_type = union_types.pop()
|
|
165
|
+
|
|
166
|
+
self._add_value_field(field, required_type)
|
|
167
|
+
|
|
168
|
+
elif isinstance(field.type, type):
|
|
169
|
+
if hasattr(field.type, "__dataclass_fields__"):
|
|
170
|
+
composite_opt = _get_metadata(field, CompositeOption)
|
|
171
|
+
if composite_opt is None:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
self.prefixes.append(arg_name)
|
|
175
|
+
self.add_arguments(field.type)
|
|
176
|
+
self.prefixes.pop()
|
|
177
|
+
|
|
178
|
+
else:
|
|
179
|
+
self._add_value_field(field, field.type)
|
|
180
|
+
|
|
181
|
+
elif _has_metadata(field):
|
|
182
|
+
raise TypeError(f"expected: known argument type; got: {field.type}")
|
|
183
|
+
|
|
184
|
+
def add_arguments(self, options_type: type[Any]) -> None:
|
|
185
|
+
if is_dataclass(options_type):
|
|
186
|
+
for field in fields(options_type):
|
|
187
|
+
self._add_field_as_argument(field)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def add_arguments(parser: ArgumentParser, options_type: type[Any]) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Adds arguments to a command-line argument parser.
|
|
193
|
+
|
|
194
|
+
:param parser: A command-line argument parser.
|
|
195
|
+
:param options_type: A data-class type that encapsulates configuration options.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
_OptionTreeVisitor(parser).add_arguments(options_type)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _get_options(args: Namespace, options_type: type[T], prefixes: tuple[str, ...]) -> T:
|
|
202
|
+
params: dict[str, Any] = {}
|
|
203
|
+
if is_dataclass(options_type): # always true, condition included for type checkers
|
|
204
|
+
for field in fields(options_type):
|
|
205
|
+
field_prefixes = (*prefixes, field.name)
|
|
206
|
+
if isinstance(field.type, type) and is_dataclass(field.type):
|
|
207
|
+
params[field.name] = _get_options(args, field.type, field_prefixes)
|
|
208
|
+
else:
|
|
209
|
+
field_param = getattr(args, "_".join(field_prefixes), MISSING)
|
|
210
|
+
if field_param is not MISSING:
|
|
211
|
+
params[field.name] = field_param
|
|
212
|
+
return options_type(**params)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_options(args: Namespace, options_type: type[T]) -> T:
|
|
216
|
+
"""
|
|
217
|
+
Extracts configuration options from command-line arguments acquired by an argument parser.
|
|
218
|
+
|
|
219
|
+
:param args: Arguments acquired by a command-line argument parser.
|
|
220
|
+
:param options_type: A data-class type that encapsulates configuration options.
|
|
221
|
+
:returns: Configuration options as a data-class instance.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
if not is_dataclass(options_type):
|
|
225
|
+
raise TypeError(f"expected: data-class as argument target; got: {type(options_type).__name__}")
|
|
226
|
+
return _get_options(args, cast(type[T], options_type), ())
|
md2conf/compatibility.py
CHANGED
|
@@ -8,6 +8,11 @@ Copyright 2022-2026, Levente Hunyadi
|
|
|
8
8
|
|
|
9
9
|
import sys
|
|
10
10
|
|
|
11
|
+
if sys.version_info >= (3, 11):
|
|
12
|
+
from typing import LiteralString as LiteralString # noqa: F401
|
|
13
|
+
else:
|
|
14
|
+
from typing_extensions import LiteralString as LiteralString # noqa: F401
|
|
15
|
+
|
|
11
16
|
if sys.version_info >= (3, 12):
|
|
12
17
|
from typing import override as override # noqa: F401
|
|
13
18
|
else:
|