modusa 0.4.29__py3-none-any.whl → 0.4.30__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.
- modusa/__init__.py +9 -8
- modusa/tools/__init__.py +7 -2
- modusa/tools/ann_saver.py +30 -0
- modusa/tools/audio_recorder.py +0 -1
- modusa/tools/youtube_downloader.py +1 -4
- {modusa-0.4.29.dist-info → modusa-0.4.30.dist-info}/METADATA +2 -2
- modusa-0.4.30.dist-info/RECORD +21 -0
- pyproject.toml +2 -2
- modusa/config.py +0 -18
- modusa/decorators.py +0 -176
- modusa/devtools/generate_docs_source.py +0 -92
- modusa/devtools/generate_template.py +0 -144
- modusa/devtools/list_authors.py +0 -2
- modusa/devtools/list_plugins.py +0 -60
- modusa/devtools/main.py +0 -45
- modusa/devtools/templates/generator.py +0 -24
- modusa/devtools/templates/io.py +0 -24
- modusa/devtools/templates/model.py +0 -47
- modusa/devtools/templates/plugin.py +0 -41
- modusa/devtools/templates/test.py +0 -10
- modusa/devtools/templates/tool.py +0 -24
- modusa/generators/__init__.py +0 -13
- modusa/generators/audio.py +0 -188
- modusa/generators/audio_waveforms.py +0 -236
- modusa/generators/base.py +0 -29
- modusa/generators/ftds.py +0 -298
- modusa/generators/s1d.py +0 -270
- modusa/generators/s2d.py +0 -300
- modusa/generators/s_ax.py +0 -102
- modusa/generators/t_ax.py +0 -64
- modusa/generators/tds.py +0 -267
- modusa/models/__init__.py +0 -14
- modusa/models/audio.py +0 -90
- modusa/models/base.py +0 -70
- modusa/models/data.py +0 -457
- modusa/models/ftds.py +0 -584
- modusa/models/s1d.py +0 -578
- modusa/models/s2d.py +0 -619
- modusa/models/s_ax.py +0 -448
- modusa/models/t_ax.py +0 -335
- modusa/models/tds.py +0 -465
- modusa/plugins/__init__.py +0 -3
- modusa/plugins/base.py +0 -100
- modusa/tools/_plotter_old.py +0 -629
- modusa/tools/audio_saver.py +0 -30
- modusa/tools/base.py +0 -43
- modusa/tools/math_ops.py +0 -335
- modusa/utils/__init__.py +0 -1
- modusa/utils/config.py +0 -25
- modusa/utils/excp.py +0 -49
- modusa/utils/logger.py +0 -18
- modusa/utils/np_func_cat.py +0 -44
- modusa/utils/plot.py +0 -142
- modusa-0.4.29.dist-info/RECORD +0 -65
- {modusa-0.4.29.dist-info → modusa-0.4.30.dist-info}/WHEEL +0 -0
- {modusa-0.4.29.dist-info → modusa-0.4.30.dist-info}/entry_points.txt +0 -0
- {modusa-0.4.29.dist-info → modusa-0.4.30.dist-info}/licenses/LICENSE.md +0 -0
modusa/__init__.py
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
-
|
1
|
+
# Audio related
|
2
|
+
from modusa.tools import load, play, convert, record
|
3
|
+
from modusa.tools import download
|
2
4
|
|
3
|
-
|
4
|
-
from modusa.tools import
|
5
|
-
#=====
|
5
|
+
# Annotation related
|
6
|
+
from modusa.tools import load_ann, save_ann
|
6
7
|
|
7
|
-
|
8
|
-
from modusa.tools import
|
9
|
-
from modusa.tools import load, load_ann
|
8
|
+
# Plotting related
|
9
|
+
from modusa.tools import dist_plot, hill_plot, plot, fig
|
10
10
|
|
11
|
+
# Synthsizing related
|
11
12
|
from modusa.tools import synth_f0
|
12
13
|
|
13
|
-
__version__ = "0.4.
|
14
|
+
__version__ = "0.4.30" # This is dynamically used by the documentation, and pyproject.toml; Only need to change it here; rest gets taken care of.
|
modusa/tools/__init__.py
CHANGED
@@ -1,14 +1,19 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
|
3
|
+
# Audio related
|
3
4
|
from .audio_player import play
|
4
5
|
from .audio_converter import convert
|
5
6
|
from .youtube_downloader import download
|
6
7
|
from .audio_loader import load
|
7
|
-
from .audio_saver import save
|
8
|
-
from .ann_loader import load_ann
|
9
8
|
from .audio_recorder import record
|
10
9
|
|
10
|
+
# Annotation related
|
11
|
+
from .ann_loader import load_ann
|
12
|
+
from .ann_saver import save_ann
|
13
|
+
|
14
|
+
# Plotting related
|
11
15
|
from .plotter import Fig as fig
|
12
16
|
from .plotter import dist_plot, hill_plot, plot
|
13
17
|
|
18
|
+
# Synthesizing related
|
14
19
|
from .synth import synth_f0
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
#---------------------------------
|
4
|
+
# Author: Ankit Anand
|
5
|
+
# Date: 23/10/25
|
6
|
+
# Email: ankit0.anand0@gmail.com
|
7
|
+
#---------------------------------
|
8
|
+
|
9
|
+
from pathlib import Path
|
10
|
+
|
11
|
+
def save_ann(ann, output_fp):
|
12
|
+
"""
|
13
|
+
Saves annotation as a text file.
|
14
|
+
It can be opened in audacity for inspection.
|
15
|
+
|
16
|
+
Paramters
|
17
|
+
---------
|
18
|
+
ann: list[tuple[float, float, str]]
|
19
|
+
- List of (start, end, label).
|
20
|
+
output_fp: str
|
21
|
+
- Filepath to save the annotation.
|
22
|
+
"""
|
23
|
+
|
24
|
+
output_fp = Path(output_fp)
|
25
|
+
output_fp.parent.mkdir(parents=True, exist_ok=True)
|
26
|
+
|
27
|
+
with open(output_fp, "w") as f:
|
28
|
+
for (s, e, label) in ann:
|
29
|
+
f.write(f"{s:.6f}\t{e:.6f}\t{label}\n")
|
30
|
+
|
modusa/tools/audio_recorder.py
CHANGED
@@ -1,9 +1,6 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
|
3
3
|
|
4
|
-
from modusa import excp
|
5
|
-
from modusa.decorators import validate_args_type
|
6
|
-
from modusa.tools.base import ModusaTool
|
7
4
|
from typing import Any
|
8
5
|
from pathlib import Path
|
9
6
|
import yt_dlp
|
@@ -66,7 +63,7 @@ def download(url, content_type, output_dir):
|
|
66
63
|
info = ydl.extract_info(url, download=True)
|
67
64
|
return Path(info['requested_downloads'][0]['filepath'])
|
68
65
|
else:
|
69
|
-
raise
|
66
|
+
raise ValueError(f"`content_type` can either take 'audio' or 'video' not {content_type}")
|
70
67
|
|
71
68
|
|
72
69
|
|
@@ -1,13 +1,13 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: modusa
|
3
|
-
Version: 0.4.
|
3
|
+
Version: 0.4.30
|
4
4
|
Summary: A modular signal analysis python library.
|
5
5
|
Author-Email: Ankit Anand <ankit0.anand0@gmail.com>
|
6
6
|
License: MIT
|
7
7
|
Requires-Python: >=3.11
|
8
8
|
Requires-Dist: numpy>=2.2.6
|
9
9
|
Requires-Dist: matplotlib>=3.10.3
|
10
|
-
Requires-Dist: yt-dlp==2025.
|
10
|
+
Requires-Dist: yt-dlp==2025.10.22
|
11
11
|
Requires-Dist: IPython>=9.5.0
|
12
12
|
Requires-Dist: sounddevice>=0.5.2
|
13
13
|
Requires-Dist: ipywidgets>=8.1.7
|
@@ -0,0 +1,21 @@
|
|
1
|
+
LICENSE.md,sha256=JTaXAjx5awk76VArKCx5dUW8vmLEWsL_ZlR7-umaHbA,1078
|
2
|
+
README.md,sha256=Ybe2rcDecfSCiUpec2n7btQvgyi4R9JG0bfdwSWijWk,981
|
3
|
+
modusa-0.4.30.dist-info/METADATA,sha256=6uwfk2M5n8Nm403plD97tZhsFlDjsrCWCGe5Q6uViLM,1443
|
4
|
+
modusa-0.4.30.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
|
5
|
+
modusa-0.4.30.dist-info/entry_points.txt,sha256=fmKpleVXj6CdaBVL14WoEy6xx7JQCs85jvzwTi3lePM,73
|
6
|
+
modusa-0.4.30.dist-info/licenses/LICENSE.md,sha256=JTaXAjx5awk76VArKCx5dUW8vmLEWsL_ZlR7-umaHbA,1078
|
7
|
+
modusa/__init__.py,sha256=Q5A97PDmbcNLEwJZKg3W660TcVyJbbJfTvjct8Y_X9s,450
|
8
|
+
modusa/fonts/NotoSansDevanagari-Regular.ttf,sha256=CEqU2J61Sq-5OgVuFUJcNP2Fn2NCh1Fl0wSDezvPwtI,221084
|
9
|
+
modusa/images/icon.png,sha256=UgGXlL4xjdIYxqO1en3iQfNN1BFsazxpT7NiPZSDu70,13321
|
10
|
+
modusa/tools/__init__.py,sha256=9tIk1j1lMJ8Ka04ppLwEad_RU-s1Ok9CRalfNhai01A,453
|
11
|
+
modusa/tools/ann_loader.py,sha256=m6Qu6jXnQ8LfUhKItoHSaHlGxUyzUJlGEyu4_50qJ8w,3099
|
12
|
+
modusa/tools/ann_saver.py,sha256=rB2gP1rD69PkSnBZrzc_83ylb7eAyqyCMjn9Bh16VaM,650
|
13
|
+
modusa/tools/audio_converter.py,sha256=415qBoPm2sBIuBSI7m1XBKm0AbmVmPydIPPr-uO8D3c,1778
|
14
|
+
modusa/tools/audio_loader.py,sha256=xmeodrJqJV5j9-lrUPk3W9-bdEA4-6RvTAn8pIErUN0,4084
|
15
|
+
modusa/tools/audio_player.py,sha256=kyBUnodkOE9Ox-hKHkfPeGAQ1RPTddbZYXO1ezz6-9w,2494
|
16
|
+
modusa/tools/audio_recorder.py,sha256=cADeeUpPcDIYBIuuVZvWqulDkv7TavkpL3o3SO9QyKc,2787
|
17
|
+
modusa/tools/plotter.py,sha256=QfF8kCi79nS-J5lQyEuH_BbAmTo-PdfjDbnK9yTAmRE,30863
|
18
|
+
modusa/tools/synth.py,sha256=tfCjTAucTDwb0yOHHnrNO7EFsuH7tNb1PwvS2xwmhK0,1222
|
19
|
+
modusa/tools/youtube_downloader.py,sha256=Ij7fipSlRpsf0pFOwRY-j7Yf2PMp0kpWMhyMQchR0e0,1574
|
20
|
+
pyproject.toml,sha256=zBJWAUi45I8BE-MxTX-ZYMBzNpvhiYE8zcKUviuHNFc,1414
|
21
|
+
modusa-0.4.30.dist-info/RECORD,,
|
pyproject.toml
CHANGED
@@ -8,7 +8,7 @@ authors = [
|
|
8
8
|
dependencies = [
|
9
9
|
"numpy>=2.2.6",
|
10
10
|
"matplotlib>=3.10.3",
|
11
|
-
"yt-dlp==2025.
|
11
|
+
"yt-dlp==2025.10.22",
|
12
12
|
"IPython>=9.5.0",
|
13
13
|
"sounddevice>=0.5.2",
|
14
14
|
"ipywidgets>=8.1.7",
|
@@ -16,7 +16,7 @@ dependencies = [
|
|
16
16
|
]
|
17
17
|
requires-python = ">=3.11"
|
18
18
|
readme = "README.md"
|
19
|
-
version = "0.4.
|
19
|
+
version = "0.4.30"
|
20
20
|
|
21
21
|
[project.license]
|
22
22
|
text = "MIT"
|
modusa/config.py
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
|
3
|
-
import logging
|
4
|
-
|
5
|
-
class Config:
|
6
|
-
LOG_LEVEL = logging.WARNING
|
7
|
-
SR = 44100 # Default sampling rate
|
8
|
-
TIME_UNIT = "sec"
|
9
|
-
|
10
|
-
|
11
|
-
def __str__(self):
|
12
|
-
return self.__dict__
|
13
|
-
|
14
|
-
def __repr__(self):
|
15
|
-
return self.__dict__
|
16
|
-
|
17
|
-
# Create a singleton instance
|
18
|
-
config = Config()
|
modusa/decorators.py
DELETED
@@ -1,176 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
|
3
|
-
from modusa import excp
|
4
|
-
from functools import wraps
|
5
|
-
from typing import Any, Callable, Type
|
6
|
-
from inspect import signature, Parameter
|
7
|
-
from typing import get_origin, get_args, Union
|
8
|
-
import types
|
9
|
-
|
10
|
-
|
11
|
-
#----------------------------------------------------
|
12
|
-
# Safety check for plugin (apply method)
|
13
|
-
# Check if the input type, output type is allowed,
|
14
|
-
# Also logs plugin usage.
|
15
|
-
#----------------------------------------------------
|
16
|
-
def plugin_safety_check(
|
17
|
-
validate_plugin_input: bool = True,
|
18
|
-
validate_plugin_output: bool = True,
|
19
|
-
track_plugin_usage: bool = True
|
20
|
-
):
|
21
|
-
def decorator(func: Callable) -> Callable:
|
22
|
-
@wraps(func)
|
23
|
-
def wrapper(self, signal: Any, *args, **kwargs):
|
24
|
-
|
25
|
-
if validate_plugin_input:
|
26
|
-
if not hasattr(self, 'allowed_input_signal_types'):
|
27
|
-
raise excp.AttributeNotFoundError(f"{self.__class__.__name__} must define `allowed_input_signal_types`.")
|
28
|
-
|
29
|
-
if type(signal) not in self.allowed_input_signal_types:
|
30
|
-
raise excp.PluginInputError(f"{self.__class__.__name__} must take input signal of type {self.allowed_input_signal_types} but got {type(signal)}")
|
31
|
-
|
32
|
-
if track_plugin_usage:
|
33
|
-
if not hasattr(signal, '_plugin_chain'):
|
34
|
-
raise excp.AttributeNotFoundError(f"Signal of type {type(signal).__name__} must have a `_plugin_chain` attribute for plugin tracking.")
|
35
|
-
|
36
|
-
if not isinstance(signal._plugin_chain, list):
|
37
|
-
raise excp.TypeError(f"`_plugin_chain` must be a list, but got {type(signal._plugin_chain)}")
|
38
|
-
|
39
|
-
signal._plugin_chain.append(self.__class__.__name__)
|
40
|
-
|
41
|
-
result = func(self, signal, *args, **kwargs)
|
42
|
-
|
43
|
-
if validate_plugin_output:
|
44
|
-
if not hasattr(self, 'allowed_output_signal_types'):
|
45
|
-
raise excp.AttributeNotFoundError(f"{self.__class__.__name__} must define `allowed_output_signal_types`.")
|
46
|
-
if type(result) not in self.allowed_output_signal_types:
|
47
|
-
raise excp.PluginInputError(f"{self.__class__.__name__} must return output of type {self.allowed_output_signal_types} but returned {type(result)}")
|
48
|
-
return result
|
49
|
-
|
50
|
-
return wrapper
|
51
|
-
return decorator
|
52
|
-
|
53
|
-
|
54
|
-
#----------------------------------------------------
|
55
|
-
# Safety check for generators (generate method)
|
56
|
-
# Check if the ouput type is allowed.
|
57
|
-
#----------------------------------------------------
|
58
|
-
def generator_safety_check():
|
59
|
-
"""
|
60
|
-
We assume that the first argument is self, so that we can actually extract properties to
|
61
|
-
validate.
|
62
|
-
"""
|
63
|
-
def decorator(func: Callable) -> Callable:
|
64
|
-
@wraps(func)
|
65
|
-
def wrapper(self, *args, **kwargs):
|
66
|
-
result = func(self, *args, **kwargs)
|
67
|
-
|
68
|
-
if not hasattr(self, 'allowed_output_signal_types'):
|
69
|
-
raise excp.AttributeNotFoundError(
|
70
|
-
f"{self.__class__.__name__} must define `allowed_output_signal_types`."
|
71
|
-
)
|
72
|
-
if type(result) not in self.allowed_output_signal_types:
|
73
|
-
raise excp.PluginInputError(
|
74
|
-
f"{self.__class__.__name__} must return output of type {self.allowed_output_signal_types}, "
|
75
|
-
f"but returned {type(result)}"
|
76
|
-
)
|
77
|
-
|
78
|
-
return result
|
79
|
-
return wrapper
|
80
|
-
return decorator
|
81
|
-
|
82
|
-
|
83
|
-
#----------------------------------------------------
|
84
|
-
# Validation for args type
|
85
|
-
# When this decorator is added to a function, it
|
86
|
-
# automatically checks all the arguments with their
|
87
|
-
# expected types. (self, forward type references are
|
88
|
-
# ignored)
|
89
|
-
#----------------------------------------------------
|
90
|
-
|
91
|
-
def validate_arg(arg_name: str, value: Any, expected_type: Any) -> None:
|
92
|
-
"""
|
93
|
-
Checks if `value_type` matches `expected_type`.
|
94
|
-
Raises TypeError if not.
|
95
|
-
"""
|
96
|
-
import types
|
97
|
-
from typing import get_origin, get_args, Union
|
98
|
-
|
99
|
-
origin = get_origin(expected_type)
|
100
|
-
|
101
|
-
# Handle Union (e.g. int | None)
|
102
|
-
if origin in (Union, types.UnionType):
|
103
|
-
union_args = get_args(expected_type)
|
104
|
-
for typ in union_args:
|
105
|
-
typ_origin = get_origin(typ) or typ
|
106
|
-
if isinstance(value, typ_origin):
|
107
|
-
return
|
108
|
-
|
109
|
-
# ❌ If none match
|
110
|
-
expected_names = ", ".join(
|
111
|
-
get_origin(t).__name__ if get_origin(t) else t.__name__ for t in union_args
|
112
|
-
)
|
113
|
-
raise excp.InputTypeError(
|
114
|
-
f"Argument '{arg_name}' must be one of ({expected_names}), got {type(value).__name__}"
|
115
|
-
)
|
116
|
-
|
117
|
-
# Handle generic types like list[float], tuple[int, str]
|
118
|
-
elif origin is not None:
|
119
|
-
if not isinstance(value, origin):
|
120
|
-
raise excp.InputTypeError(
|
121
|
-
f"Argument '{arg_name}' must be of type {origin.__name__}, got {type(value).__name__}"
|
122
|
-
)
|
123
|
-
return
|
124
|
-
|
125
|
-
# ✅ Handle plain types
|
126
|
-
elif isinstance(expected_type, type):
|
127
|
-
if not isinstance(value, expected_type):
|
128
|
-
raise excp.InputTypeError(
|
129
|
-
f"Argument '{arg_name}' must be of type {expected_type.__name__}, got {type(value).__name__}"
|
130
|
-
)
|
131
|
-
return
|
132
|
-
# ❌ Unsupported type structure
|
133
|
-
else:
|
134
|
-
raise excp.InputTypeError(f"Unsupported annotation for '{arg_name}': {expected_type}")
|
135
|
-
|
136
|
-
def validate_args_type() -> Callable:
|
137
|
-
def decorator(func: Callable) -> Callable:
|
138
|
-
@wraps(func)
|
139
|
-
def wrapper(*args, **kwargs):
|
140
|
-
sig = signature(func)
|
141
|
-
bound = sig.bind(*args, **kwargs)
|
142
|
-
bound.apply_defaults()
|
143
|
-
|
144
|
-
for arg_name, value in bound.arguments.items():
|
145
|
-
param = sig.parameters[arg_name]
|
146
|
-
expected_type = param.annotation
|
147
|
-
|
148
|
-
# Skip unannotated or special args
|
149
|
-
if expected_type is Parameter.empty or arg_name in ("self", "cls") or isinstance(expected_type, str):
|
150
|
-
continue
|
151
|
-
|
152
|
-
validate_arg(arg_name, value, expected_type) # <- this is assumed to be defined elsewhere
|
153
|
-
|
154
|
-
return func(*args, **kwargs)
|
155
|
-
return wrapper
|
156
|
-
return decorator
|
157
|
-
|
158
|
-
#-----------------------------------
|
159
|
-
# Making a property immutable
|
160
|
-
# and raising custom error message
|
161
|
-
# during attempt to modify the values
|
162
|
-
#-----------------------------------
|
163
|
-
def immutable_property(error_msg: str):
|
164
|
-
"""
|
165
|
-
Returns a read-only property. Raises an error with a custom message on mutation.
|
166
|
-
"""
|
167
|
-
def decorator(getter):
|
168
|
-
name = getter.__name__
|
169
|
-
private_name = f"_{name}"
|
170
|
-
|
171
|
-
def setter(self, value):
|
172
|
-
raise excp.ImmutableAttributeError(error_msg)
|
173
|
-
|
174
|
-
return property(getter, setter)
|
175
|
-
|
176
|
-
return decorator
|
@@ -1,92 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
|
3
|
-
import inspect
|
4
|
-
import importlib
|
5
|
-
import pkgutil
|
6
|
-
from pathlib import Path
|
7
|
-
from collections import defaultdict
|
8
|
-
|
9
|
-
# === Configuration ===
|
10
|
-
BASE_MODULES = [
|
11
|
-
'modusa.tools',
|
12
|
-
# 'modusa.models',
|
13
|
-
# 'modusa.generators',
|
14
|
-
# 'modusa.plugins',
|
15
|
-
]
|
16
|
-
OUTPUT_DIRS = [
|
17
|
-
Path('docs/source/tools'),
|
18
|
-
# Path('docs/source/models'),
|
19
|
-
# Path('docs/source/generators'),
|
20
|
-
# Path('docs/source/plugins'),
|
21
|
-
]
|
22
|
-
|
23
|
-
# Ensure output directories exist
|
24
|
-
for output_dir in OUTPUT_DIRS:
|
25
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
26
|
-
|
27
|
-
# === Utils ===
|
28
|
-
def get_classes_grouped_by_module(base_module):
|
29
|
-
"""
|
30
|
-
Returns a dictionary: { module_path: [class_name, ...] }
|
31
|
-
"""
|
32
|
-
found = defaultdict(list)
|
33
|
-
module = importlib.import_module(base_module)
|
34
|
-
|
35
|
-
for _, modname, _ in pkgutil.walk_packages(module.__path__, base_module + "."):
|
36
|
-
try:
|
37
|
-
submodule = importlib.import_module(modname)
|
38
|
-
for name, obj in inspect.getmembers(submodule, inspect.isclass):
|
39
|
-
if obj.__module__ == modname:
|
40
|
-
found[modname].append(name)
|
41
|
-
except Exception as e:
|
42
|
-
print(f"⚠️ Skipping {modname} due to import error: {e}")
|
43
|
-
return found
|
44
|
-
|
45
|
-
def write_module_rst_file(module_path, class_names, output_dir):
|
46
|
-
"""
|
47
|
-
Writes a .rst file for a module, documenting all its classes.
|
48
|
-
Filename is based on the module, but title is the first class (Modusa* gets priority).
|
49
|
-
"""
|
50
|
-
filename = output_dir / f"{module_path.split('.')[-1]}.rst"
|
51
|
-
|
52
|
-
# Prioritize 'Modusa*' classes first, then alphabetical
|
53
|
-
sorted_classes = sorted(class_names, key=lambda x: (not x.startswith("Modusa"), x.lower()))
|
54
|
-
title = sorted_classes[0] if sorted_classes else module_path.split('.')[-1]
|
55
|
-
|
56
|
-
with open(filename, 'w') as f:
|
57
|
-
f.write(f"{title}\n{'=' * len(title)}\n\n")
|
58
|
-
for class_name in sorted_classes:
|
59
|
-
f.write(f".. autoclass:: {module_path}.{class_name}\n")
|
60
|
-
f.write(" :members:\n")
|
61
|
-
f.write(" :undoc-members:\n")
|
62
|
-
f.write(" :show-inheritance:\n\n")
|
63
|
-
|
64
|
-
return filename.name
|
65
|
-
|
66
|
-
def write_index_rst_file(tools_by_module, output_dir, section_name="Tools"):
|
67
|
-
"""
|
68
|
-
Writes index.rst in the given output_dir with 'base' on top, then other files alphabetically.
|
69
|
-
"""
|
70
|
-
index_file = output_dir / "index.rst"
|
71
|
-
with open(index_file, "w") as f:
|
72
|
-
f.write(f"{section_name}\n{'=' * len(section_name)}\n\n")
|
73
|
-
f.write(".. toctree::\n :maxdepth: 1\n\n")
|
74
|
-
|
75
|
-
filenames = [module_path.split('.')[-1] for module_path in tools_by_module]
|
76
|
-
sorted_filenames = sorted(filenames, key=lambda x: (x != "base", x.lower()))
|
77
|
-
|
78
|
-
for name in sorted_filenames:
|
79
|
-
f.write(f" {name}\n")
|
80
|
-
|
81
|
-
# === Main Script ===
|
82
|
-
def generate_docs_source():
|
83
|
-
for base_module, output_dir in zip(BASE_MODULES, OUTPUT_DIRS):
|
84
|
-
module_class_map = get_classes_grouped_by_module(base_module)
|
85
|
-
|
86
|
-
for module_path, class_list in module_class_map.items():
|
87
|
-
write_module_rst_file(module_path, class_list, output_dir)
|
88
|
-
|
89
|
-
section_name = base_module.split('.')[-1].capitalize()
|
90
|
-
|
91
|
-
write_index_rst_file(module_class_map, output_dir, section_name=section_name)
|
92
|
-
print(f"✅ Documentation generated for {base_module} in {output_dir}")
|
@@ -1,144 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
|
3
|
-
from datetime import date
|
4
|
-
from pathlib import Path
|
5
|
-
import questionary
|
6
|
-
import sys
|
7
|
-
|
8
|
-
ROOT_DIR = Path(__file__).parents[3].resolve()
|
9
|
-
SRC_CODE_DIR = ROOT_DIR / "src/modusa"
|
10
|
-
TESTS_DIR = ROOT_DIR / "tests"
|
11
|
-
TEMPLATES_DIR = ROOT_DIR / "src/modusa/devtools/templates"
|
12
|
-
|
13
|
-
PLUGIN_INFO = {
|
14
|
-
"template_fp": TEMPLATES_DIR / "plugin.py",
|
15
|
-
"test_template_fp": TEMPLATES_DIR / "test.py",
|
16
|
-
"template_dump_dp": SRC_CODE_DIR / "plugins",
|
17
|
-
"test_template_dump_dp": TESTS_DIR / "test_plugins"
|
18
|
-
}
|
19
|
-
IO_INFO = {
|
20
|
-
"template_fp": TEMPLATES_DIR / "io.py",
|
21
|
-
"test_template_fp": TEMPLATES_DIR / "test.py",
|
22
|
-
"template_dump_dp": SRC_CODE_DIR / "io",
|
23
|
-
"test_template_dump_dp": TESTS_DIR / "test_io"
|
24
|
-
}
|
25
|
-
GENERATOR_INFO = {
|
26
|
-
"template_fp": TEMPLATES_DIR / "generator.py",
|
27
|
-
"test_template_fp": TEMPLATES_DIR / "test.py",
|
28
|
-
"template_dump_dp": SRC_CODE_DIR / "generators",
|
29
|
-
"test_template_dump_dp": TESTS_DIR / "test_generators"
|
30
|
-
}
|
31
|
-
MODEL_INFO = {
|
32
|
-
"template_fp": TEMPLATES_DIR / "model.py",
|
33
|
-
"test_template_fp": TEMPLATES_DIR / "test.py",
|
34
|
-
"template_dump_dp": SRC_CODE_DIR / "models",
|
35
|
-
"test_template_dump_dp": TESTS_DIR / "test_models"
|
36
|
-
}
|
37
|
-
TOOL_INFO = {
|
38
|
-
"template_fp": TEMPLATES_DIR / "tool.py",
|
39
|
-
"test_template_fp": TEMPLATES_DIR / "test.py",
|
40
|
-
"template_dump_dp": SRC_CODE_DIR / "tools",
|
41
|
-
"test_template_dump_dp": TESTS_DIR / "test_tools"
|
42
|
-
}
|
43
|
-
|
44
|
-
|
45
|
-
class TemplateGenerator():
|
46
|
-
"""
|
47
|
-
Generates template for `plugin`, `engine`, `signal`, `generator` along with its corresponding `test` file
|
48
|
-
in the `tests` directory.
|
49
|
-
"""
|
50
|
-
|
51
|
-
|
52
|
-
@staticmethod
|
53
|
-
def get_path_info(for_what: str):
|
54
|
-
if for_what == "plugin": return PLUGIN_INFO
|
55
|
-
if for_what == "io": return IO_INFO
|
56
|
-
if for_what == "model": return MODEL_INFO
|
57
|
-
if for_what == "tool": return TOOL_INFO
|
58
|
-
if for_what == "generator": return GENERATOR_INFO
|
59
|
-
|
60
|
-
@staticmethod
|
61
|
-
def ask_questions(for_what: str, path_info: dict) -> dict:
|
62
|
-
"""Asks question about the template to be generated."""
|
63
|
-
print("----------------------")
|
64
|
-
print(for_what.upper())
|
65
|
-
print("----------------------")
|
66
|
-
module_name = questionary.text("Module name (snake_case): ").ask()
|
67
|
-
|
68
|
-
if module_name is None:
|
69
|
-
sys.exit(1)
|
70
|
-
|
71
|
-
if not module_name.endswith(".py"): # Adding extension
|
72
|
-
module_name = module_name + ".py"
|
73
|
-
# Checking if the module name already exists in the dump directory
|
74
|
-
if (path_info["template_dump_dp"] / module_name).exists():
|
75
|
-
print(f"⚠️ File already exists, choose another name.")
|
76
|
-
sys.exit(1)
|
77
|
-
|
78
|
-
class_name = questionary.text(f"Class name (CamelCase): ").ask()
|
79
|
-
if class_name is None:
|
80
|
-
sys.exit(1)
|
81
|
-
|
82
|
-
author_name = questionary.text("Author name: ").ask()
|
83
|
-
if author_name is None:
|
84
|
-
sys.exit(1)
|
85
|
-
|
86
|
-
author_email = questionary.text("Author email: ").ask()
|
87
|
-
if author_email is None:
|
88
|
-
sys.exit(1)
|
89
|
-
|
90
|
-
answers = {"for_what": for_what, "module_name": module_name, "class_name": class_name, "author_name": author_name, "author_email": author_email, "date_created": date.today()}
|
91
|
-
|
92
|
-
return answers
|
93
|
-
|
94
|
-
@staticmethod
|
95
|
-
def load_template_file(template_fp: Path) -> str:
|
96
|
-
"""Loads template file."""
|
97
|
-
if not template_fp.exists():
|
98
|
-
print(f"❌ Template not found: {template_fp}")
|
99
|
-
sys.exit(1)
|
100
|
-
|
101
|
-
template_code = template_fp.read_text()
|
102
|
-
|
103
|
-
return template_code
|
104
|
-
|
105
|
-
@staticmethod
|
106
|
-
def fill_placeholders(template_code: str, placehoders_dict: dict) -> str:
|
107
|
-
"""Fills placeholder in the template with the user input from CLI."""
|
108
|
-
template_code = template_code.format(**placehoders_dict) # Fill placeholders
|
109
|
-
return template_code
|
110
|
-
|
111
|
-
@staticmethod
|
112
|
-
def save_file(content: str, output_path: Path) -> None:
|
113
|
-
"""Saves file in the correct directory with the right tempalate content."""
|
114
|
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
115
|
-
output_path.write_text(content)
|
116
|
-
|
117
|
-
# Generating a corresponding test file too
|
118
|
-
what_for = output_path.parent.name # plugins, generators, ...
|
119
|
-
module_name = output_path.name # this.py
|
120
|
-
|
121
|
-
|
122
|
-
@staticmethod
|
123
|
-
def create_template(for_what: str) -> None:
|
124
|
-
|
125
|
-
# Load correct path location info for the templates and where to dump the files
|
126
|
-
path_info: dict = TemplateGenerator.get_path_info(for_what)
|
127
|
-
|
128
|
-
# Ask basic questions to create the template for `plugin`, `generator`, ...
|
129
|
-
answers: dict = TemplateGenerator.ask_questions(for_what, path_info)
|
130
|
-
|
131
|
-
# Load the correct template file and test file
|
132
|
-
template_code: str = TemplateGenerator.load_template_file(template_fp=path_info['template_fp'])
|
133
|
-
test_code: str = TemplateGenerator.load_template_file(template_fp=path_info['test_template_fp'])
|
134
|
-
|
135
|
-
# Update the dynamic values based on the answers
|
136
|
-
template_code: str = TemplateGenerator.fill_placeholders(template_code, answers)
|
137
|
-
test_code: str = TemplateGenerator.fill_placeholders(test_code, answers)
|
138
|
-
|
139
|
-
# Save it to a file and put it in the correct folder
|
140
|
-
TemplateGenerator.save_file(content=template_code, output_path=path_info['template_dump_dp'] / answers['module_name'])
|
141
|
-
TemplateGenerator.save_file(content=test_code, output_path=path_info['test_template_dump_dp'] / f"test_{answers['module_name']}")
|
142
|
-
|
143
|
-
print(f"✅ {for_what}:", "open " + str(path_info['template_dump_dp'] / answers['module_name']))
|
144
|
-
print(f"✅ test:", "open " + str(path_info['test_template_dump_dp'] / f"test_{answers['module_name']}"))
|
modusa/devtools/list_authors.py
DELETED
modusa/devtools/list_plugins.py
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
|
3
|
-
import inspect
|
4
|
-
from pathlib import Path
|
5
|
-
|
6
|
-
PLUGIN_PATH = Path(__file__).parent.parent / "plugins" # This directory contains all the plugins
|
7
|
-
|
8
|
-
def find_plugin_files():
|
9
|
-
return [
|
10
|
-
path for path in PLUGIN_PATH.rglob("*.py")
|
11
|
-
if path.name not in {"__init__.py", "base.py"} # We do not want to show base plugins
|
12
|
-
]
|
13
|
-
|
14
|
-
def load_plugin_class_from_file(file_path):
|
15
|
-
import importlib.util
|
16
|
-
from modusa.plugins.base import ModusaPlugin
|
17
|
-
|
18
|
-
spec = importlib.util.spec_from_file_location(file_path.stem, file_path)
|
19
|
-
module = importlib.util.module_from_spec(spec)
|
20
|
-
try:
|
21
|
-
spec.loader.exec_module(module)
|
22
|
-
except Exception as e:
|
23
|
-
print(f"❌ Error loading {file_path}: {e}")
|
24
|
-
return []
|
25
|
-
|
26
|
-
plugin_classes = []
|
27
|
-
for _, obj in inspect.getmembers(module, inspect.isclass):
|
28
|
-
if issubclass(obj, ModusaPlugin) and obj is not ModusaPlugin:
|
29
|
-
plugin_classes.append(obj)
|
30
|
-
|
31
|
-
return plugin_classes
|
32
|
-
|
33
|
-
def list_plugins():
|
34
|
-
from rich.console import Console
|
35
|
-
from rich.table import Table
|
36
|
-
from modusa.plugins.base import ModusaPlugin
|
37
|
-
|
38
|
-
console = Console()
|
39
|
-
table = Table(title="🔌 Available Modusa Plugins")
|
40
|
-
|
41
|
-
table.add_column("Plugin", style="bold green")
|
42
|
-
table.add_column("Module", style="dim")
|
43
|
-
table.add_column("Description", style="white")
|
44
|
-
|
45
|
-
all_plugins = []
|
46
|
-
|
47
|
-
for file_path in find_plugin_files():
|
48
|
-
plugin_classes = load_plugin_class_from_file(file_path)
|
49
|
-
for cls in plugin_classes:
|
50
|
-
name = cls.__name__
|
51
|
-
module = file_path.relative_to(PLUGIN_PATH.parent)
|
52
|
-
author = getattr(cls, "author_name", "—")
|
53
|
-
email = getattr(cls, "author_email", "—")
|
54
|
-
desc = getattr(cls, "description", "—")
|
55
|
-
table.add_row(name, str(module), desc)
|
56
|
-
table.add_row("")
|
57
|
-
all_plugins.append(cls)
|
58
|
-
|
59
|
-
console.print(table)
|
60
|
-
|