1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169 | """Transform maps."""
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from types import MappingProxyType
from typing import Any
from glom import A, Coalesce, Path, S, T, Val, assign
from comicbox.empty import is_empty
from comicbox.schemas.comicbox import ComicboxSchemaMixin
GLOBAL_SCOPE_PREFIX = "S.globals.comicbox"
@dataclass
class MetaSpec:
"""Define a key mapping and transform functions."""
key_map: Mapping[str, str | tuple[str, ...]]
spec: Callable | tuple | None = None
inherit_root_keypath: bool = True
assign_global: bool = False
def _path_str_from_tuple(head_keypath: str, tail_keypath: str) -> str:
return ".".join(tuple(filter(bool, (head_keypath, tail_keypath))))
def _path_from_tuple(head_keypath: str, tail_keypath: str) -> Path:
path_str = _path_str_from_tuple(head_keypath, tail_keypath)
return Path.from_text(path_str)
def _get_multi_values_spec(
source_root_path: Path | None, keypath: str
) -> tuple[str, Coalesce]:
path_parts = []
tail_path = Path.from_text(keypath)
if keypath.startswith(GLOBAL_SCOPE_PREFIX):
tail_path_parts = tail_path.values()
path = S.globals.comicbox
for part in tail_path_parts[3:]:
path = path[part]
else:
if source_root_path:
path_parts.append(source_root_path)
path_parts.append(tail_path)
path = Path(*path_parts)
# Don't know which of multiple values are critical so don't throw.
return keypath, Coalesce(path, skip=is_empty, default=None)
def _get_spec_source_values(
source_root_path_str: str, source_path_strs: tuple[str, ...] | str
) -> dict | Coalesce:
if isinstance(source_path_strs, tuple | list):
source_root_path = (
Path.from_text(source_root_path_str) if source_root_path_str else None
)
values = {}
for p in source_path_strs:
key, value = _get_multi_values_spec(source_root_path, p)
values[key] = value
else:
source_path = _path_from_tuple(source_root_path_str, source_path_strs)
# No default so it throws out of the current spec line.
values = Coalesce(source_path, skip=is_empty)
return values
def _get_tail_spec(
metaspec_spec: Any,
) -> filter:
tail_spec = metaspec_spec if isinstance(metaspec_spec, tuple) else (metaspec_spec,)
return filter(bool, tail_spec)
def _get_spec(
source_head: str,
source_keypaths: str | tuple[str, ...],
metaspec: MetaSpec,
dest_keypath: str,
) -> Coalesce:
spec = []
if values := _get_spec_source_values(source_head, source_keypaths):
spec.append(values)
if metaspec.spec:
tail_spec = _get_tail_spec(metaspec.spec)
spec.extend(tail_spec)
if metaspec.assign_global:
global_assign = (A.globals.comicbox, T[dest_keypath])
spec.extend(global_assign)
spec = spec[0] if len(spec) == 1 else tuple(spec)
# Trap errors to complete the spec
return Coalesce(spec, default=None)
def _create_spec(
dest_head: str,
source_head: str,
metaspec: MetaSpec,
dest_keypath: str,
source_keypaths: str | tuple[str, ...],
) -> tuple[str, Coalesce] | tuple[str, tuple]:
full_dest_path = _path_str_from_tuple(dest_head, dest_keypath)
if not full_dest_path:
return full_dest_path, ()
spec = _get_spec(source_head, source_keypaths, metaspec, dest_keypath)
return full_dest_path, spec
def _create_specs(
*args: MetaSpec,
dest_root_keypath: str = "",
source_root_keypath: str = "",
) -> MappingProxyType[str, Any]:
"""Create spec from metaspec map."""
specs = {}
for metaspec in args:
dest_head, source_head = (
(dest_root_keypath, source_root_keypath)
if metaspec.inherit_root_keypath and dest_root_keypath
else ("", "")
)
for dest_keypath, source_keypaths in metaspec.key_map.items():
full_dest_keypath, spec = _create_spec(
dest_head,
source_head,
metaspec,
dest_keypath,
source_keypaths,
)
if full_dest_keypath and spec:
# Have to to double assign when assigning actual glom structures
# They get evaluated or something.
# But it's in the spec creator so not a huge deal.
assign(specs, full_dest_keypath, None, missing=dict)
assign(specs, full_dest_keypath, Val(spec), missing=dict)
return MappingProxyType(specs)
def create_specs_to_comicbox(
*metaspecs: MetaSpec,
format_root_keypath: str = "",
comicbox_root_keypath: str = ComicboxSchemaMixin.ROOT_KEYPATH,
) -> MappingProxyType[str, dict[str, Coalesce] | Coalesce]:
"""Create to comicbox specs."""
return _create_specs(
*metaspecs,
dest_root_keypath=comicbox_root_keypath,
source_root_keypath=format_root_keypath,
)
def create_specs_from_comicbox(
*metaspecs: MetaSpec,
format_root_keypath: str = "",
comicbox_root_keypath: str = ComicboxSchemaMixin.ROOT_KEYPATH,
) -> MappingProxyType[
str, dict[str, dict[str, Coalesce]] | dict[str, Coalesce] | Coalesce
]:
"""Create from comicbox specs."""
return _create_specs(
*metaspecs,
dest_root_keypath=format_root_keypath,
source_root_keypath=comicbox_root_keypath,
)
|