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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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}})
@@ -0,0 +1,9 @@
1
+ """
2
+ file: __init__.py
3
+ description: Public exports for the {{MODULE_NAME}} module.
4
+ author: {{AUTHOR}}
5
+ """
6
+
7
+ {{MODULE_IMPORTS}}
8
+
9
+ __all__ = [{{MODULE_EXPORTS}}]