pyqmh-tools 0.0.1__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.
- pyqmh_tools-0.0.1/LICENSE +21 -0
- pyqmh_tools-0.0.1/PKG-INFO +158 -0
- pyqmh_tools-0.0.1/README.md +140 -0
- pyqmh_tools-0.0.1/pyproject.toml +47 -0
- pyqmh_tools-0.0.1/setup.cfg +4 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/__init__.py +1 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/assets/new-.gitignore +29 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/assets/new-app.py +79 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/assets/new-base.py +57 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/assets/new-factory.py +73 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/assets/new-implementation.py +39 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/assets/new-init.py +9 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/assets/new-module.py +59 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/assets/new-simulated.py +39 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/pyqmh_module_add.py +484 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/pyqmh_module_remove.py +256 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools/pyqmh_project_init.py +90 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools.egg-info/PKG-INFO +158 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools.egg-info/SOURCES.txt +24 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools.egg-info/dependency_links.txt +1 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools.egg-info/entry_points.txt +4 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools.egg-info/requires.txt +3 -0
- pyqmh_tools-0.0.1/src/pyqmh_tools.egg-info/top_level.txt +1 -0
- pyqmh_tools-0.0.1/tests/test_module_add.py +82 -0
- pyqmh_tools-0.0.1/tests/test_module_remove.py +80 -0
- pyqmh_tools-0.0.1/tests/test_project_init.py +38 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PCLabTools
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyqmh-tools
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: CLI tooling to scaffold and manage Python Queued Message Handler projects
|
|
5
|
+
Author-email: PCLabTools <pclabtools@github.io>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PCLabTools/pyqmh-tools
|
|
8
|
+
Project-URL: Issues, https://github.com/PCLabTools/pyqmh-tools/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Provides-Extra: test
|
|
16
|
+
Requires-Dist: pytest>=9.0; extra == "test"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# pyqmh-tools
|
|
20
|
+
|
|
21
|
+
Scaffolding and maintenance utilities for building a Python Queued Message Handler (QMH) project.
|
|
22
|
+
|
|
23
|
+
This repository is intended to be published as a PyPI package so the tooling can be installed with `pip` and used as project commands.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Install from PyPI (after publish):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install pyqmh-tools
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
This package provides three main commands:
|
|
36
|
+
|
|
37
|
+
1. `pyqmh_project_init`
|
|
38
|
+
2. `pyqmh_module_add`
|
|
39
|
+
3. `pyqmh_module_remove`
|
|
40
|
+
|
|
41
|
+
Note: if you see `pyqmg-module-remove` elsewhere, treat that as a typo. The command name in this project is `pyqmh_module_remove`.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
### `pyqmh_project_init`
|
|
46
|
+
|
|
47
|
+
Initializes a new QMH project in the current directory.
|
|
48
|
+
|
|
49
|
+
What it does:
|
|
50
|
+
|
|
51
|
+
- Creates `src/` and `src/modules/` if they do not already exist.
|
|
52
|
+
- Creates `src/modules/__init__.py` if missing.
|
|
53
|
+
- Copies template `app.py` into `src/app.py` if it does not already exist.
|
|
54
|
+
- Prompts for app/project description (only when creating `app.py`).
|
|
55
|
+
- Fills template placeholders such as description/author.
|
|
56
|
+
- Creates root `.gitignore` from template if `.gitignore` does not exist.
|
|
57
|
+
|
|
58
|
+
Run from your target project directory:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pyqmh_project_init
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### `pyqmh_module_add`
|
|
67
|
+
|
|
68
|
+
Adds a new module to `src/modules`, or adds a new implementation to an existing factory module.
|
|
69
|
+
|
|
70
|
+
Behavior summary:
|
|
71
|
+
|
|
72
|
+
- Prompts for module name first.
|
|
73
|
+
- If module already exists and is a factory module, prompts to add a new implementation.
|
|
74
|
+
- If module does not exist, prompts for module type:
|
|
75
|
+
- `standard`
|
|
76
|
+
- `factory`
|
|
77
|
+
- `repository`
|
|
78
|
+
|
|
79
|
+
`standard` creation:
|
|
80
|
+
|
|
81
|
+
- Creates `src/modules/<module_name>/module.py` from template.
|
|
82
|
+
- Creates module `__init__.py` from template with imports/exports.
|
|
83
|
+
- Updates `src/modules/__init__.py` import and `__all__` (if file exists).
|
|
84
|
+
- Updates `src/app.py` import and constructor registration (if file exists).
|
|
85
|
+
|
|
86
|
+
`factory` creation:
|
|
87
|
+
|
|
88
|
+
- Creates `factory.py`, `base.py`, and `simulated.py` from templates.
|
|
89
|
+
- Creates module `__init__.py` from template with imports/exports.
|
|
90
|
+
- Includes factory/base/simulated exports.
|
|
91
|
+
- Updates `src/modules/__init__.py` import and `__all__` (if file exists).
|
|
92
|
+
- Updates `src/app.py` import and constructor registration (if file exists).
|
|
93
|
+
|
|
94
|
+
`repository` creation:
|
|
95
|
+
|
|
96
|
+
- Prompts for Git repository URL.
|
|
97
|
+
- Adds the repository as a Git submodule under `src/modules/<module_name>`.
|
|
98
|
+
- If repository URL is blank, falls back to template-based creation prompts.
|
|
99
|
+
|
|
100
|
+
Add new module:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pyqmh_module_add
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### `pyqmh_module_remove`
|
|
109
|
+
|
|
110
|
+
Removes an existing module from `src/modules`.
|
|
111
|
+
|
|
112
|
+
Behavior summary:
|
|
113
|
+
|
|
114
|
+
- Prints available modules before prompting.
|
|
115
|
+
- Accepts module name as folder name or CamelCase class name.
|
|
116
|
+
- Shows files to be removed and asks for confirmation.
|
|
117
|
+
- If module is a Git submodule:
|
|
118
|
+
- deinitializes submodule
|
|
119
|
+
- removes it from Git index
|
|
120
|
+
- removes submodule metadata
|
|
121
|
+
- If module is a regular folder:
|
|
122
|
+
- removes the folder directly
|
|
123
|
+
- Cleans up references after removal:
|
|
124
|
+
- `src/modules/__init__.py` imports and `__all__`
|
|
125
|
+
- `src/app.py` import and constructor registration
|
|
126
|
+
|
|
127
|
+
Remove module:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pyqmh_module_remove
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Typical New Project Flow
|
|
134
|
+
|
|
135
|
+
From a new directory:
|
|
136
|
+
|
|
137
|
+
1. Initialize project scaffold.
|
|
138
|
+
2. Add one or more modules.
|
|
139
|
+
3. Run app.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
mkdir my-qmh-project
|
|
145
|
+
cd my-qmh-project
|
|
146
|
+
|
|
147
|
+
pyqmh_project_init
|
|
148
|
+
pyqmh_module_add
|
|
149
|
+
pyqmh_module_add
|
|
150
|
+
|
|
151
|
+
python src/app.py --debug
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Development Notes
|
|
155
|
+
|
|
156
|
+
- Commands are designed to be run from the project root where `src/` should exist.
|
|
157
|
+
- The tools are idempotent for common setup steps (existing files/folders are generally preserved).
|
|
158
|
+
- Generated files are template-driven from `src/pyqmh_tools/assets`.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# pyqmh-tools
|
|
2
|
+
|
|
3
|
+
Scaffolding and maintenance utilities for building a Python Queued Message Handler (QMH) project.
|
|
4
|
+
|
|
5
|
+
This repository is intended to be published as a PyPI package so the tooling can be installed with `pip` and used as project commands.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install from PyPI (after publish):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install pyqmh-tools
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
This package provides three main commands:
|
|
18
|
+
|
|
19
|
+
1. `pyqmh_project_init`
|
|
20
|
+
2. `pyqmh_module_add`
|
|
21
|
+
3. `pyqmh_module_remove`
|
|
22
|
+
|
|
23
|
+
Note: if you see `pyqmg-module-remove` elsewhere, treat that as a typo. The command name in this project is `pyqmh_module_remove`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
### `pyqmh_project_init`
|
|
28
|
+
|
|
29
|
+
Initializes a new QMH project in the current directory.
|
|
30
|
+
|
|
31
|
+
What it does:
|
|
32
|
+
|
|
33
|
+
- Creates `src/` and `src/modules/` if they do not already exist.
|
|
34
|
+
- Creates `src/modules/__init__.py` if missing.
|
|
35
|
+
- Copies template `app.py` into `src/app.py` if it does not already exist.
|
|
36
|
+
- Prompts for app/project description (only when creating `app.py`).
|
|
37
|
+
- Fills template placeholders such as description/author.
|
|
38
|
+
- Creates root `.gitignore` from template if `.gitignore` does not exist.
|
|
39
|
+
|
|
40
|
+
Run from your target project directory:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pyqmh_project_init
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### `pyqmh_module_add`
|
|
49
|
+
|
|
50
|
+
Adds a new module to `src/modules`, or adds a new implementation to an existing factory module.
|
|
51
|
+
|
|
52
|
+
Behavior summary:
|
|
53
|
+
|
|
54
|
+
- Prompts for module name first.
|
|
55
|
+
- If module already exists and is a factory module, prompts to add a new implementation.
|
|
56
|
+
- If module does not exist, prompts for module type:
|
|
57
|
+
- `standard`
|
|
58
|
+
- `factory`
|
|
59
|
+
- `repository`
|
|
60
|
+
|
|
61
|
+
`standard` creation:
|
|
62
|
+
|
|
63
|
+
- Creates `src/modules/<module_name>/module.py` from template.
|
|
64
|
+
- Creates module `__init__.py` from template with imports/exports.
|
|
65
|
+
- Updates `src/modules/__init__.py` import and `__all__` (if file exists).
|
|
66
|
+
- Updates `src/app.py` import and constructor registration (if file exists).
|
|
67
|
+
|
|
68
|
+
`factory` creation:
|
|
69
|
+
|
|
70
|
+
- Creates `factory.py`, `base.py`, and `simulated.py` from templates.
|
|
71
|
+
- Creates module `__init__.py` from template with imports/exports.
|
|
72
|
+
- Includes factory/base/simulated exports.
|
|
73
|
+
- Updates `src/modules/__init__.py` import and `__all__` (if file exists).
|
|
74
|
+
- Updates `src/app.py` import and constructor registration (if file exists).
|
|
75
|
+
|
|
76
|
+
`repository` creation:
|
|
77
|
+
|
|
78
|
+
- Prompts for Git repository URL.
|
|
79
|
+
- Adds the repository as a Git submodule under `src/modules/<module_name>`.
|
|
80
|
+
- If repository URL is blank, falls back to template-based creation prompts.
|
|
81
|
+
|
|
82
|
+
Add new module:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pyqmh_module_add
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### `pyqmh_module_remove`
|
|
91
|
+
|
|
92
|
+
Removes an existing module from `src/modules`.
|
|
93
|
+
|
|
94
|
+
Behavior summary:
|
|
95
|
+
|
|
96
|
+
- Prints available modules before prompting.
|
|
97
|
+
- Accepts module name as folder name or CamelCase class name.
|
|
98
|
+
- Shows files to be removed and asks for confirmation.
|
|
99
|
+
- If module is a Git submodule:
|
|
100
|
+
- deinitializes submodule
|
|
101
|
+
- removes it from Git index
|
|
102
|
+
- removes submodule metadata
|
|
103
|
+
- If module is a regular folder:
|
|
104
|
+
- removes the folder directly
|
|
105
|
+
- Cleans up references after removal:
|
|
106
|
+
- `src/modules/__init__.py` imports and `__all__`
|
|
107
|
+
- `src/app.py` import and constructor registration
|
|
108
|
+
|
|
109
|
+
Remove module:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pyqmh_module_remove
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Typical New Project Flow
|
|
116
|
+
|
|
117
|
+
From a new directory:
|
|
118
|
+
|
|
119
|
+
1. Initialize project scaffold.
|
|
120
|
+
2. Add one or more modules.
|
|
121
|
+
3. Run app.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
mkdir my-qmh-project
|
|
127
|
+
cd my-qmh-project
|
|
128
|
+
|
|
129
|
+
pyqmh_project_init
|
|
130
|
+
pyqmh_module_add
|
|
131
|
+
pyqmh_module_add
|
|
132
|
+
|
|
133
|
+
python src/app.py --debug
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Development Notes
|
|
137
|
+
|
|
138
|
+
- Commands are designed to be run from the project root where `src/` should exist.
|
|
139
|
+
- The tools are idempotent for common setup steps (existing files/folders are generally preserved).
|
|
140
|
+
- Generated files are template-driven from `src/pyqmh_tools/assets`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyqmh-tools"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "PCLabTools", email = "pclabtools@github.io" },
|
|
10
|
+
]
|
|
11
|
+
description = "CLI tooling to scaffold and manage Python Queued Message Handler projects"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
license = "MIT"
|
|
20
|
+
license-files = ["LICEN[CS]E*"]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/PCLabTools/pyqmh-tools"
|
|
24
|
+
Issues = "https://github.com/PCLabTools/pyqmh-tools/issues"
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
test = ["pytest>=9.0"]
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
pyqmh_module_add = "pyqmh_tools.pyqmh_module_add:main"
|
|
31
|
+
pyqmh_module_remove = "pyqmh_tools.pyqmh_module_remove:main"
|
|
32
|
+
pyqmh_project_init = "pyqmh_tools.pyqmh_project_init:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
package-dir = {"" = "src"}
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
include = ["pyqmh_tools*"]
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
pyqmh_tools = ["assets/*.py", "assets/new-.gitignore"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
python_files = "test_*.py"
|
|
47
|
+
addopts = "-q"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""pyqmh-tools package."""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.venv
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.egg-info
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.coverage
|
|
6
|
+
|
|
7
|
+
# Packaging/build outputs
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
*.egg
|
|
11
|
+
pip-wheel-metadata/
|
|
12
|
+
|
|
13
|
+
# Coverage outputs
|
|
14
|
+
.coverage.*
|
|
15
|
+
htmlcov/
|
|
16
|
+
coverage.xml
|
|
17
|
+
|
|
18
|
+
# Type/lint/test tooling caches
|
|
19
|
+
.mypy_cache/
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
.tox/
|
|
22
|
+
.nox/
|
|
23
|
+
|
|
24
|
+
# Editor/IDE
|
|
25
|
+
.vscode/
|
|
26
|
+
.idea/
|
|
27
|
+
|
|
28
|
+
# Optional local Python version file
|
|
29
|
+
.python-version
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: app.py
|
|
3
|
+
description: {{DESCRIPTION}}
|
|
4
|
+
author: {{AUTHOR}}
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import argparse
|
|
9
|
+
from pyqmh import Protocol, Message
|
|
10
|
+
|
|
11
|
+
class App():
|
|
12
|
+
def __init__(self, debug: bool = False):
|
|
13
|
+
self.debug = debug
|
|
14
|
+
self.address = "main"
|
|
15
|
+
self.protocol = Protocol(self.address)
|
|
16
|
+
self.logger = logging.getLogger("pyqmh.module").getChild(self.address)
|
|
17
|
+
self.logger.setLevel(logging.DEBUG if self.debug else logging.INFO)
|
|
18
|
+
|
|
19
|
+
# Register modules here
|
|
20
|
+
# {{MODULE_NAME}}("{{MODULE_NAME}}", self.protocol, debug=self.debug)
|
|
21
|
+
|
|
22
|
+
def __del__(self):
|
|
23
|
+
"""Clean up the main module by deleting the protocol instance.
|
|
24
|
+
"""
|
|
25
|
+
del self.protocol
|
|
26
|
+
|
|
27
|
+
def run(self):
|
|
28
|
+
"""Run the main application loop and handles application shutdown.
|
|
29
|
+
"""
|
|
30
|
+
self.logger.debug("Starting main application loop.")
|
|
31
|
+
|
|
32
|
+
# Perform any actions needed before entering the main loop, such as initializing modules or setting up resources.
|
|
33
|
+
|
|
34
|
+
print(f"\033[92mMain application loop has started. Press Ctrl+C to exit.\033[0m")
|
|
35
|
+
|
|
36
|
+
while True:
|
|
37
|
+
try:
|
|
38
|
+
message = self.protocol.receive_message(self.address, timeout=0.2)
|
|
39
|
+
if self.handle_message(message):
|
|
40
|
+
break
|
|
41
|
+
except TimeoutError:
|
|
42
|
+
continue
|
|
43
|
+
except KeyboardInterrupt:
|
|
44
|
+
self.logger.debug("Keyboard interrupt received. Shutting down.")
|
|
45
|
+
self.protocol.broadcast_message("shutdown")
|
|
46
|
+
break
|
|
47
|
+
|
|
48
|
+
def handle_message(self, message: Message) -> bool:
|
|
49
|
+
"""Handle incoming messages.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
message: The message to handle.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
bool: True if the message was handled successfully, False otherwise.
|
|
56
|
+
"""
|
|
57
|
+
self.logger.debug(f"Handling message: {message}")
|
|
58
|
+
if message.command == "shutdown":
|
|
59
|
+
self.logger.debug("Received shutdown command. Shutting down.")
|
|
60
|
+
self.protocol.broadcast_message("shutdown")
|
|
61
|
+
try:
|
|
62
|
+
self.protocol.receive_message(self.address, timeout=5) # Wait for acknowledgments
|
|
63
|
+
except TimeoutError:
|
|
64
|
+
self.logger.debug("Timeout occurred while waiting for acknowledgments.")
|
|
65
|
+
return True
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
parser = argparse.ArgumentParser(description="Run the pyqmh app.")
|
|
71
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug logging.")
|
|
72
|
+
args = parser.parse_args()
|
|
73
|
+
|
|
74
|
+
logging.basicConfig(
|
|
75
|
+
level=logging.DEBUG if args.debug else logging.INFO,
|
|
76
|
+
format="%(name)s - %(levelname)s - %(message)s",
|
|
77
|
+
)
|
|
78
|
+
app = App(debug=args.debug)
|
|
79
|
+
app.run()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: base.py
|
|
3
|
+
description: Base implementation contract for {{MODULE_NAME}} modules.
|
|
4
|
+
author: {{AUTHOR}}
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from pyqmh import Message, Protocol, Module
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Base{{MODULE_NAME}}(Module, ABC):
|
|
14
|
+
"""Abstract base class for {{MODULE_NAME}} implementations."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, address: str, protocol: Protocol, debug: Optional[bool] = None):
|
|
17
|
+
"""Initialises the factory module.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
address (str): Unique address for the module.
|
|
21
|
+
protocol (Protocol): The protocol instance.
|
|
22
|
+
debug (bool, optional): Debug flag. Defaults to None.
|
|
23
|
+
"""
|
|
24
|
+
super().__init__(address, protocol, debug=debug)
|
|
25
|
+
self.logger = logging.getLogger("pyqmh.module").getChild(self.address)
|
|
26
|
+
self.logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
27
|
+
|
|
28
|
+
def handle_message(self, message: Message) -> bool:
|
|
29
|
+
"""Handle incoming messages.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
message (Message): The message to handle.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
bool: True if the module should shutdown, False otherwise.
|
|
36
|
+
"""
|
|
37
|
+
self.logger.debug(f"Handling message: {message}")
|
|
38
|
+
if message.command == "greet":
|
|
39
|
+
return self.greet(message)
|
|
40
|
+
return super().handle_message(message)
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def background_task(self):
|
|
44
|
+
"""Background task - must be implemented by each factory implementation."""
|
|
45
|
+
raise NotImplementedError("background_task must be implemented by subclasses")
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def greet(self, message: Message) -> bool:
|
|
49
|
+
"""Handle the greet message - must be implemented by each factory implementation.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
message (Message): Incoming message.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
bool: False to continue running.
|
|
56
|
+
"""
|
|
57
|
+
raise NotImplementedError("greet must be implemented by subclasses")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: factory.py
|
|
3
|
+
description: Factory module for creating {{MODULE_NAME}} instances with swappable implementations.
|
|
4
|
+
author: {{AUTHOR}}
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from pyqmh import Protocol
|
|
9
|
+
|
|
10
|
+
from .base import Base{{MODULE_NAME}}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class {{MODULE_NAME}}:
|
|
14
|
+
"""Factory for creating {{MODULE_NAME}} instances with swappable implementations at runtime.
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
ValueError: When an invalid implementation type is specified.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Base{{MODULE_NAME}}: An instance of the factory module based on the specified implementation type.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_implementations: dict[str, type[Base{{MODULE_NAME}}]] = {}
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def register(cls, implementation: str, module_class: type[Base{{MODULE_NAME}}]):
|
|
27
|
+
"""Registers a factory implementation.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
implementation (str): The name of the implementation.
|
|
31
|
+
module_class (type[Base{{MODULE_NAME}}]): The class to register.
|
|
32
|
+
"""
|
|
33
|
+
cls._implementations[implementation.lower()] = module_class
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def create(
|
|
37
|
+
cls,
|
|
38
|
+
address: str,
|
|
39
|
+
protocol: Protocol,
|
|
40
|
+
debug: Optional[bool] = None,
|
|
41
|
+
implementation_type: str = "simulated",
|
|
42
|
+
) -> Base{{MODULE_NAME}}:
|
|
43
|
+
"""Creates a factory module instance based on implementation type.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
address (str): Unique address for the module.
|
|
47
|
+
protocol (Protocol): The protocol instance.
|
|
48
|
+
debug (bool, optional): Debug flag. Defaults to None.
|
|
49
|
+
implementation_type (str, optional): Implementation to create. Defaults to "simulated".
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Base{{MODULE_NAME}}: The created module instance.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
ValueError: If the specified implementation type is not registered.
|
|
56
|
+
"""
|
|
57
|
+
implementation_type = implementation_type.lower()
|
|
58
|
+
if implementation_type not in cls._implementations:
|
|
59
|
+
raise ValueError(f"{{MODULE_NAME}}: No factory implementation registered for type '{implementation_type}'")
|
|
60
|
+
return cls._implementations[implementation_type](address, protocol, debug)
|
|
61
|
+
|
|
62
|
+
def __new__(
|
|
63
|
+
cls,
|
|
64
|
+
address: str,
|
|
65
|
+
protocol: Protocol,
|
|
66
|
+
debug: Optional[bool] = None,
|
|
67
|
+
implementation_type: str = "simulated",
|
|
68
|
+
) -> Base{{MODULE_NAME}}:
|
|
69
|
+
return cls.create(address, protocol, debug, implementation_type)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Import implementations to register them.
|
|
73
|
+
from . import simulated # noqa: E402,F401
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
file: {{IMPLEMENTATION_NAME}}.py
|
|
3
|
+
description: {{IMPLEMENTATION_NAME}} implementation of {{MODULE_NAME}}. {{DESCRIPTION}}
|
|
4
|
+
author: {{AUTHOR}}
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from time import sleep
|
|
8
|
+
from pyqmh import Message
|
|
9
|
+
|
|
10
|
+
from .factory import {{MODULE_NAME}}
|
|
11
|
+
from .base import Base{{MODULE_NAME}}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class {{IMPLEMENTATION_NAME}}{{MODULE_NAME}}(Base{{MODULE_NAME}}):
|
|
15
|
+
"""Simulated implementation of {{MODULE_NAME}}."""
|
|
16
|
+
|
|
17
|
+
def background_task(self):
|
|
18
|
+
"""Simulated background task."""
|
|
19
|
+
while self.background_task_running:
|
|
20
|
+
self.logger.debug("Performing background task.")
|
|
21
|
+
# TODO: implement simulated background task logic
|
|
22
|
+
sleep(1)
|
|
23
|
+
|
|
24
|
+
def message_custom_action(self, message: Message) -> bool:
|
|
25
|
+
"""Handles the custom_action message.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
message (Message): Incoming message.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
bool: False to continue running.
|
|
32
|
+
"""
|
|
33
|
+
self.logger.debug(f"Handling custom action: {message}")
|
|
34
|
+
# TODO: implement simulated custom action logic
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Register this implementation with the factory.
|
|
39
|
+
{{MODULE_NAME}}.register("{{IMPLEMENTATION_NAME}}", {{IMPLEMENTATION_NAME}}{{MODULE_NAME}})
|