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
audex/cli/args.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import argparse
|
|
5
|
+
import pathlib
|
|
6
|
+
import typing as t
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic import ConfigDict
|
|
10
|
+
from pydantic import ModelWrapValidatorHandler
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
from pydantic import model_validator
|
|
13
|
+
|
|
14
|
+
from audex import __description__
|
|
15
|
+
from audex import __title__
|
|
16
|
+
from audex import __version__
|
|
17
|
+
from audex.cli.exceptions import InvalidArgumentError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseArgs(BaseModel, abc.ABC):
|
|
21
|
+
"""Base class for CLI arguments with automatic argparse integration.
|
|
22
|
+
|
|
23
|
+
Automatically generates argparse arguments from Pydantic fields with support for:
|
|
24
|
+
- Basic types (str, int, float, bool)
|
|
25
|
+
- Optional types (Optional[T])
|
|
26
|
+
- List types (list[T])
|
|
27
|
+
- Literal types for choices
|
|
28
|
+
- Union types
|
|
29
|
+
- Path types
|
|
30
|
+
- Nested subcommands
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
```python
|
|
34
|
+
class MyArgs(BaseArgs):
|
|
35
|
+
name: str = Field(description="Your name")
|
|
36
|
+
age: int = Field(default=18, description="Your age")
|
|
37
|
+
verbose: bool = Field(
|
|
38
|
+
default=False, description="Verbose output"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def run(self) -> None:
|
|
42
|
+
print(f"Hello {self.name}, age {self.age}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
parser = argparse.ArgumentParser()
|
|
46
|
+
MyArgs.build_args(parser)
|
|
47
|
+
parser.set_defaults(func=MyArgs.func)
|
|
48
|
+
args = parser.parse_args()
|
|
49
|
+
args.func(args)
|
|
50
|
+
```
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@abc.abstractmethod
|
|
54
|
+
def run(self) -> None:
|
|
55
|
+
"""Execute the command logic.
|
|
56
|
+
|
|
57
|
+
Must be implemented by subclasses.
|
|
58
|
+
"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def func(cls, args: argparse.Namespace) -> None:
|
|
63
|
+
"""Entry point called by argparse. Parses args and calls run().
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
args: The argparse Namespace containing parsed arguments.
|
|
67
|
+
"""
|
|
68
|
+
instance = cls.parse_args(args)
|
|
69
|
+
instance.run()
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def build_args(cls, parser: argparse.ArgumentParser) -> None:
|
|
73
|
+
"""Build argparse arguments from Pydantic fields.
|
|
74
|
+
|
|
75
|
+
Automatically generates appropriate argparse arguments based on field types:
|
|
76
|
+
- bool: Uses store_true/store_false based on default value
|
|
77
|
+
- list[T]: Uses nargs='+' or nargs='*'
|
|
78
|
+
- Optional[T]: Makes argument optional with default None
|
|
79
|
+
- Literal[... ]: Uses choices parameter
|
|
80
|
+
- Path/pathlib.Path: Uses type=pathlib.Path
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
parser: The argparse parser to add arguments to.
|
|
84
|
+
"""
|
|
85
|
+
for field, fieldinfo in cls.__pydantic_fields__.items():
|
|
86
|
+
# Skip internal fields
|
|
87
|
+
if field.startswith("_"):
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
arg_name = f"--{field.replace('_', '-')}"
|
|
91
|
+
alias = fieldinfo.alias
|
|
92
|
+
flags = (arg_name, f"-{alias}") if alias else (arg_name,)
|
|
93
|
+
required = fieldinfo.is_required()
|
|
94
|
+
ann = fieldinfo.annotation
|
|
95
|
+
help_text = fieldinfo.description or ""
|
|
96
|
+
default = fieldinfo.default
|
|
97
|
+
|
|
98
|
+
# Unwrap the annotation to get the actual type
|
|
99
|
+
origin = t.get_origin(ann)
|
|
100
|
+
args_types = t.get_args(ann)
|
|
101
|
+
|
|
102
|
+
# Handle Optional[T] (Union[T, None])
|
|
103
|
+
if origin is t.Union:
|
|
104
|
+
# Filter out NoneType
|
|
105
|
+
non_none_types = [a for a in args_types if a is not type(None)]
|
|
106
|
+
if type(None) in args_types and non_none_types:
|
|
107
|
+
# This is Optional[T]
|
|
108
|
+
ann = (
|
|
109
|
+
non_none_types[0]
|
|
110
|
+
if len(non_none_types) == 1
|
|
111
|
+
else t.Union[tuple(non_none_types)] # noqa
|
|
112
|
+
)
|
|
113
|
+
required = False
|
|
114
|
+
origin = t.get_origin(ann)
|
|
115
|
+
args_types = t.get_args(ann)
|
|
116
|
+
|
|
117
|
+
# Handle Literal types
|
|
118
|
+
choices = None
|
|
119
|
+
if origin is t.Literal:
|
|
120
|
+
choices = list(args_types)
|
|
121
|
+
ann = type(choices[0]) if choices else str
|
|
122
|
+
origin = None # Reset origin since we've extracted the type
|
|
123
|
+
|
|
124
|
+
# Handle List types
|
|
125
|
+
if origin is list:
|
|
126
|
+
item_type = args_types[0] if args_types else str
|
|
127
|
+
|
|
128
|
+
# Handle List[Literal[...]]
|
|
129
|
+
if t.get_origin(item_type) is t.Literal:
|
|
130
|
+
choices = list(t.get_args(item_type))
|
|
131
|
+
item_type = type(choices[0]) if choices else str
|
|
132
|
+
|
|
133
|
+
# Determine nargs
|
|
134
|
+
nargs = "*" if not required else "+"
|
|
135
|
+
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
*flags,
|
|
138
|
+
type=item_type,
|
|
139
|
+
nargs=nargs,
|
|
140
|
+
default=default if not required else None,
|
|
141
|
+
help=help_text,
|
|
142
|
+
choices=choices,
|
|
143
|
+
dest=field,
|
|
144
|
+
)
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# Handle Path types
|
|
148
|
+
if ann in (pathlib.Path, pathlib.PosixPath, pathlib.WindowsPath):
|
|
149
|
+
if required:
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
*flags,
|
|
152
|
+
required=True,
|
|
153
|
+
type=pathlib.Path,
|
|
154
|
+
help=help_text,
|
|
155
|
+
dest=field,
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
*flags,
|
|
160
|
+
type=pathlib.Path,
|
|
161
|
+
default=default,
|
|
162
|
+
help=help_text,
|
|
163
|
+
dest=field,
|
|
164
|
+
)
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
# Handle boolean types
|
|
168
|
+
is_bool = ann is bool or (origin is None and ann is bool)
|
|
169
|
+
if is_bool:
|
|
170
|
+
# Determine action based on default value
|
|
171
|
+
if default is True:
|
|
172
|
+
# If default is True, use store_false with --no-prefix
|
|
173
|
+
neg_flags = (
|
|
174
|
+
(f"--no-{field.replace('_', '-')}", f"-N{alias}")
|
|
175
|
+
if alias
|
|
176
|
+
else (f"--no-{field.replace('_', '-')}",)
|
|
177
|
+
)
|
|
178
|
+
parser.add_argument(
|
|
179
|
+
*neg_flags,
|
|
180
|
+
action="store_false",
|
|
181
|
+
help=help_text or f"Disable {field}",
|
|
182
|
+
dest=field,
|
|
183
|
+
default=True,
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
# If default is False or not set, use store_true
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
*flags,
|
|
189
|
+
action="store_true",
|
|
190
|
+
help=help_text,
|
|
191
|
+
dest=field,
|
|
192
|
+
default=False,
|
|
193
|
+
)
|
|
194
|
+
continue
|
|
195
|
+
|
|
196
|
+
# Handle regular types (str, int, float, etc.)
|
|
197
|
+
try:
|
|
198
|
+
type_func = ann if callable(ann) else str
|
|
199
|
+
except Exception:
|
|
200
|
+
type_func = str
|
|
201
|
+
|
|
202
|
+
if required:
|
|
203
|
+
parser.add_argument(
|
|
204
|
+
*flags,
|
|
205
|
+
required=True,
|
|
206
|
+
type=type_func,
|
|
207
|
+
help=help_text,
|
|
208
|
+
choices=choices,
|
|
209
|
+
dest=field,
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
*flags,
|
|
214
|
+
default=default,
|
|
215
|
+
type=type_func,
|
|
216
|
+
help=help_text,
|
|
217
|
+
choices=choices,
|
|
218
|
+
dest=field,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def parse_args(cls, args: argparse.Namespace) -> t.Self:
|
|
223
|
+
"""Parse argparse Namespace into a Pydantic model instance.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
args: The argparse Namespace to parse.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
An instance of the class with validated fields.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
pydantic.ValidationError: If validation fails.
|
|
233
|
+
"""
|
|
234
|
+
# Convert Namespace to dict and filter out None values for optional fields
|
|
235
|
+
args_dict = vars(args)
|
|
236
|
+
|
|
237
|
+
# Remove non-field attributes (like 'func')
|
|
238
|
+
field_names = set(cls.__pydantic_fields__.keys())
|
|
239
|
+
filtered_dict = {k: v for k, v in args_dict.items() if k in field_names}
|
|
240
|
+
|
|
241
|
+
return cls.model_validate(filtered_dict, by_alias=False, by_name=True, strict=False)
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def register_subparser(
|
|
245
|
+
cls,
|
|
246
|
+
subparsers: argparse._SubParsersAction,
|
|
247
|
+
name: str,
|
|
248
|
+
help_text: str | None = None,
|
|
249
|
+
aliases: list[str] | None = None,
|
|
250
|
+
) -> argparse.ArgumentParser:
|
|
251
|
+
"""Register this command as a subcommand.
|
|
252
|
+
|
|
253
|
+
Supports nested subcommands by returning the parser which can have
|
|
254
|
+
its own subparsers added.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
subparsers: The subparsers action from parent parser.
|
|
258
|
+
name: The name of this subcommand.
|
|
259
|
+
help_text: Help text for this subcommand. If None, uses class docstring.
|
|
260
|
+
aliases: Alternative names for this subcommand.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
The argument parser for this subcommand, which can be used to
|
|
264
|
+
add nested subcommands.
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
```python
|
|
268
|
+
# Create main parser
|
|
269
|
+
parser = argparse.ArgumentParser()
|
|
270
|
+
subparsers = parser.add_subparsers(
|
|
271
|
+
dest="command", required=True
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Register top-level command
|
|
275
|
+
db_parser = DBCommand.register_subparser(
|
|
276
|
+
subparsers, "db", "Database commands"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Add nested subcommands
|
|
280
|
+
db_subparsers = db_parser.add_subparsers(
|
|
281
|
+
dest="db_command", required=True
|
|
282
|
+
)
|
|
283
|
+
MigrateCommand.register_subparser(
|
|
284
|
+
db_subparsers, "migrate", "Run migrations"
|
|
285
|
+
)
|
|
286
|
+
```
|
|
287
|
+
"""
|
|
288
|
+
if help_text is None:
|
|
289
|
+
help_text = cls.__doc__.strip() if cls.__doc__ else f"{name} command"
|
|
290
|
+
|
|
291
|
+
parser = subparsers.add_parser(
|
|
292
|
+
name,
|
|
293
|
+
help=help_text,
|
|
294
|
+
description=help_text,
|
|
295
|
+
aliases=aliases or [],
|
|
296
|
+
)
|
|
297
|
+
cls.build_args(parser)
|
|
298
|
+
parser.set_defaults(func=cls.func)
|
|
299
|
+
return parser
|
|
300
|
+
|
|
301
|
+
@model_validator(mode="wrap")
|
|
302
|
+
@classmethod
|
|
303
|
+
def reraise(cls, data: t.Any, handler: ModelWrapValidatorHandler[t.Self]) -> t.Self:
|
|
304
|
+
"""Reraise validation errors with clearer messages.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
data: The input data to validate.
|
|
308
|
+
handler: The model wrap validator handler.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
The validated model instance.
|
|
312
|
+
|
|
313
|
+
Raises:
|
|
314
|
+
InvalidArgumentError: If validation fails.
|
|
315
|
+
"""
|
|
316
|
+
try:
|
|
317
|
+
return handler(data)
|
|
318
|
+
except ValidationError as e:
|
|
319
|
+
raise InvalidArgumentError.from_validation_error(e) from e
|
|
320
|
+
|
|
321
|
+
model_config = ConfigDict(
|
|
322
|
+
extra="ignore",
|
|
323
|
+
frozen=True,
|
|
324
|
+
arbitrary_types_allowed=True, # Allow types like pathlib.Path
|
|
325
|
+
populate_by_name=True, # Allow population by field name
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def parser_with_version(
|
|
330
|
+
prog: str = __title__,
|
|
331
|
+
description: str = __description__,
|
|
332
|
+
) -> argparse.ArgumentParser:
|
|
333
|
+
"""Create an argument parser with --version and --help built-in.
|
|
334
|
+
|
|
335
|
+
Helper function to create a parser with common options.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
prog: Program name.
|
|
339
|
+
description: Program description.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Configured ArgumentParser with version info.
|
|
343
|
+
"""
|
|
344
|
+
parser = argparse.ArgumentParser(
|
|
345
|
+
prog=prog,
|
|
346
|
+
description=description,
|
|
347
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
348
|
+
)
|
|
349
|
+
parser.add_argument(
|
|
350
|
+
"--version",
|
|
351
|
+
"-V",
|
|
352
|
+
action="version",
|
|
353
|
+
version=f"%(prog)s {__version__}",
|
|
354
|
+
help="Show the version number and exit.",
|
|
355
|
+
)
|
|
356
|
+
return parser
|
audex/cli/exceptions.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
from audex.exceptions import AudexError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CLIError(AudexError):
|
|
11
|
+
exit_code: t.ClassVar[int] = 1
|
|
12
|
+
code = 0x30
|
|
13
|
+
default_message = "An error occurred in Audex CLI."
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InvalidArgumentError(CLIError):
|
|
17
|
+
default_message = "Invalid argument provided: {arg}={value!r}\nReason: {reason}"
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str | None = None, *, arg: str, value: t.Any, reason: str) -> None:
|
|
20
|
+
if message is None:
|
|
21
|
+
message = self.default_message.format(arg=arg, value=value, reason=reason)
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def from_validation_error(cls, error: ValidationError) -> t.Self:
|
|
26
|
+
errors = "; ".join(
|
|
27
|
+
f"{'.'.join(str(loc) for loc in err['loc'])}: {err['msg']}" for err in error.errors()
|
|
28
|
+
)
|
|
29
|
+
return cls(
|
|
30
|
+
arg="; ".join(str(loc) for loc in error.errors()[0]["loc"]),
|
|
31
|
+
value="; ".join(
|
|
32
|
+
str(err["ctx"]["given"]) for err in error.errors() if "given" in err.get("ctx", {})
|
|
33
|
+
),
|
|
34
|
+
reason=errors,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class IllegalOperationError(CLIError):
|
|
39
|
+
default_message = "Illegal operation: {operation}\nReason: {reason}"
|
|
40
|
+
|
|
41
|
+
def __init__(self, message: str | None = None, *, operation: str, reason: str) -> None:
|
|
42
|
+
if message is None:
|
|
43
|
+
message = self.default_message.format(operation=operation, reason=reason)
|
|
44
|
+
super().__init__(message)
|
|
File without changes
|
audex/cli/helper/ansi.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ANSI:
|
|
9
|
+
"""Enhanced formatter for ANSI color and style formatting in console
|
|
10
|
+
output.
|
|
11
|
+
|
|
12
|
+
Provides organized color constants, helper methods, and context
|
|
13
|
+
managers for applying consistent styling to terminal output.
|
|
14
|
+
Automatically detects color support in the terminal environment.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
class FG(enum.StrEnum):
|
|
18
|
+
"""Foreground colors."""
|
|
19
|
+
|
|
20
|
+
BLACK = "\033[30m"
|
|
21
|
+
RED = "\033[31m"
|
|
22
|
+
GREEN = "\033[32m"
|
|
23
|
+
YELLOW = "\033[33m"
|
|
24
|
+
BLUE = "\033[34m"
|
|
25
|
+
MAGENTA = "\033[35m"
|
|
26
|
+
CYAN = "\033[36m"
|
|
27
|
+
WHITE = "\033[37m"
|
|
28
|
+
GRAY = "\033[90m"
|
|
29
|
+
BRIGHT_RED = "\033[91m"
|
|
30
|
+
BRIGHT_GREEN = "\033[92m"
|
|
31
|
+
BRIGHT_YELLOW = "\033[93m"
|
|
32
|
+
BRIGHT_BLUE = "\033[94m"
|
|
33
|
+
BRIGHT_MAGENTA = "\033[95m"
|
|
34
|
+
BRIGHT_CYAN = "\033[96m"
|
|
35
|
+
BRIGHT_WHITE = "\033[97m"
|
|
36
|
+
|
|
37
|
+
class BG(enum.StrEnum):
|
|
38
|
+
"""Background colors."""
|
|
39
|
+
|
|
40
|
+
BLACK = "\033[40m"
|
|
41
|
+
RED = "\033[41m"
|
|
42
|
+
GREEN = "\033[42m"
|
|
43
|
+
YELLOW = "\033[43m"
|
|
44
|
+
BLUE = "\033[44m"
|
|
45
|
+
MAGENTA = "\033[45m"
|
|
46
|
+
CYAN = "\033[46m"
|
|
47
|
+
WHITE = "\033[47m"
|
|
48
|
+
GRAY = "\033[100m"
|
|
49
|
+
BRIGHT_RED = "\033[101m"
|
|
50
|
+
BRIGHT_GREEN = "\033[102m"
|
|
51
|
+
BRIGHT_YELLOW = "\033[103m"
|
|
52
|
+
BRIGHT_BLUE = "\033[104m"
|
|
53
|
+
BRIGHT_MAGENTA = "\033[105m"
|
|
54
|
+
BRIGHT_CYAN = "\033[106m"
|
|
55
|
+
BRIGHT_WHITE = "\033[107m"
|
|
56
|
+
|
|
57
|
+
class STYLE(enum.StrEnum):
|
|
58
|
+
"""Text styles."""
|
|
59
|
+
|
|
60
|
+
RESET = "\033[0m"
|
|
61
|
+
BOLD = "\033[1m"
|
|
62
|
+
DIM = "\033[2m"
|
|
63
|
+
ITALIC = "\033[3m"
|
|
64
|
+
UNDERLINE = "\033[4m"
|
|
65
|
+
BLINK = "\033[5m"
|
|
66
|
+
REVERSE = "\033[7m"
|
|
67
|
+
HIDDEN = "\033[8m"
|
|
68
|
+
STRIKETHROUGH = "\033[9m"
|
|
69
|
+
|
|
70
|
+
# For backward compatibility
|
|
71
|
+
RESET = STYLE.RESET
|
|
72
|
+
BOLD = STYLE.BOLD
|
|
73
|
+
UNDERLINE = STYLE.UNDERLINE
|
|
74
|
+
REVERSED = STYLE.REVERSE
|
|
75
|
+
RED = FG.BRIGHT_RED
|
|
76
|
+
GREEN = FG.BRIGHT_GREEN
|
|
77
|
+
YELLOW = FG.BRIGHT_YELLOW
|
|
78
|
+
BLUE = FG.BRIGHT_BLUE
|
|
79
|
+
MAGENTA = FG.BRIGHT_MAGENTA
|
|
80
|
+
CYAN = FG.BRIGHT_CYAN
|
|
81
|
+
WHITE = FG.BRIGHT_WHITE
|
|
82
|
+
|
|
83
|
+
# Control whether ANSI colors are enabled
|
|
84
|
+
_enabled = True
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def supports_color(cls) -> bool:
|
|
88
|
+
"""Determine if the current terminal supports colors.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
bool: True if the terminal supports colors, False otherwise.
|
|
92
|
+
"""
|
|
93
|
+
# Check for NO_COLOR environment variable (https://no-color.org/)
|
|
94
|
+
if os.environ.get("NO_COLOR", ""):
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
# Check for explicit color control
|
|
98
|
+
if os.environ.get("FORCE_COLOR", ""):
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
# Check if stdout is a TTY
|
|
102
|
+
return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def enable(cls, enabled: bool = True) -> None:
|
|
106
|
+
"""Enable or disable ANSI formatting.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
enabled: True to enable colors, False to disable.
|
|
110
|
+
"""
|
|
111
|
+
cls._enabled = enabled and cls.supports_color()
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def format(cls, text: str, /, *styles: STYLE | FG | BG) -> str:
|
|
115
|
+
"""Format text with the specified ANSI styles.
|
|
116
|
+
|
|
117
|
+
Intelligently reapplies styles after any reset sequences in the text.
|
|
118
|
+
If colors are disabled, returns the original text without formatting.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
text: The text to format.
|
|
122
|
+
*styles: One or more ANSI style codes to apply.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
The formatted text with ANSI styles applied.
|
|
126
|
+
"""
|
|
127
|
+
if not cls._enabled or not styles:
|
|
128
|
+
return text
|
|
129
|
+
|
|
130
|
+
# Filter out None values and get the actual string values from enums
|
|
131
|
+
valid_styles = [
|
|
132
|
+
str(s.value) if hasattr(s, "value") else str(s) for s in styles if s is not None
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
if not valid_styles:
|
|
136
|
+
return text
|
|
137
|
+
|
|
138
|
+
style_str = "".join(valid_styles)
|
|
139
|
+
|
|
140
|
+
# Handle text that already contains reset codes
|
|
141
|
+
if cls.STYLE.RESET in text:
|
|
142
|
+
text = text.replace(cls.STYLE.RESET, f"{cls.STYLE.RESET}{style_str}")
|
|
143
|
+
|
|
144
|
+
return f"{style_str}{text}{cls.STYLE.RESET}"
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def success(cls, text: str, /) -> str:
|
|
148
|
+
"""Format text as a success message (green, bold)."""
|
|
149
|
+
return cls.format(text, cls.FG.BRIGHT_GREEN, cls.STYLE.BOLD)
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def error(cls, text: str, /) -> str:
|
|
153
|
+
"""Format text as an error message (red, bold)."""
|
|
154
|
+
return cls.format(text, cls.FG.BRIGHT_RED, cls.STYLE.BOLD)
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def warning(cls, text: str, /) -> str:
|
|
158
|
+
"""Format text as a warning message (yellow, bold)."""
|
|
159
|
+
return cls.format(text, cls.FG.BRIGHT_YELLOW, cls.STYLE.BOLD)
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def info(cls, text: str, /) -> str:
|
|
163
|
+
"""Format text as an info message (cyan)."""
|
|
164
|
+
return cls.format(text, cls.FG.BRIGHT_CYAN)
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
def highlight(cls, text: str, /) -> str:
|
|
168
|
+
"""Format text as highlighted (magenta, bold)."""
|
|
169
|
+
return cls.format(text, cls.FG.BRIGHT_MAGENTA, cls.STYLE.BOLD)
|
|
170
|
+
|
|
171
|
+
@classmethod
|
|
172
|
+
def rgb(cls, text: str, /, r: int, g: int, b: int, background: bool = False) -> str:
|
|
173
|
+
"""Format text with a specific RGB color.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
text: The text to format
|
|
177
|
+
r: Red value (0-255)
|
|
178
|
+
g: Green value (0-255)
|
|
179
|
+
b: Blue value (0-255)
|
|
180
|
+
background: If True, set as background color instead of foreground
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Formatted text with the specified RGB color
|
|
184
|
+
"""
|
|
185
|
+
if not cls._enabled:
|
|
186
|
+
return text
|
|
187
|
+
|
|
188
|
+
code = 48 if background else 38
|
|
189
|
+
color_seq = f"\033[{code};2;{r};{g};{b}m"
|
|
190
|
+
return f"{color_seq}{text}{cls.STYLE.RESET}"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
ANSI.enable()
|