modusa 0.1.0__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.
- modusa-0.1.0/LICENSE.md +9 -0
- modusa-0.1.0/PKG-INFO +86 -0
- modusa-0.1.0/README.md +58 -0
- modusa-0.1.0/pyproject.toml +48 -0
- modusa-0.1.0/src/modusa/.DS_Store +0 -0
- modusa-0.1.0/src/modusa/__init__.py +1 -0
- modusa-0.1.0/src/modusa/config.py +18 -0
- modusa-0.1.0/src/modusa/decorators.py +176 -0
- modusa-0.1.0/src/modusa/devtools/generate_template.py +79 -0
- modusa-0.1.0/src/modusa/devtools/list_authors.py +2 -0
- modusa-0.1.0/src/modusa/devtools/list_plugins.py +60 -0
- modusa-0.1.0/src/modusa/devtools/main.py +42 -0
- modusa-0.1.0/src/modusa/devtools/templates/engines.py +28 -0
- modusa-0.1.0/src/modusa/devtools/templates/generators.py +26 -0
- modusa-0.1.0/src/modusa/devtools/templates/plugins.py +40 -0
- modusa-0.1.0/src/modusa/devtools/templates/signals.py +63 -0
- modusa-0.1.0/src/modusa/engines/__init__.py +4 -0
- modusa-0.1.0/src/modusa/engines/base.py +14 -0
- modusa-0.1.0/src/modusa/engines/plot_1dsignal.py +130 -0
- modusa-0.1.0/src/modusa/engines/plot_2dmatrix.py +159 -0
- modusa-0.1.0/src/modusa/generators/__init__.py +3 -0
- modusa-0.1.0/src/modusa/generators/base.py +40 -0
- modusa-0.1.0/src/modusa/generators/basic_waveform.py +185 -0
- modusa-0.1.0/src/modusa/main.py +35 -0
- modusa-0.1.0/src/modusa/plugins/__init__.py +7 -0
- modusa-0.1.0/src/modusa/plugins/base.py +100 -0
- modusa-0.1.0/src/modusa/plugins/plot_1dsignal.py +59 -0
- modusa-0.1.0/src/modusa/plugins/plot_2dmatrix.py +76 -0
- modusa-0.1.0/src/modusa/plugins/plot_time_domain_signal.py +59 -0
- modusa-0.1.0/src/modusa/signals/__init__.py +9 -0
- modusa-0.1.0/src/modusa/signals/audio_signal.py +230 -0
- modusa-0.1.0/src/modusa/signals/base.py +294 -0
- modusa-0.1.0/src/modusa/signals/signal1d.py +311 -0
- modusa-0.1.0/src/modusa/signals/signal2d.py +226 -0
- modusa-0.1.0/src/modusa/signals/uniform_time_domain_signal.py +212 -0
- modusa-0.1.0/src/modusa/utils/.DS_Store +0 -0
- modusa-0.1.0/src/modusa/utils/__init__.py +1 -0
- modusa-0.1.0/src/modusa/utils/config.py +25 -0
- modusa-0.1.0/src/modusa/utils/excp.py +71 -0
- modusa-0.1.0/src/modusa/utils/logger.py +18 -0
- modusa-0.1.0/tests/__init__.py +0 -0
- modusa-0.1.0/tests/test_signals/test_signal1d.py +52 -0
modusa-0.1.0/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 [Ankit Anand @meluron]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
modusa-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: modusa
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A modular signal analysis python library.
|
|
5
|
+
Author-Email: Ankit Anand <ankit0.anand0@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: jupyter>=1.1.1
|
|
9
|
+
Requires-Dist: pytest>=8.4.0
|
|
10
|
+
Requires-Dist: numpy>=2.2.6
|
|
11
|
+
Requires-Dist: librosa>=0.11.0
|
|
12
|
+
Requires-Dist: matplotlib>=3.10.3
|
|
13
|
+
Requires-Dist: pandas>=2.3.0
|
|
14
|
+
Requires-Dist: pydantic>=2.11.5
|
|
15
|
+
Requires-Dist: sqlalchemy>=2.0.41
|
|
16
|
+
Requires-Dist: tqdm>=4.67.1
|
|
17
|
+
Requires-Dist: sphinx==8.1.2
|
|
18
|
+
Requires-Dist: sphinx-autodoc-typehints==2.1.0
|
|
19
|
+
Requires-Dist: sphinx-copybutton>=0.5.2
|
|
20
|
+
Requires-Dist: furo>=2024.8.6
|
|
21
|
+
Requires-Dist: questionary>=2.1.0
|
|
22
|
+
Requires-Dist: rich>=14.0.0
|
|
23
|
+
Requires-Dist: snakeviz>=2.2.2
|
|
24
|
+
Requires-Dist: line-profiler>=4.2.0
|
|
25
|
+
Requires-Dist: nbsphinx==0.9.7
|
|
26
|
+
Requires-Dist: ghp-import>=2.1.0
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# modusa
|
|
30
|
+
|
|
31
|
+
**modusa**: **Mod**ular **U**nified **S**ignal **A**rchitecture* is a flexible, extensible Python framework for building, transforming, and analyzing different signal representations. It is a domain-agnostic core architecture for modern signal processing workflows.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## ๐ง Features
|
|
36
|
+
|
|
37
|
+
- โ๏ธ **modusa Signals**
|
|
38
|
+
- ๐งฉ **modusa Plugins**
|
|
39
|
+
- ๐ **modusa Genetators**
|
|
40
|
+
- โป๏ธ **modusa Engine**
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## ๐ Installation
|
|
45
|
+
|
|
46
|
+
> modusa is under active development. You can install the latest version via:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/meluron/modusa.git
|
|
50
|
+
cd modusa
|
|
51
|
+
pdm install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## ๐งช Tests
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pytest tests/
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## ๐ง Status
|
|
65
|
+
|
|
66
|
+
modusa is in **early alpha**. Expect rapid iteration, breaking changes, and big ideas.
|
|
67
|
+
If you like the direction, consider โญ starring the repo and opening issues or ideas.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## ๐ง About
|
|
72
|
+
|
|
73
|
+
**modusa** is developed and maintained by [meluron](https://www.github.com/meluron),
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## ๐ License
|
|
78
|
+
|
|
79
|
+
MIT License. See `LICENSE` for details.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## ๐ Contributions
|
|
84
|
+
|
|
85
|
+
Pull requests, ideas, and discussions are welcome!
|
|
86
|
+
No matter which domain you are in, if you work with any signal, we'd love your input.
|
modusa-0.1.0/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# modusa
|
|
2
|
+
|
|
3
|
+
**modusa**: **Mod**ular **U**nified **S**ignal **A**rchitecture* is a flexible, extensible Python framework for building, transforming, and analyzing different signal representations. It is a domain-agnostic core architecture for modern signal processing workflows.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ๐ง Features
|
|
8
|
+
|
|
9
|
+
- โ๏ธ **modusa Signals**
|
|
10
|
+
- ๐งฉ **modusa Plugins**
|
|
11
|
+
- ๐ **modusa Genetators**
|
|
12
|
+
- โป๏ธ **modusa Engine**
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## ๐ Installation
|
|
17
|
+
|
|
18
|
+
> modusa is under active development. You can install the latest version via:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git clone https://github.com/meluron/modusa.git
|
|
22
|
+
cd modusa
|
|
23
|
+
pdm install
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## ๐งช Tests
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pytest tests/
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## ๐ง Status
|
|
37
|
+
|
|
38
|
+
modusa is in **early alpha**. Expect rapid iteration, breaking changes, and big ideas.
|
|
39
|
+
If you like the direction, consider โญ starring the repo and opening issues or ideas.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## ๐ง About
|
|
44
|
+
|
|
45
|
+
**modusa** is developed and maintained by [meluron](https://www.github.com/meluron),
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## ๐ License
|
|
50
|
+
|
|
51
|
+
MIT License. See `LICENSE` for details.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## ๐ Contributions
|
|
56
|
+
|
|
57
|
+
Pull requests, ideas, and discussions are welcome!
|
|
58
|
+
No matter which domain you are in, if you work with any signal, we'd love your input.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "modusa"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A modular signal analysis python library."
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "Ankit Anand", email = "ankit0.anand0@gmail.com" },
|
|
7
|
+
]
|
|
8
|
+
dependencies = [
|
|
9
|
+
"jupyter>=1.1.1",
|
|
10
|
+
"pytest>=8.4.0",
|
|
11
|
+
"numpy>=2.2.6",
|
|
12
|
+
"librosa>=0.11.0",
|
|
13
|
+
"matplotlib>=3.10.3",
|
|
14
|
+
"pandas>=2.3.0",
|
|
15
|
+
"pydantic>=2.11.5",
|
|
16
|
+
"sqlalchemy>=2.0.41",
|
|
17
|
+
"tqdm>=4.67.1",
|
|
18
|
+
"sphinx==8.1.2",
|
|
19
|
+
"sphinx-autodoc-typehints==2.1.0",
|
|
20
|
+
"sphinx-copybutton>=0.5.2",
|
|
21
|
+
"furo>=2024.8.6",
|
|
22
|
+
"questionary>=2.1.0",
|
|
23
|
+
"rich>=14.0.0",
|
|
24
|
+
"snakeviz>=2.2.2",
|
|
25
|
+
"line-profiler>=4.2.0",
|
|
26
|
+
"nbsphinx==0.9.7",
|
|
27
|
+
"ghp-import>=2.1.0",
|
|
28
|
+
]
|
|
29
|
+
requires-python = ">=3.12"
|
|
30
|
+
readme = "README.md"
|
|
31
|
+
|
|
32
|
+
[project.license]
|
|
33
|
+
text = "MIT"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
modusa-dev = "modusa.devtools.main:main"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = [
|
|
40
|
+
"pdm-backend",
|
|
41
|
+
]
|
|
42
|
+
build-backend = "pdm.backend"
|
|
43
|
+
|
|
44
|
+
[tool.pdm]
|
|
45
|
+
distribution = true
|
|
46
|
+
|
|
47
|
+
[tool.pdm.package-dir]
|
|
48
|
+
"" = "src"
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from modusa.utils import excp, config
|
|
@@ -0,0 +1,18 @@
|
|
|
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()
|
|
@@ -0,0 +1,176 @@
|
|
|
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 type(value) is 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.ValidationError(
|
|
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 type(value) is not origin:
|
|
120
|
+
raise excp.ValidationError(
|
|
121
|
+
f"Argument '{arg_name}' must be exactly of type {origin.__name__}, got {type(value).__name__}"
|
|
122
|
+
)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# โ
Handle plain types
|
|
126
|
+
elif isinstance(expected_type, type):
|
|
127
|
+
if type(value) is not expected_type:
|
|
128
|
+
raise excp.ValidationError(
|
|
129
|
+
f"Argument '{arg_name}' must be exactly {expected_type.__name__}, got {type(value).__name__}"
|
|
130
|
+
)
|
|
131
|
+
return
|
|
132
|
+
# โ Unsupported type structure
|
|
133
|
+
else:
|
|
134
|
+
raise excp.ValidationError(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
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
TEMPLATES_DIR = ROOT_DIR / "src/modusa/devtools/templates"
|
|
10
|
+
|
|
11
|
+
class TemplateGenerator():
|
|
12
|
+
"""
|
|
13
|
+
Generates template for `plugin`, `engine`, `signal`, `generator`.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def ask_questions(for_what: str) -> dict:
|
|
18
|
+
print("----------------------")
|
|
19
|
+
print(for_what.upper())
|
|
20
|
+
print("----------------------")
|
|
21
|
+
module_name = questionary.text("Module name (snake_case): ").ask()
|
|
22
|
+
if module_name is None:
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
if Path(f"src/modusa/{for_what}/{module_name}.py").exists():
|
|
25
|
+
print(f"โ ๏ธ File already exists, choose another name.")
|
|
26
|
+
sys.exit(1)
|
|
27
|
+
|
|
28
|
+
class_name = questionary.text("Class name (CamelCase): ").ask()
|
|
29
|
+
if class_name is None:
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
author_name = questionary.text("Author name: ").ask()
|
|
33
|
+
if author_name is None:
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
author_email = questionary.text("Author email: ").ask()
|
|
37
|
+
if author_email is None:
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
answers = {"module_name": module_name, "class_name": class_name, "author_name": author_name, "author_email": author_email, "date_created": date.today()}
|
|
41
|
+
|
|
42
|
+
return answers
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def load_template_file(for_what: str) -> str:
|
|
46
|
+
template_path = TEMPLATES_DIR / f"{for_what}.py"
|
|
47
|
+
if not template_path.exists():
|
|
48
|
+
print(f"โ Template not found: {template_path}")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
|
|
51
|
+
template_code = template_path.read_text()
|
|
52
|
+
|
|
53
|
+
return template_code
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def fill_placeholders(template_code: str, placehoders_dict: dict) -> str:
|
|
57
|
+
template_code = template_code.format(**placehoders_dict) # Fill placeholders
|
|
58
|
+
return template_code
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def save_file(content: str, output_path: Path) -> None:
|
|
62
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
output_path.write_text(content)
|
|
64
|
+
print(f"โ
Successfully created.\n\n open {output_path.resolve()}")
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def create_template(for_what: str) -> None:
|
|
68
|
+
|
|
69
|
+
# Ask basic questions to create the template for `plugin`, `generator`, ...
|
|
70
|
+
answers: dict = TemplateGenerator.ask_questions(for_what)
|
|
71
|
+
|
|
72
|
+
# Load the correct template file
|
|
73
|
+
template_code: str = TemplateGenerator.load_template_file(for_what)
|
|
74
|
+
|
|
75
|
+
# Update the dynamic values based on the answers
|
|
76
|
+
template_code: str = TemplateGenerator.fill_placeholders(template_code, answers)
|
|
77
|
+
|
|
78
|
+
# Save it to a file and put it in the correct folder
|
|
79
|
+
TemplateGenerator.save_file(content=template_code, output_path=ROOT_DIR / f"src/modusa/{for_what}/{answers['module_name']}.py")
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from .generate_template import TemplateGenerator
|
|
5
|
+
from .list_plugins import list_plugins
|
|
6
|
+
from . import list_authors
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
try:
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
prog="modusa-dev",
|
|
15
|
+
description="Modusa CLI Tools"
|
|
16
|
+
)
|
|
17
|
+
subparsers = parser.add_subparsers(dest="group", required=True)
|
|
18
|
+
|
|
19
|
+
# --- CREATE group ---
|
|
20
|
+
create_parser = subparsers.add_parser("create", help="Create new Modusa components")
|
|
21
|
+
create_subparsers = create_parser.add_subparsers(dest="what", required=True)
|
|
22
|
+
|
|
23
|
+
create_subparsers.add_parser("engine", help="Create a new engine class").set_defaults(func=lambda:TemplateGenerator.create_template("engines"))
|
|
24
|
+
create_subparsers.add_parser("plugin", help="Create a new plugin class").set_defaults(func=lambda:TemplateGenerator.create_template("plugins"))
|
|
25
|
+
create_subparsers.add_parser("signal", help="Create a new signal class").set_defaults(func=lambda:TemplateGenerator.create_template("signals"))
|
|
26
|
+
create_subparsers.add_parser("generator", help="Create a new signal generator class").set_defaults(func=lambda:TemplateGenerator.create_template("generators"))
|
|
27
|
+
|
|
28
|
+
# --- LIST group ---
|
|
29
|
+
list_parser = subparsers.add_parser("list", help="List information about Modusa components")
|
|
30
|
+
list_subparsers = list_parser.add_subparsers(dest="what", required=True)
|
|
31
|
+
|
|
32
|
+
list_subparsers.add_parser("plugins", help="List available plugins").set_defaults(func=list_plugins)
|
|
33
|
+
list_subparsers.add_parser("authors", help="List plugin authors").set_defaults(func=list_authors)
|
|
34
|
+
|
|
35
|
+
# --- Parse and execute ---
|
|
36
|
+
args = parser.parse_args()
|
|
37
|
+
args.func()
|
|
38
|
+
|
|
39
|
+
except KeyboardInterrupt:
|
|
40
|
+
print("\nโ Aborted by user.")
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from modusa import excp
|
|
5
|
+
from modusa.decorators import validate_args_type
|
|
6
|
+
from modusa.engines.base import ModusaEngine
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
class {class_name}(ModusaEngine):
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
#--------Meta Information----------
|
|
15
|
+
name = ""
|
|
16
|
+
description = ""
|
|
17
|
+
author_name = "{author_name}"
|
|
18
|
+
author_email = "{author_email}"
|
|
19
|
+
created_at = "{date_created}"
|
|
20
|
+
#----------------------------------
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@validate_args_type()
|
|
27
|
+
def run(self) -> Any:
|
|
28
|
+
pass
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from modusa.decorators import validate_args_type
|
|
5
|
+
from modusa.generators.base import ModusaGenerator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class {class_name}(ModusaGenerator):
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
#--------Meta Information----------
|
|
14
|
+
name = ""
|
|
15
|
+
description = ""
|
|
16
|
+
author_name = "{author_name}"
|
|
17
|
+
author_email = "{author_email}"
|
|
18
|
+
created_at = "{date_created}"
|
|
19
|
+
#----------------------------------
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def generate(self) -> Any:
|
|
26
|
+
pass
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from modusa.plugins.base import ModusaPlugin
|
|
5
|
+
from modusa.decorators import immutable_property, validate_args_type, plugin_safety_check
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class {class_name}(ModusaPlugin):
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
#--------Meta Information----------
|
|
14
|
+
name = ""
|
|
15
|
+
description = ""
|
|
16
|
+
author_name = "{author_name}"
|
|
17
|
+
author_email = "{author_email}"
|
|
18
|
+
created_at = "{date_created}"
|
|
19
|
+
#----------------------------------
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__()
|
|
23
|
+
|
|
24
|
+
@immutable_property(error_msg="Mutation not allowed.")
|
|
25
|
+
def allowed_input_signal_types(self) -> tuple[type, ...]:
|
|
26
|
+
return ()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@immutable_property(error_msg="Mutation not allowed.")
|
|
30
|
+
def allowed_output_signal_types(self) -> tuple[type, ...]:
|
|
31
|
+
return ()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@plugin_safety_check()
|
|
35
|
+
@validate_args_type()
|
|
36
|
+
def apply(self, signal: "") -> "":
|
|
37
|
+
|
|
38
|
+
# Run the engine here
|
|
39
|
+
|
|
40
|
+
return
|