modusa 0.4.29__tar.gz → 0.4.31__tar.gz

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.
Files changed (64) hide show
  1. {modusa-0.4.29 → modusa-0.4.31}/PKG-INFO +2 -2
  2. {modusa-0.4.29 → modusa-0.4.31}/pyproject.toml +2 -2
  3. modusa-0.4.31/src/modusa/__init__.py +17 -0
  4. {modusa-0.4.29 → modusa-0.4.31}/src/modusa/tools/__init__.py +11 -3
  5. modusa-0.4.31/src/modusa/tools/ann_saver.py +30 -0
  6. {modusa-0.4.29 → modusa-0.4.31}/src/modusa/tools/audio_recorder.py +0 -1
  7. modusa-0.4.31/src/modusa/tools/audio_stft.py +72 -0
  8. {modusa-0.4.29 → modusa-0.4.31}/src/modusa/tools/youtube_downloader.py +1 -4
  9. modusa-0.4.29/src/modusa/__init__.py +0 -13
  10. modusa-0.4.29/src/modusa/config.py +0 -18
  11. modusa-0.4.29/src/modusa/decorators.py +0 -176
  12. modusa-0.4.29/src/modusa/devtools/generate_docs_source.py +0 -92
  13. modusa-0.4.29/src/modusa/devtools/generate_template.py +0 -144
  14. modusa-0.4.29/src/modusa/devtools/list_authors.py +0 -2
  15. modusa-0.4.29/src/modusa/devtools/list_plugins.py +0 -60
  16. modusa-0.4.29/src/modusa/devtools/main.py +0 -45
  17. modusa-0.4.29/src/modusa/devtools/templates/generator.py +0 -24
  18. modusa-0.4.29/src/modusa/devtools/templates/io.py +0 -24
  19. modusa-0.4.29/src/modusa/devtools/templates/model.py +0 -47
  20. modusa-0.4.29/src/modusa/devtools/templates/plugin.py +0 -41
  21. modusa-0.4.29/src/modusa/devtools/templates/test.py +0 -10
  22. modusa-0.4.29/src/modusa/devtools/templates/tool.py +0 -24
  23. modusa-0.4.29/src/modusa/generators/__init__.py +0 -13
  24. modusa-0.4.29/src/modusa/generators/audio.py +0 -188
  25. modusa-0.4.29/src/modusa/generators/audio_waveforms.py +0 -236
  26. modusa-0.4.29/src/modusa/generators/base.py +0 -29
  27. modusa-0.4.29/src/modusa/generators/ftds.py +0 -298
  28. modusa-0.4.29/src/modusa/generators/s1d.py +0 -270
  29. modusa-0.4.29/src/modusa/generators/s2d.py +0 -300
  30. modusa-0.4.29/src/modusa/generators/s_ax.py +0 -102
  31. modusa-0.4.29/src/modusa/generators/t_ax.py +0 -64
  32. modusa-0.4.29/src/modusa/generators/tds.py +0 -267
  33. modusa-0.4.29/src/modusa/models/__init__.py +0 -14
  34. modusa-0.4.29/src/modusa/models/audio.py +0 -90
  35. modusa-0.4.29/src/modusa/models/base.py +0 -70
  36. modusa-0.4.29/src/modusa/models/data.py +0 -457
  37. modusa-0.4.29/src/modusa/models/ftds.py +0 -584
  38. modusa-0.4.29/src/modusa/models/s1d.py +0 -578
  39. modusa-0.4.29/src/modusa/models/s2d.py +0 -619
  40. modusa-0.4.29/src/modusa/models/s_ax.py +0 -448
  41. modusa-0.4.29/src/modusa/models/t_ax.py +0 -335
  42. modusa-0.4.29/src/modusa/models/tds.py +0 -465
  43. modusa-0.4.29/src/modusa/plugins/__init__.py +0 -3
  44. modusa-0.4.29/src/modusa/plugins/base.py +0 -100
  45. modusa-0.4.29/src/modusa/tools/_plotter_old.py +0 -629
  46. modusa-0.4.29/src/modusa/tools/audio_saver.py +0 -30
  47. modusa-0.4.29/src/modusa/tools/base.py +0 -43
  48. modusa-0.4.29/src/modusa/tools/math_ops.py +0 -335
  49. modusa-0.4.29/src/modusa/utils/__init__.py +0 -1
  50. modusa-0.4.29/src/modusa/utils/config.py +0 -25
  51. modusa-0.4.29/src/modusa/utils/excp.py +0 -49
  52. modusa-0.4.29/src/modusa/utils/logger.py +0 -18
  53. modusa-0.4.29/src/modusa/utils/np_func_cat.py +0 -44
  54. modusa-0.4.29/src/modusa/utils/plot.py +0 -142
  55. {modusa-0.4.29 → modusa-0.4.31}/LICENSE.md +0 -0
  56. {modusa-0.4.29 → modusa-0.4.31}/README.md +0 -0
  57. {modusa-0.4.29 → modusa-0.4.31}/src/modusa/fonts/NotoSansDevanagari-Regular.ttf +0 -0
  58. {modusa-0.4.29 → modusa-0.4.31}/src/modusa/images/icon.png +0 -0
  59. {modusa-0.4.29 → modusa-0.4.31}/src/modusa/tools/ann_loader.py +0 -0
  60. {modusa-0.4.29 → modusa-0.4.31}/src/modusa/tools/audio_converter.py +0 -0
  61. {modusa-0.4.29 → modusa-0.4.31}/src/modusa/tools/audio_loader.py +0 -0
  62. {modusa-0.4.29 → modusa-0.4.31}/src/modusa/tools/audio_player.py +0 -0
  63. {modusa-0.4.29 → modusa-0.4.31}/src/modusa/tools/plotter.py +0 -0
  64. {modusa-0.4.29 → modusa-0.4.31}/src/modusa/tools/synth.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modusa
3
- Version: 0.4.29
3
+ Version: 0.4.31
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.9.23
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
@@ -8,7 +8,7 @@ authors = [
8
8
  dependencies = [
9
9
  "numpy>=2.2.6",
10
10
  "matplotlib>=3.10.3",
11
- "yt-dlp==2025.9.23",
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.29"
19
+ version = "0.4.31"
20
20
 
21
21
  [project.license]
22
22
  text = "MIT"
@@ -0,0 +1,17 @@
1
+ # Audio related
2
+ from modusa.tools import load, play, convert, record
3
+ from modusa.tools import download
4
+
5
+ # Annotation related
6
+ from modusa.tools import load_ann, save_ann
7
+
8
+ # Plotting related
9
+ from modusa.tools import dist_plot, hill_plot, plot, fig
10
+
11
+ # Synthsizing related
12
+ from modusa.tools import synth_f0
13
+
14
+ # Audio features related
15
+ from modusa.tools import stft
16
+
17
+ __version__ = "0.4.31" # This is dynamically used by the documentation, and pyproject.toml; Only need to change it here; rest gets taken care of.
@@ -1,14 +1,22 @@
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
 
14
- from .synth import synth_f0
18
+ # Synthesizing related
19
+ from .synth import synth_f0
20
+
21
+ # Audio features
22
+ from .audio_stft import stft
@@ -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
+
@@ -6,7 +6,6 @@
6
6
  # Email: ankit0.anand0@gmail.com
7
7
  #---------------------------------
8
8
 
9
-
10
9
  import sounddevice as sd
11
10
  import numpy as np
12
11
  import ipywidgets as widgets
@@ -0,0 +1,72 @@
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
+ import numpy as np
10
+
11
+ def stft(y, sr, winlen=None, hoplen=None, gamma=None):
12
+ """
13
+ Compute spectrogram with just numpy.
14
+
15
+ Parameters
16
+ ----------
17
+ y: ndarray
18
+ - Audio signal.
19
+ sr: int
20
+ - Sampling rate of the audio signal.
21
+ winlen: int
22
+ - Window length in samples.
23
+ - Default: None => set at 0.064 sec
24
+ hoplen: int
25
+ - Hop length in samples.
26
+ - Default: None => set at one-forth of winlen
27
+ gamma: int | None
28
+ - Log compression factor.
29
+ - Add contrast to the plot.
30
+
31
+ Returns
32
+ -------
33
+ ndarray:
34
+ - Spectrogram matrix, complex is gamma is None else real
35
+ ndarray:
36
+ - Frequency bins in Hz.
37
+ ndarray:
38
+ - Timeframes in sec.
39
+ """
40
+
41
+ if winlen is None:
42
+ winlen = 2 ** int(np.log2(0.064 * sr))
43
+ if hoplen is None:
44
+ hoplen = int(winlen * 0.25)
45
+
46
+ # Estimating the shape of the S matrix
47
+ M = int(np.ceil(winlen / 2))
48
+ N = int(np.ceil((y.size - winlen) / hoplen))
49
+
50
+ # Initialize the S matrix
51
+ S = np.empty((M, N), dtype=np.complex64) # M X N => freq bin X time frame
52
+
53
+ # We will need a hann window
54
+ hann = np.hanning(winlen)
55
+
56
+ # Get the frames
57
+ frames = np.lib.stride_tricks.sliding_window_view(y, window_shape=winlen)[::hoplen] # frame X chunk
58
+
59
+ # Apply window to the frame
60
+ frames_windowed = frames * hann # frame X chunk
61
+
62
+ # Compute fft for each frame
63
+ S = np.fft.rfft(frames_windowed, n=winlen, axis=1).T # transpose to match shape (freq_bins, frame)
64
+
65
+ # Magnitude spectrogram
66
+ Sf = np.fft.rfftfreq(winlen, d=1/sr) # Frequency bins (Hz)
67
+ St = np.arange(N) * hoplen / sr # Time bins (sec)
68
+
69
+ if gamma is not None:
70
+ S = np.log1p(gamma * np.abs(S))
71
+
72
+ return S, Sf, St
@@ -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 excp.InputValueError(f"`content_type` can either take 'audio' or 'video' not {content_type}")
66
+ raise ValueError(f"`content_type` can either take 'audio' or 'video' not {content_type}")
70
67
 
71
68
 
72
69
 
@@ -1,13 +0,0 @@
1
- from modusa.utils import excp, config
2
-
3
- #=====Giving access to plot functions to plot multiple signals.=====
4
- from modusa.tools import dist_plot, hill_plot, plot, fig
5
- #=====
6
-
7
- from modusa.tools import play, convert, record, save
8
- from modusa.tools import download
9
- from modusa.tools import load, load_ann
10
-
11
- from modusa.tools import synth_f0
12
-
13
- __version__ = "0.4.29"
@@ -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()
@@ -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']}"))
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env python3
2
-