audex 1.0.7a3__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.
- audex/__init__.py +9 -0
- audex/__main__.py +7 -0
- audex/cli/__init__.py +189 -0
- audex/cli/apis/__init__.py +12 -0
- audex/cli/apis/init/__init__.py +34 -0
- audex/cli/apis/init/gencfg.py +130 -0
- audex/cli/apis/init/setup.py +330 -0
- audex/cli/apis/init/vprgroup.py +125 -0
- audex/cli/apis/serve.py +141 -0
- audex/cli/args.py +356 -0
- audex/cli/exceptions.py +44 -0
- audex/cli/helper/__init__.py +0 -0
- audex/cli/helper/ansi.py +193 -0
- audex/cli/helper/display.py +288 -0
- audex/config/__init__.py +64 -0
- audex/config/core/__init__.py +30 -0
- audex/config/core/app.py +29 -0
- audex/config/core/audio.py +45 -0
- audex/config/core/logging.py +163 -0
- audex/config/core/session.py +11 -0
- audex/config/helper/__init__.py +1 -0
- audex/config/helper/client/__init__.py +1 -0
- audex/config/helper/client/http.py +28 -0
- audex/config/helper/client/websocket.py +21 -0
- audex/config/helper/provider/__init__.py +1 -0
- audex/config/helper/provider/dashscope.py +13 -0
- audex/config/helper/provider/unisound.py +18 -0
- audex/config/helper/provider/xfyun.py +23 -0
- audex/config/infrastructure/__init__.py +31 -0
- audex/config/infrastructure/cache.py +51 -0
- audex/config/infrastructure/database.py +48 -0
- audex/config/infrastructure/recorder.py +32 -0
- audex/config/infrastructure/store.py +19 -0
- audex/config/provider/__init__.py +18 -0
- audex/config/provider/transcription.py +109 -0
- audex/config/provider/vpr.py +99 -0
- audex/container.py +40 -0
- audex/entity/__init__.py +468 -0
- audex/entity/doctor.py +109 -0
- audex/entity/doctor.pyi +51 -0
- audex/entity/fields.py +401 -0
- audex/entity/segment.py +115 -0
- audex/entity/segment.pyi +38 -0
- audex/entity/session.py +133 -0
- audex/entity/session.pyi +47 -0
- audex/entity/utterance.py +142 -0
- audex/entity/utterance.pyi +48 -0
- audex/entity/vp.py +68 -0
- audex/entity/vp.pyi +35 -0
- audex/exceptions.py +157 -0
- audex/filters/__init__.py +692 -0
- audex/filters/generated/__init__.py +21 -0
- audex/filters/generated/doctor.py +987 -0
- audex/filters/generated/segment.py +723 -0
- audex/filters/generated/session.py +978 -0
- audex/filters/generated/utterance.py +939 -0
- audex/filters/generated/vp.py +815 -0
- audex/helper/__init__.py +1 -0
- audex/helper/hash.py +33 -0
- audex/helper/mixin.py +65 -0
- audex/helper/net.py +19 -0
- audex/helper/settings/__init__.py +830 -0
- audex/helper/settings/fields.py +317 -0
- audex/helper/stream.py +153 -0
- audex/injectors/__init__.py +1 -0
- audex/injectors/config.py +12 -0
- audex/injectors/lifespan.py +7 -0
- audex/lib/__init__.py +1 -0
- audex/lib/cache/__init__.py +383 -0
- audex/lib/cache/inmemory.py +513 -0
- audex/lib/database/__init__.py +83 -0
- audex/lib/database/sqlite.py +406 -0
- audex/lib/exporter.py +189 -0
- audex/lib/injectors/__init__.py +1 -0
- audex/lib/injectors/cache.py +25 -0
- audex/lib/injectors/container.py +47 -0
- audex/lib/injectors/exporter.py +26 -0
- audex/lib/injectors/recorder.py +33 -0
- audex/lib/injectors/server.py +17 -0
- audex/lib/injectors/session.py +18 -0
- audex/lib/injectors/sqlite.py +24 -0
- audex/lib/injectors/store.py +13 -0
- audex/lib/injectors/transcription.py +42 -0
- audex/lib/injectors/usb.py +12 -0
- audex/lib/injectors/vpr.py +65 -0
- audex/lib/injectors/wifi.py +7 -0
- audex/lib/recorder.py +844 -0
- audex/lib/repos/__init__.py +149 -0
- audex/lib/repos/container.py +23 -0
- audex/lib/repos/database/__init__.py +1 -0
- audex/lib/repos/database/sqlite.py +672 -0
- audex/lib/repos/decorators.py +74 -0
- audex/lib/repos/doctor.py +286 -0
- audex/lib/repos/segment.py +302 -0
- audex/lib/repos/session.py +285 -0
- audex/lib/repos/tables/__init__.py +70 -0
- audex/lib/repos/tables/doctor.py +137 -0
- audex/lib/repos/tables/segment.py +113 -0
- audex/lib/repos/tables/session.py +140 -0
- audex/lib/repos/tables/utterance.py +131 -0
- audex/lib/repos/tables/vp.py +102 -0
- audex/lib/repos/utterance.py +288 -0
- audex/lib/repos/vp.py +286 -0
- audex/lib/restful.py +251 -0
- audex/lib/server/__init__.py +97 -0
- audex/lib/server/auth.py +98 -0
- audex/lib/server/handlers.py +248 -0
- audex/lib/server/templates/index.html.j2 +226 -0
- audex/lib/server/templates/login.html.j2 +111 -0
- audex/lib/server/templates/static/script.js +68 -0
- audex/lib/server/templates/static/style.css +579 -0
- audex/lib/server/types.py +123 -0
- audex/lib/session.py +503 -0
- audex/lib/store/__init__.py +238 -0
- audex/lib/store/localfile.py +411 -0
- audex/lib/transcription/__init__.py +33 -0
- audex/lib/transcription/dashscope.py +525 -0
- audex/lib/transcription/events.py +62 -0
- audex/lib/usb.py +554 -0
- audex/lib/vpr/__init__.py +38 -0
- audex/lib/vpr/unisound/__init__.py +185 -0
- audex/lib/vpr/unisound/types.py +469 -0
- audex/lib/vpr/xfyun/__init__.py +483 -0
- audex/lib/vpr/xfyun/types.py +679 -0
- audex/lib/websocket/__init__.py +8 -0
- audex/lib/websocket/connection.py +485 -0
- audex/lib/websocket/pool.py +991 -0
- audex/lib/wifi.py +1146 -0
- audex/lifespan.py +75 -0
- audex/service/__init__.py +27 -0
- audex/service/decorators.py +73 -0
- audex/service/doctor/__init__.py +652 -0
- audex/service/doctor/const.py +36 -0
- audex/service/doctor/exceptions.py +96 -0
- audex/service/doctor/types.py +54 -0
- audex/service/export/__init__.py +236 -0
- audex/service/export/const.py +17 -0
- audex/service/export/exceptions.py +34 -0
- audex/service/export/types.py +21 -0
- audex/service/injectors/__init__.py +1 -0
- audex/service/injectors/container.py +53 -0
- audex/service/injectors/doctor.py +34 -0
- audex/service/injectors/export.py +27 -0
- audex/service/injectors/session.py +49 -0
- audex/service/session/__init__.py +754 -0
- audex/service/session/const.py +34 -0
- audex/service/session/exceptions.py +67 -0
- audex/service/session/types.py +91 -0
- audex/types.py +39 -0
- audex/utils.py +287 -0
- audex/valueobj/__init__.py +81 -0
- audex/valueobj/common/__init__.py +1 -0
- audex/valueobj/common/auth.py +84 -0
- audex/valueobj/common/email.py +16 -0
- audex/valueobj/common/ops.py +22 -0
- audex/valueobj/common/phone.py +84 -0
- audex/valueobj/common/version.py +72 -0
- audex/valueobj/session.py +19 -0
- audex/valueobj/utterance.py +15 -0
- audex/view/__init__.py +51 -0
- audex/view/container.py +17 -0
- audex/view/decorators.py +303 -0
- audex/view/pages/__init__.py +1 -0
- audex/view/pages/dashboard/__init__.py +286 -0
- audex/view/pages/dashboard/wifi.py +407 -0
- audex/view/pages/login.py +110 -0
- audex/view/pages/recording.py +348 -0
- audex/view/pages/register.py +202 -0
- audex/view/pages/sessions/__init__.py +196 -0
- audex/view/pages/sessions/details.py +224 -0
- audex/view/pages/sessions/export.py +443 -0
- audex/view/pages/settings.py +374 -0
- audex/view/pages/voiceprint/__init__.py +1 -0
- audex/view/pages/voiceprint/enroll.py +195 -0
- audex/view/pages/voiceprint/update.py +195 -0
- audex/view/static/css/dashboard.css +452 -0
- audex/view/static/css/glass.css +22 -0
- audex/view/static/css/global.css +541 -0
- audex/view/static/css/login.css +386 -0
- audex/view/static/css/recording.css +439 -0
- audex/view/static/css/register.css +293 -0
- audex/view/static/css/sessions/styles.css +501 -0
- audex/view/static/css/settings.css +186 -0
- audex/view/static/css/voiceprint/enroll.css +43 -0
- audex/view/static/css/voiceprint/styles.css +209 -0
- audex/view/static/css/voiceprint/update.css +44 -0
- audex/view/static/images/logo.svg +95 -0
- audex/view/static/js/recording.js +42 -0
- audex-1.0.7a3.dist-info/METADATA +361 -0
- audex-1.0.7a3.dist-info/RECORD +192 -0
- audex-1.0.7a3.dist-info/WHEEL +4 -0
- audex-1.0.7a3.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
import typing as t
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel as PydBaseModel
|
|
9
|
+
from pydantic import ConfigDict
|
|
10
|
+
from pydantic_core import PydanticUndefined
|
|
11
|
+
from pydantic_settings import BaseSettings
|
|
12
|
+
|
|
13
|
+
from audex import utils
|
|
14
|
+
from audex.exceptions import RequiredModuleNotFoundError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseModel(PydBaseModel):
|
|
18
|
+
"""Base model class with common configuration for all models.
|
|
19
|
+
|
|
20
|
+
This class sets default configurations for Pydantic models used in
|
|
21
|
+
the Audex project, ensuring consistent behavior across all derived
|
|
22
|
+
models.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config: t.ClassVar[ConfigDict] = ConfigDict(
|
|
26
|
+
validate_assignment=True,
|
|
27
|
+
extra="ignore",
|
|
28
|
+
arbitrary_types_allowed=True,
|
|
29
|
+
use_enum_values=True,
|
|
30
|
+
populate_by_name=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def __hash__(self) -> int:
|
|
34
|
+
"""Generate a hash based on the model's serializable
|
|
35
|
+
representation.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
An integer hash value.
|
|
39
|
+
"""
|
|
40
|
+
serl_dict = self.model_dump()
|
|
41
|
+
serl_json = json.dumps(serl_dict, sort_keys=True)
|
|
42
|
+
return hash(serl_json)
|
|
43
|
+
|
|
44
|
+
def __repr__(self) -> str:
|
|
45
|
+
field_reprs = ", ".join(
|
|
46
|
+
f"{field_name}={getattr(self, field_name)!r}"
|
|
47
|
+
for field_name in self.model_fields # type: ignore
|
|
48
|
+
)
|
|
49
|
+
return f"MODEL <{self.__class__.__name__}({field_reprs})>"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Settings(BaseSettings):
|
|
53
|
+
"""Base settings class with YAML/JSON/dotenv export capabilities."""
|
|
54
|
+
|
|
55
|
+
# ============================================================================
|
|
56
|
+
# Public Methods - Loading
|
|
57
|
+
# ============================================================================
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_yaml(cls, path: str | pathlib.Path | os.PathLike[str]) -> t.Self:
|
|
61
|
+
"""Load configuration from YAML file.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
path: Path to YAML configuration file.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Settings instance.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
RequiredModuleNotFoundError: If PyYAML is not installed.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
import yaml
|
|
74
|
+
|
|
75
|
+
data = yaml.safe_load(pathlib.Path(path).read_text(encoding="utf-8"))
|
|
76
|
+
return cls.model_validate(data, strict=False)
|
|
77
|
+
except ImportError as e:
|
|
78
|
+
raise RequiredModuleNotFoundError(
|
|
79
|
+
"`yaml` module is required to load configuration from YAML "
|
|
80
|
+
"files. Please install it using `pip install pyyaml`."
|
|
81
|
+
) from e
|
|
82
|
+
|
|
83
|
+
# ============================================================================
|
|
84
|
+
# Public Methods - Export
|
|
85
|
+
# ============================================================================
|
|
86
|
+
|
|
87
|
+
def serl(self, include_none: bool = False) -> dict[str, t.Any]:
|
|
88
|
+
"""Get a serializable version of the model dump.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
include_none: If True, include fields with None values.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Dictionary with only serializable values.
|
|
95
|
+
"""
|
|
96
|
+
raw_dump = self.model_dump()
|
|
97
|
+
return self._clean_dict(raw_dump, include_none=include_none)
|
|
98
|
+
|
|
99
|
+
def to_yaml(
|
|
100
|
+
self,
|
|
101
|
+
fpath: str | pathlib.Path | os.PathLike[str],
|
|
102
|
+
exclude_unset: bool = False,
|
|
103
|
+
exclude_none: bool = False,
|
|
104
|
+
with_comments: bool = True,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Export configuration to YAML file with optional field
|
|
107
|
+
descriptions as comments.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
fpath: Path to the output YAML file.
|
|
111
|
+
exclude_unset: If True, exclude fields with Unset values and empty nested models.
|
|
112
|
+
exclude_none: If True, exclude fields with None values.
|
|
113
|
+
with_comments: If True, include field descriptions as comments.
|
|
114
|
+
"""
|
|
115
|
+
descriptions = self._collect_field_desc() if with_comments else {}
|
|
116
|
+
data = self.serl(include_none=True)
|
|
117
|
+
|
|
118
|
+
# Clean unset and none values
|
|
119
|
+
if exclude_unset:
|
|
120
|
+
data = self._clean_unset_recursive(data)
|
|
121
|
+
if exclude_none:
|
|
122
|
+
data = self._clean_none_recursive(data)
|
|
123
|
+
|
|
124
|
+
def write_yaml_value(
|
|
125
|
+
f: t.TextIO,
|
|
126
|
+
key: str,
|
|
127
|
+
value: t.Any,
|
|
128
|
+
field_path: str,
|
|
129
|
+
indent: int = 0,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Write a single YAML key-value pair with proper
|
|
132
|
+
formatting."""
|
|
133
|
+
indent_str = " " * indent
|
|
134
|
+
desc = descriptions.get(field_path, "")
|
|
135
|
+
comment = f" # {desc}" if desc and with_comments else ""
|
|
136
|
+
|
|
137
|
+
if value is None:
|
|
138
|
+
f.write(f"{indent_str}{key}: ~{comment}\n")
|
|
139
|
+
elif isinstance(value, dict):
|
|
140
|
+
f.write(f"{indent_str}{key}:{comment}\n")
|
|
141
|
+
write_yaml_dict(f, value, field_path, indent + 1)
|
|
142
|
+
elif isinstance(value, list):
|
|
143
|
+
f.write(f"{indent_str}{key}:{comment}\n")
|
|
144
|
+
write_yaml_list(f, value, field_path, indent + 1)
|
|
145
|
+
elif isinstance(value, str) and ("\n" in value or len(value) > 80):
|
|
146
|
+
f.write(f"{indent_str}{key}: |-{comment}\n")
|
|
147
|
+
for line in value.split("\n"):
|
|
148
|
+
f.write(f"{indent_str} {line}\n")
|
|
149
|
+
else:
|
|
150
|
+
yaml_value = self._yaml_repr(value)
|
|
151
|
+
f.write(f"{indent_str}{key}: {yaml_value}{comment}\n")
|
|
152
|
+
|
|
153
|
+
def write_yaml_dict(
|
|
154
|
+
f: t.TextIO,
|
|
155
|
+
obj: dict[str, t.Any],
|
|
156
|
+
prefix: str,
|
|
157
|
+
indent: int = 0,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Write a dictionary as YAML."""
|
|
160
|
+
for key, value in obj.items():
|
|
161
|
+
field_path = f"{prefix}.{key}" if prefix else key
|
|
162
|
+
write_yaml_value(f, key, value, field_path, indent)
|
|
163
|
+
|
|
164
|
+
def write_yaml_list(
|
|
165
|
+
f: t.TextIO,
|
|
166
|
+
items: list[t.Any],
|
|
167
|
+
prefix: str,
|
|
168
|
+
indent: int = 0,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Write a list as YAML."""
|
|
171
|
+
indent_str = " " * indent
|
|
172
|
+
for item in items:
|
|
173
|
+
if isinstance(item, dict):
|
|
174
|
+
items_list = list(item.items())
|
|
175
|
+
if items_list:
|
|
176
|
+
first_key, first_value = items_list[0]
|
|
177
|
+
first_field_path = f"{prefix}.{first_key}"
|
|
178
|
+
first_desc = descriptions.get(first_field_path, "")
|
|
179
|
+
first_comment = f" # {first_desc}" if first_desc and with_comments else ""
|
|
180
|
+
|
|
181
|
+
if first_value is None:
|
|
182
|
+
f.write(f"{indent_str}- {first_key}: ~{first_comment}\n")
|
|
183
|
+
elif isinstance(first_value, dict):
|
|
184
|
+
f.write(f"{indent_str}- {first_key}:{first_comment}\n")
|
|
185
|
+
write_yaml_dict(f, first_value, first_field_path, indent + 1)
|
|
186
|
+
elif isinstance(first_value, list):
|
|
187
|
+
f.write(f"{indent_str}- {first_key}:{first_comment}\n")
|
|
188
|
+
write_yaml_list(f, first_value, first_field_path, indent + 1)
|
|
189
|
+
elif isinstance(first_value, str) and (
|
|
190
|
+
"\n" in first_value or len(first_value) > 80
|
|
191
|
+
):
|
|
192
|
+
f.write(f"{indent_str}- {first_key}: |-{first_comment}\n")
|
|
193
|
+
for line in first_value.split("\n"):
|
|
194
|
+
f.write(f"{indent_str} {line}\n")
|
|
195
|
+
else:
|
|
196
|
+
yaml_val = self._yaml_repr(first_value)
|
|
197
|
+
f.write(f"{indent_str}- {first_key}: {yaml_val}{first_comment}\n")
|
|
198
|
+
|
|
199
|
+
for sub_key, sub_value in items_list[1:]:
|
|
200
|
+
sub_field_path = f"{prefix}.{sub_key}"
|
|
201
|
+
write_yaml_value(f, sub_key, sub_value, sub_field_path, indent + 1)
|
|
202
|
+
elif isinstance(item, list):
|
|
203
|
+
f.write(f"{indent_str}-\n")
|
|
204
|
+
write_yaml_list(f, item, prefix, indent + 1)
|
|
205
|
+
else:
|
|
206
|
+
yaml_item = self._yaml_repr(item)
|
|
207
|
+
f.write(f"{indent_str}- {yaml_item}\n")
|
|
208
|
+
|
|
209
|
+
with pathlib.Path(fpath).open("w", encoding="utf-8") as f:
|
|
210
|
+
write_yaml_dict(f, data, "")
|
|
211
|
+
|
|
212
|
+
def to_system_yaml(
|
|
213
|
+
self,
|
|
214
|
+
fpath: str | pathlib.Path | os.PathLike[str],
|
|
215
|
+
platform: t.Literal["linux", "windows"] | None = None,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Export system configuration to YAML file with platform-
|
|
218
|
+
specific defaults.
|
|
219
|
+
|
|
220
|
+
This method generates a system configuration file using platform-specific
|
|
221
|
+
default values defined in AudexFieldInfo descriptors. Fields with Unset
|
|
222
|
+
values are omitted. Empty nested models (all fields Unset) are also omitted.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
fpath: Path to the output YAML file
|
|
226
|
+
platform: Target platform ("linux" or "windows"). If None, uses current platform.
|
|
227
|
+
"""
|
|
228
|
+
if platform is None:
|
|
229
|
+
platform = "linux" if os.name != "nt" else "windows"
|
|
230
|
+
|
|
231
|
+
# Build data with platform-specific defaults
|
|
232
|
+
data = self._build_platform_data(self.__class__, platform)
|
|
233
|
+
|
|
234
|
+
# Clean Unset values and empty dicts
|
|
235
|
+
data = self._clean_unset_values(data)
|
|
236
|
+
|
|
237
|
+
# Write YAML without comments
|
|
238
|
+
with pathlib.Path(fpath).open("w", encoding="utf-8") as f:
|
|
239
|
+
self._write_system_yaml_header(f, platform)
|
|
240
|
+
self._write_yaml_dict(f, data)
|
|
241
|
+
|
|
242
|
+
def to_dotenv(self, fpath: str | pathlib.Path | os.PathLike[str]) -> None:
|
|
243
|
+
"""Export configuration to .env file with field descriptions as
|
|
244
|
+
comments.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
fpath: Path to the output .env file.
|
|
248
|
+
"""
|
|
249
|
+
sep = self.model_config.get("env_nested_delimiter") or "__"
|
|
250
|
+
prefix = self.model_config.get("env_prefix") or "AUDEX__"
|
|
251
|
+
|
|
252
|
+
if prefix.endswith(sep):
|
|
253
|
+
prefix = prefix[: -len(sep)]
|
|
254
|
+
|
|
255
|
+
serializable_data = self.serl(include_none=True)
|
|
256
|
+
descriptions = self._collect_field_desc()
|
|
257
|
+
|
|
258
|
+
grouped_keys = {}
|
|
259
|
+
for top_level_key in serializable_data:
|
|
260
|
+
section_data = {top_level_key: serializable_data[top_level_key]}
|
|
261
|
+
flattened = utils.flatten_dict(section_data, sep=sep)
|
|
262
|
+
grouped_keys[top_level_key] = flattened
|
|
263
|
+
|
|
264
|
+
with pathlib.Path(fpath).open("w", encoding="utf-8") as f:
|
|
265
|
+
f.write(
|
|
266
|
+
"# Description: Example environment configuration file for Audex application.\n"
|
|
267
|
+
)
|
|
268
|
+
f.write("# Note: Copy this file to '.env' and modify the values as needed.\n\n")
|
|
269
|
+
for top_key, flattened in grouped_keys.items():
|
|
270
|
+
f.write(f"# {'=' * 70}\n")
|
|
271
|
+
|
|
272
|
+
top_key_desc = descriptions.get(top_key)
|
|
273
|
+
if top_key_desc:
|
|
274
|
+
f.write(f"# {top_key.upper()}: {top_key_desc}\n")
|
|
275
|
+
f.write(f"# {'=' * 70}\n")
|
|
276
|
+
f.write("\n")
|
|
277
|
+
|
|
278
|
+
for key, value in flattened.items():
|
|
279
|
+
env_key = f"{prefix}{sep}{key.upper()}"
|
|
280
|
+
|
|
281
|
+
field_path = key.replace(sep, ".")
|
|
282
|
+
if field_path in descriptions:
|
|
283
|
+
f.write(f"# {descriptions[field_path]}\n")
|
|
284
|
+
|
|
285
|
+
if value is None:
|
|
286
|
+
f.write(f"# {env_key}=\n")
|
|
287
|
+
else:
|
|
288
|
+
formatted_value = self._format_env_value(value)
|
|
289
|
+
f.write(f"{env_key}={formatted_value}\n")
|
|
290
|
+
f.write("\n")
|
|
291
|
+
|
|
292
|
+
# ============================================================================
|
|
293
|
+
# Private Methods - Field Introspection
|
|
294
|
+
# ============================================================================
|
|
295
|
+
|
|
296
|
+
def _collect_field_desc(
|
|
297
|
+
self,
|
|
298
|
+
model: type[BaseModel] | None = None,
|
|
299
|
+
prefix: str = "",
|
|
300
|
+
) -> dict[str, str]:
|
|
301
|
+
"""Recursively collect field descriptions from the model and
|
|
302
|
+
nested models.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
model: The pydantic model to collect descriptions from.
|
|
306
|
+
prefix: The prefix for nested field paths.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
A dictionary mapping field paths to their descriptions.
|
|
310
|
+
"""
|
|
311
|
+
if model is None:
|
|
312
|
+
model = self.__class__
|
|
313
|
+
|
|
314
|
+
descriptions = {}
|
|
315
|
+
for field_name, field_info in model.model_fields.items():
|
|
316
|
+
field_path = f"{prefix}.{field_name}" if prefix else field_name
|
|
317
|
+
|
|
318
|
+
if field_info.description:
|
|
319
|
+
descriptions[field_path] = field_info.description
|
|
320
|
+
|
|
321
|
+
if hasattr(field_info.annotation, "model_fields"):
|
|
322
|
+
nested_descriptions = self._collect_field_desc(
|
|
323
|
+
field_info.annotation, prefix=field_path
|
|
324
|
+
)
|
|
325
|
+
descriptions.update(nested_descriptions)
|
|
326
|
+
|
|
327
|
+
return descriptions
|
|
328
|
+
|
|
329
|
+
# ============================================================================
|
|
330
|
+
# Private Methods - Serialization
|
|
331
|
+
# ============================================================================
|
|
332
|
+
|
|
333
|
+
def _serl_value(self, value: t.Any, include_none: bool = False) -> t.Any:
|
|
334
|
+
"""Serialize a value, handling special cases like callables.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
value: The value to serialize.
|
|
338
|
+
include_none: If True, preserve None values in nested structures.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Serialized value or None if not serializable.
|
|
342
|
+
"""
|
|
343
|
+
if value is None:
|
|
344
|
+
return value if include_none else None
|
|
345
|
+
|
|
346
|
+
if isinstance(value, os.PathLike):
|
|
347
|
+
try:
|
|
348
|
+
return os.fspath(value)
|
|
349
|
+
except Exception:
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
if callable(value):
|
|
353
|
+
try:
|
|
354
|
+
return self._serl_value(value(), include_none=include_none)
|
|
355
|
+
except Exception:
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
if isinstance(value, PydBaseModel):
|
|
359
|
+
return value.model_dump()
|
|
360
|
+
|
|
361
|
+
serialized: list[t.Any] | dict[str, t.Any]
|
|
362
|
+
|
|
363
|
+
if isinstance(value, (list, tuple)):
|
|
364
|
+
serialized = []
|
|
365
|
+
for item in value:
|
|
366
|
+
serialized_item = self._serl_value(item, include_none=include_none)
|
|
367
|
+
if include_none or serialized_item is not None:
|
|
368
|
+
serialized.append(serialized_item)
|
|
369
|
+
return serialized if serialized or include_none else None
|
|
370
|
+
|
|
371
|
+
if isinstance(value, dict):
|
|
372
|
+
serialized = {}
|
|
373
|
+
for k, v in value.items():
|
|
374
|
+
serialized_v = self._serl_value(v, include_none=include_none)
|
|
375
|
+
if include_none or serialized_v is not None:
|
|
376
|
+
serialized[k] = serialized_v
|
|
377
|
+
return serialized if serialized or include_none else None
|
|
378
|
+
|
|
379
|
+
if isinstance(value, (str, int, float, bool)):
|
|
380
|
+
return value
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
json.dumps(value)
|
|
384
|
+
return value
|
|
385
|
+
except (TypeError, ValueError):
|
|
386
|
+
return None
|
|
387
|
+
|
|
388
|
+
def _clean_dict(self, data: dict[str, t.Any], include_none: bool = False) -> dict[str, t.Any]:
|
|
389
|
+
"""Recursively clean a dictionary to remove non-serializable
|
|
390
|
+
values.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
data: Dictionary to clean.
|
|
394
|
+
include_none: If True, include fields with None values.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
Cleaned dictionary.
|
|
398
|
+
"""
|
|
399
|
+
cleaned: dict[str, t.Any] = {}
|
|
400
|
+
for key, value in data.items():
|
|
401
|
+
if value is None:
|
|
402
|
+
if include_none:
|
|
403
|
+
cleaned[key] = None
|
|
404
|
+
else:
|
|
405
|
+
serialized = self._serl_value(value, include_none=include_none)
|
|
406
|
+
if serialized is not None or include_none:
|
|
407
|
+
cleaned[key] = serialized
|
|
408
|
+
return cleaned
|
|
409
|
+
|
|
410
|
+
def _clean_unset_recursive(self, data: t.Any) -> t.Any:
|
|
411
|
+
"""Recursively remove Unset values and empty nested structures.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
data: Data structure to clean.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Cleaned data structure with Unset values and empty dicts removed.
|
|
418
|
+
"""
|
|
419
|
+
if isinstance(data, utils.Unset) or isinstance(data, str) and data == "<UNSET>":
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
if isinstance(data, dict):
|
|
423
|
+
cleaned = {}
|
|
424
|
+
for key, value in data.items():
|
|
425
|
+
# Skip Unset values
|
|
426
|
+
if isinstance(value, utils.Unset) or isinstance(value, str) and value == "<UNSET>":
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
cleaned_value = self._clean_unset_recursive(value)
|
|
430
|
+
|
|
431
|
+
# Skip None (which indicates Unset was found)
|
|
432
|
+
if cleaned_value is None:
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
# Skip empty dicts (all nested fields were Unset)
|
|
436
|
+
if isinstance(cleaned_value, dict) and not cleaned_value:
|
|
437
|
+
continue
|
|
438
|
+
|
|
439
|
+
# Skip empty lists
|
|
440
|
+
if isinstance(cleaned_value, list) and not cleaned_value:
|
|
441
|
+
continue
|
|
442
|
+
|
|
443
|
+
cleaned[key] = cleaned_value
|
|
444
|
+
|
|
445
|
+
# Return None if dict is empty (all fields were Unset)
|
|
446
|
+
return cleaned if cleaned else None
|
|
447
|
+
|
|
448
|
+
if isinstance(data, list):
|
|
449
|
+
cleaned_list = []
|
|
450
|
+
for item in data:
|
|
451
|
+
if isinstance(item, utils.Unset) or isinstance(item, str) and item == "<UNSET>":
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
cleaned_item = self._clean_unset_recursive(item)
|
|
455
|
+
if cleaned_item is not None:
|
|
456
|
+
# Don't add empty dicts or lists
|
|
457
|
+
if isinstance(cleaned_item, (dict, list)) and not cleaned_item:
|
|
458
|
+
continue
|
|
459
|
+
cleaned_list.append(cleaned_item)
|
|
460
|
+
|
|
461
|
+
return cleaned_list if cleaned_list else None
|
|
462
|
+
|
|
463
|
+
return data
|
|
464
|
+
|
|
465
|
+
def _clean_none_recursive(self, data: t.Any) -> t.Any:
|
|
466
|
+
"""Recursively remove None values and empty nested structures.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
data: Data structure to clean.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Cleaned data structure with None values removed.
|
|
473
|
+
"""
|
|
474
|
+
if data is None:
|
|
475
|
+
return None
|
|
476
|
+
|
|
477
|
+
if isinstance(data, dict):
|
|
478
|
+
cleaned = {}
|
|
479
|
+
for key, value in data.items():
|
|
480
|
+
if value is None:
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
cleaned_value = self._clean_none_recursive(value)
|
|
484
|
+
|
|
485
|
+
if cleaned_value is None:
|
|
486
|
+
continue
|
|
487
|
+
|
|
488
|
+
# Skip empty dicts
|
|
489
|
+
if isinstance(cleaned_value, dict) and not cleaned_value:
|
|
490
|
+
continue
|
|
491
|
+
|
|
492
|
+
# Skip empty lists
|
|
493
|
+
if isinstance(cleaned_value, list) and not cleaned_value:
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
cleaned[key] = cleaned_value
|
|
497
|
+
|
|
498
|
+
return cleaned if cleaned else None
|
|
499
|
+
|
|
500
|
+
if isinstance(data, list):
|
|
501
|
+
cleaned_list = []
|
|
502
|
+
for item in data:
|
|
503
|
+
if item is None:
|
|
504
|
+
continue
|
|
505
|
+
|
|
506
|
+
cleaned_item = self._clean_none_recursive(item)
|
|
507
|
+
if cleaned_item is not None:
|
|
508
|
+
if isinstance(cleaned_item, (dict, list)) and not cleaned_item:
|
|
509
|
+
continue
|
|
510
|
+
cleaned_list.append(cleaned_item)
|
|
511
|
+
|
|
512
|
+
return cleaned_list if cleaned_list else None
|
|
513
|
+
|
|
514
|
+
return data
|
|
515
|
+
|
|
516
|
+
# ============================================================================
|
|
517
|
+
# Private Methods - System YAML Generation
|
|
518
|
+
# ============================================================================
|
|
519
|
+
|
|
520
|
+
def _build_platform_data(
|
|
521
|
+
self,
|
|
522
|
+
model: type[PydBaseModel],
|
|
523
|
+
platform: str,
|
|
524
|
+
prefix: str = "",
|
|
525
|
+
) -> dict[str, t.Any]:
|
|
526
|
+
"""Recursively build configuration data with platform-specific
|
|
527
|
+
defaults.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
model: Pydantic model to process.
|
|
531
|
+
platform: Target platform.
|
|
532
|
+
prefix: Field path prefix.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Dictionary with platform-specific default values.
|
|
536
|
+
"""
|
|
537
|
+
from audex.helper.settings.fields import AudexFieldInfo
|
|
538
|
+
|
|
539
|
+
result = {}
|
|
540
|
+
|
|
541
|
+
for field_name, field_info in model.model_fields.items():
|
|
542
|
+
field_path = f"{prefix}.{field_name}" if prefix else field_name
|
|
543
|
+
|
|
544
|
+
# Handle nested models
|
|
545
|
+
if hasattr(field_info.annotation, "model_fields"):
|
|
546
|
+
nested_data = self._build_platform_data(
|
|
547
|
+
field_info.annotation, # type: ignore
|
|
548
|
+
platform,
|
|
549
|
+
field_path,
|
|
550
|
+
)
|
|
551
|
+
# Only add if nested model has content
|
|
552
|
+
if nested_data:
|
|
553
|
+
result[field_name] = nested_data
|
|
554
|
+
continue
|
|
555
|
+
|
|
556
|
+
# Get value based on field type
|
|
557
|
+
if isinstance(field_info, AudexFieldInfo):
|
|
558
|
+
value = field_info.get_platform_default(platform)
|
|
559
|
+
else:
|
|
560
|
+
# Standard field - use current value or default
|
|
561
|
+
current_value = getattr(self, field_name, PydanticUndefined)
|
|
562
|
+
if current_value is not PydanticUndefined:
|
|
563
|
+
value = current_value
|
|
564
|
+
elif field_info.default is not PydanticUndefined:
|
|
565
|
+
value = field_info.default
|
|
566
|
+
elif field_info.default_factory is not None:
|
|
567
|
+
value = field_info.default_factory()
|
|
568
|
+
else:
|
|
569
|
+
value = PydanticUndefined
|
|
570
|
+
|
|
571
|
+
# Serialize the value (handle Pydantic models, lists, dicts, etc.)
|
|
572
|
+
if value is not PydanticUndefined:
|
|
573
|
+
serialized_value = self._serl_value(value, include_none=True)
|
|
574
|
+
result[field_name] = serialized_value
|
|
575
|
+
else:
|
|
576
|
+
result[field_name] = value
|
|
577
|
+
|
|
578
|
+
return result
|
|
579
|
+
|
|
580
|
+
def _clean_unset_values(self, data: t.Any) -> t.Any:
|
|
581
|
+
"""Recursively remove Unset values, PydanticUndefined, and empty
|
|
582
|
+
dictionaries.
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
data: Data structure to clean.
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
Cleaned data structure, or None if all values were Unset.
|
|
589
|
+
"""
|
|
590
|
+
# Check for Unset or PydanticUndefined
|
|
591
|
+
if isinstance(data, utils.Unset) or data is PydanticUndefined:
|
|
592
|
+
return None
|
|
593
|
+
|
|
594
|
+
if isinstance(data, dict):
|
|
595
|
+
cleaned = {}
|
|
596
|
+
for key, value in data.items():
|
|
597
|
+
# Skip Unset and PydanticUndefined
|
|
598
|
+
if isinstance(value, utils.Unset) or value is PydanticUndefined:
|
|
599
|
+
continue
|
|
600
|
+
|
|
601
|
+
cleaned_value = self._clean_unset_values(value)
|
|
602
|
+
|
|
603
|
+
# Skip None values from recursive cleaning
|
|
604
|
+
if cleaned_value is None:
|
|
605
|
+
continue
|
|
606
|
+
|
|
607
|
+
# Skip empty dicts (all fields were Unset)
|
|
608
|
+
if isinstance(cleaned_value, dict) and not cleaned_value:
|
|
609
|
+
continue
|
|
610
|
+
|
|
611
|
+
cleaned[key] = cleaned_value
|
|
612
|
+
|
|
613
|
+
return cleaned if cleaned else None
|
|
614
|
+
|
|
615
|
+
if isinstance(data, list):
|
|
616
|
+
cleaned_list = []
|
|
617
|
+
for item in data:
|
|
618
|
+
if isinstance(item, utils.Unset) or item is PydanticUndefined:
|
|
619
|
+
continue
|
|
620
|
+
|
|
621
|
+
cleaned_item = self._clean_unset_values(item)
|
|
622
|
+
if cleaned_item is not None:
|
|
623
|
+
cleaned_list.append(cleaned_item)
|
|
624
|
+
|
|
625
|
+
return cleaned_list if cleaned_list else None
|
|
626
|
+
|
|
627
|
+
return data
|
|
628
|
+
|
|
629
|
+
def _write_system_yaml_header(self, f: t.TextIO, platform: str) -> None:
|
|
630
|
+
"""Write system YAML file header.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
f: File object.
|
|
634
|
+
platform: Target platform.
|
|
635
|
+
"""
|
|
636
|
+
import audex
|
|
637
|
+
|
|
638
|
+
f.write("# Audex System Configuration\n")
|
|
639
|
+
f.write(f"# Platform: {platform}\n")
|
|
640
|
+
f.write(f"# Version: {audex.__version__}\n")
|
|
641
|
+
f.write("#\n")
|
|
642
|
+
f.write("# This file is generated automatically. Do not edit manually.\n")
|
|
643
|
+
f.write("# User configuration should be placed in ~/.config/audex/config.yml\n")
|
|
644
|
+
f.write("#\n")
|
|
645
|
+
f.write("# For configuration examples, see: /etc/audex/config.example.yml\n")
|
|
646
|
+
f.write("\n")
|
|
647
|
+
|
|
648
|
+
def _write_yaml_dict(
|
|
649
|
+
self,
|
|
650
|
+
f: t.TextIO,
|
|
651
|
+
obj: dict[str, t.Any],
|
|
652
|
+
indent: int = 0,
|
|
653
|
+
) -> None:
|
|
654
|
+
"""Write a dictionary as YAML without comments.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
f: File object.
|
|
658
|
+
obj: Dictionary to write.
|
|
659
|
+
indent: Indentation level.
|
|
660
|
+
"""
|
|
661
|
+
for key, value in obj.items():
|
|
662
|
+
self._write_yaml_value(f, key, value, indent)
|
|
663
|
+
|
|
664
|
+
def _write_yaml_value(
|
|
665
|
+
self,
|
|
666
|
+
f: t.TextIO,
|
|
667
|
+
key: str,
|
|
668
|
+
value: t.Any,
|
|
669
|
+
indent: int = 0,
|
|
670
|
+
) -> None:
|
|
671
|
+
"""Write a single YAML key-value pair without comments.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
f: File object.
|
|
675
|
+
key: Key name.
|
|
676
|
+
value: Value to write.
|
|
677
|
+
indent: Indentation level.
|
|
678
|
+
"""
|
|
679
|
+
indent_str = " " * indent
|
|
680
|
+
|
|
681
|
+
if value is None:
|
|
682
|
+
f.write(f"{indent_str}{key}: ~\n")
|
|
683
|
+
elif isinstance(value, dict):
|
|
684
|
+
f.write(f"{indent_str}{key}:\n")
|
|
685
|
+
self._write_yaml_dict(f, value, indent + 1)
|
|
686
|
+
elif isinstance(value, list):
|
|
687
|
+
f.write(f"{indent_str}{key}:\n")
|
|
688
|
+
self._write_yaml_list(f, value, indent + 1)
|
|
689
|
+
elif isinstance(value, str) and ("\n" in value or len(value) > 80):
|
|
690
|
+
f.write(f"{indent_str}{key}: |-\n")
|
|
691
|
+
for line in value.split("\n"):
|
|
692
|
+
f.write(f"{indent_str} {line}\n")
|
|
693
|
+
else:
|
|
694
|
+
yaml_value = self._yaml_repr(value)
|
|
695
|
+
f.write(f"{indent_str}{key}: {yaml_value}\n")
|
|
696
|
+
|
|
697
|
+
def _write_yaml_list(
|
|
698
|
+
self,
|
|
699
|
+
f: t.TextIO,
|
|
700
|
+
items: list[t.Any],
|
|
701
|
+
indent: int = 0,
|
|
702
|
+
) -> None:
|
|
703
|
+
"""Write a list as YAML without comments.
|
|
704
|
+
|
|
705
|
+
Args:
|
|
706
|
+
f: File object.
|
|
707
|
+
items: List items to write.
|
|
708
|
+
indent: Indentation level.
|
|
709
|
+
"""
|
|
710
|
+
indent_str = " " * indent
|
|
711
|
+
for item in items:
|
|
712
|
+
if isinstance(item, dict):
|
|
713
|
+
items_list = list(item.items())
|
|
714
|
+
if items_list:
|
|
715
|
+
first_key, first_value = items_list[0]
|
|
716
|
+
|
|
717
|
+
if first_value is None:
|
|
718
|
+
f.write(f"{indent_str}- {first_key}: ~\n")
|
|
719
|
+
elif isinstance(first_value, dict):
|
|
720
|
+
f.write(f"{indent_str}- {first_key}:\n")
|
|
721
|
+
self._write_yaml_dict(f, first_value, indent + 1)
|
|
722
|
+
elif isinstance(first_value, list):
|
|
723
|
+
f.write(f"{indent_str}- {first_key}:\n")
|
|
724
|
+
self._write_yaml_list(f, first_value, indent + 1)
|
|
725
|
+
elif isinstance(first_value, str) and (
|
|
726
|
+
"\n" in first_value or len(first_value) > 80
|
|
727
|
+
):
|
|
728
|
+
f.write(f"{indent_str}- {first_key}: |-\n")
|
|
729
|
+
for line in first_value.split("\n"):
|
|
730
|
+
f.write(f"{indent_str} {line}\n")
|
|
731
|
+
else:
|
|
732
|
+
yaml_val = self._yaml_repr(first_value)
|
|
733
|
+
f.write(f"{indent_str}- {first_key}: {yaml_val}\n")
|
|
734
|
+
|
|
735
|
+
for sub_key, sub_value in items_list[1:]:
|
|
736
|
+
self._write_yaml_value(f, sub_key, sub_value, indent + 1)
|
|
737
|
+
elif isinstance(item, list):
|
|
738
|
+
f.write(f"{indent_str}-\n")
|
|
739
|
+
self._write_yaml_list(f, item, indent + 1)
|
|
740
|
+
else:
|
|
741
|
+
yaml_item = self._yaml_repr(item)
|
|
742
|
+
f.write(f"{indent_str}- {yaml_item}\n")
|
|
743
|
+
|
|
744
|
+
# ============================================================================
|
|
745
|
+
# Private Methods - YAML Formatting
|
|
746
|
+
# ============================================================================
|
|
747
|
+
|
|
748
|
+
def _yaml_repr(self, value: t.Any) -> str:
|
|
749
|
+
"""Convert a Python value to YAML representation string.
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
value: The value to convert.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
YAML string representation.
|
|
756
|
+
"""
|
|
757
|
+
if value is None:
|
|
758
|
+
return "~"
|
|
759
|
+
if isinstance(value, bool):
|
|
760
|
+
return "true" if value else "false"
|
|
761
|
+
if isinstance(value, (int, float)):
|
|
762
|
+
return str(value)
|
|
763
|
+
if isinstance(value, str):
|
|
764
|
+
if (
|
|
765
|
+
not value
|
|
766
|
+
or value[0] in "-? :,[]{}#&*! |>'\"%@`"
|
|
767
|
+
or value in ("true", "false", "null", "yes", "no", "on", "off")
|
|
768
|
+
or ":" in value
|
|
769
|
+
or "#" in value
|
|
770
|
+
):
|
|
771
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
|
772
|
+
return f'"{escaped}"'
|
|
773
|
+
return value
|
|
774
|
+
return str(value)
|
|
775
|
+
|
|
776
|
+
# ============================================================================
|
|
777
|
+
# Private Methods - Dotenv Formatting
|
|
778
|
+
# ============================================================================
|
|
779
|
+
|
|
780
|
+
def _format_env_value(self, value: t.Any) -> str:
|
|
781
|
+
"""Format a value for .env file with proper quoting.
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
value: The value to format.
|
|
785
|
+
|
|
786
|
+
Returns:
|
|
787
|
+
Formatted string value.
|
|
788
|
+
"""
|
|
789
|
+
if value is None:
|
|
790
|
+
return ""
|
|
791
|
+
|
|
792
|
+
if isinstance(value, os.PathLike):
|
|
793
|
+
try:
|
|
794
|
+
return os.fspath(value)
|
|
795
|
+
except Exception:
|
|
796
|
+
return ""
|
|
797
|
+
|
|
798
|
+
if isinstance(value, bool):
|
|
799
|
+
return "true" if value else "false"
|
|
800
|
+
|
|
801
|
+
if isinstance(value, (int, float)):
|
|
802
|
+
return str(value)
|
|
803
|
+
|
|
804
|
+
if isinstance(value, str):
|
|
805
|
+
needs_quotes = (
|
|
806
|
+
" " in value
|
|
807
|
+
or "#" in value
|
|
808
|
+
or "=" in value
|
|
809
|
+
or "\n" in value
|
|
810
|
+
or "\t" in value
|
|
811
|
+
or "\r" in value
|
|
812
|
+
or value.startswith(('"', "'"))
|
|
813
|
+
or not value
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
if needs_quotes:
|
|
817
|
+
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
|
818
|
+
return f'"{escaped}"'
|
|
819
|
+
return value
|
|
820
|
+
|
|
821
|
+
if isinstance(value, (list, dict)):
|
|
822
|
+
json_str = json.dumps(value, separators=(",", ":"), ensure_ascii=False)
|
|
823
|
+
escaped = json_str.replace("\\", "\\\\").replace('"', '\\"')
|
|
824
|
+
return f'"{escaped}"'
|
|
825
|
+
|
|
826
|
+
str_value = str(value)
|
|
827
|
+
if " " in str_value or "#" in str_value:
|
|
828
|
+
escaped = str_value.replace("\\", "\\\\").replace('"', '\\"')
|
|
829
|
+
return f'"{escaped}"'
|
|
830
|
+
return str_value
|