spoe-forge 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.
- spoe_forge-0.0.1/PKG-INFO +168 -0
- spoe_forge-0.0.1/README.md +141 -0
- spoe_forge-0.0.1/pyproject.toml +53 -0
- spoe_forge-0.0.1/spoe_forge/__init__.py +15 -0
- spoe_forge-0.0.1/spoe_forge/agent/__init__.py +0 -0
- spoe_forge-0.0.1/spoe_forge/agent/context.py +53 -0
- spoe_forge-0.0.1/spoe_forge/agent/exceptions.py +7 -0
- spoe_forge-0.0.1/spoe_forge/agent/registry.py +67 -0
- spoe_forge-0.0.1/spoe_forge/exception.py +4 -0
- spoe_forge-0.0.1/spoe_forge/log.py +60 -0
- spoe_forge-0.0.1/spoe_forge/server/__init__.py +0 -0
- spoe_forge-0.0.1/spoe_forge/server/configuration.py +135 -0
- spoe_forge-0.0.1/spoe_forge/server/constants.py +33 -0
- spoe_forge-0.0.1/spoe_forge/server/handler.py +245 -0
- spoe_forge-0.0.1/spoe_forge/spoe_forge.py +146 -0
- spoe_forge-0.0.1/spoe_forge/spop/__init__.py +0 -0
- spoe_forge-0.0.1/spoe_forge/spop/constants.py +112 -0
- spoe_forge-0.0.1/spoe_forge/spop/decoders/__init__.py +0 -0
- spoe_forge-0.0.1/spoe_forge/spop/decoders/data_types.py +282 -0
- spoe_forge-0.0.1/spoe_forge/spop/decoders/payloads.py +204 -0
- spoe_forge-0.0.1/spoe_forge/spop/encoders/__init__.py +0 -0
- spoe_forge-0.0.1/spoe_forge/spop/encoders/data_types.py +307 -0
- spoe_forge-0.0.1/spoe_forge/spop/encoders/payloads.py +196 -0
- spoe_forge-0.0.1/spoe_forge/spop/exception.py +19 -0
- spoe_forge-0.0.1/spoe_forge/spop/frame.py +508 -0
- spoe_forge-0.0.1/spoe_forge/spop/spop_types.py +87 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: spoe-forge
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A pure Python framework for building HAProxy SPOE agents
|
|
5
|
+
Keywords: haproxy,spoe,spoa,spop
|
|
6
|
+
Author: Mike O'Donnell
|
|
7
|
+
Author-email: Mike O'Donnell <mike@devferret.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Dist: pre-commit>=4.1.0 ; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff==0.14.2 ; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest==9.0.1 ; extra == 'test'
|
|
17
|
+
Requires-Dist: pytest-asyncio==1.3.0 ; extra == 'test'
|
|
18
|
+
Requires-Dist: coveralls==4.0.2 ; extra == 'test'
|
|
19
|
+
Requires-Dist: coverage==7.13.0 ; extra == 'test'
|
|
20
|
+
Requires-Dist: pytest-cov==7.0.0 ; extra == 'test'
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Project-URL: Homepage, https://github.com/mwodonnell/spoe-forge
|
|
23
|
+
Project-URL: Repository, https://github.com/mwodonnell/spoe-forge
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# SPOE Forge
|
|
29
|
+
|
|
30
|
+
A pure Python framework for building SPOE (Stream Processing Offload Engine) agents that communicate
|
|
31
|
+
with HAProxy using the SPOA protocol.
|
|
32
|
+
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
34
|
+
[](https://www.python.org/downloads/)
|
|
35
|
+
[](https://coveralls.io/github/mwodonnell/spoe-forge?branch=unit-tests)
|
|
36
|
+
|
|
37
|
+
## Overview
|
|
38
|
+
|
|
39
|
+
SPOE Forge provides a clean, decorator-based API for creating agents that process HAProxy messages and return
|
|
40
|
+
actions. Built with async/await throughout, it's designed for high-performance production environments. *Or at least as
|
|
41
|
+
performant as python will allow.*
|
|
42
|
+
|
|
43
|
+
### Why SPOE Forge?
|
|
44
|
+
|
|
45
|
+
Originally created to power a Google OAuth2 authentication backend for HAProxy, it became clear the project
|
|
46
|
+
could be converted to an abstracted framework. I noticed during the development of this project that there
|
|
47
|
+
was a lack of well-maintained, easily understood implementations of the SPOA protocol in python.
|
|
48
|
+
|
|
49
|
+
### Key Features
|
|
50
|
+
|
|
51
|
+
- **Simple decorator-based API** - Register message handlers with `@agent.message()`
|
|
52
|
+
- **Full SPOP protocol support** - Complete implementation of the SPOA protocol
|
|
53
|
+
- **Health check support** - Built-in HAProxy health check handling
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
Install from PyPI:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install spoe-forge
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### Basic Example
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from spoe_forge import (
|
|
69
|
+
SpoeForge,
|
|
70
|
+
AgentContext,
|
|
71
|
+
SetVarAction,
|
|
72
|
+
ActionScope
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Create an agent
|
|
76
|
+
agent = SpoeForge(name="my-agent", debug=False)
|
|
77
|
+
|
|
78
|
+
# Register a message handler
|
|
79
|
+
@agent.message("check-request")
|
|
80
|
+
def handle_request(ctx: AgentContext) -> list[SetVarAction]:
|
|
81
|
+
"""Process incoming request and set HAProxy variables"""
|
|
82
|
+
|
|
83
|
+
# Get message arguments from HAProxy
|
|
84
|
+
client_ip = ctx.get_arg("client_ip")
|
|
85
|
+
request_path = ctx.get_arg("path")
|
|
86
|
+
|
|
87
|
+
# Your business logic here
|
|
88
|
+
is_allowed = check_access(client_ip, request_path)
|
|
89
|
+
|
|
90
|
+
# Return actions to set HAProxy variables
|
|
91
|
+
return [
|
|
92
|
+
SetVarAction(
|
|
93
|
+
scope=ActionScope.TRANSACTION,
|
|
94
|
+
name="access_allowed",
|
|
95
|
+
value=is_allowed
|
|
96
|
+
)
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
# Start the server
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
agent.run(host="0.0.0.0", port=12345)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### HAProxy Configuration
|
|
105
|
+
|
|
106
|
+
SPOE Forge works with HAProxy's SPOE configuration. For details on configuring HAProxy to communicate with your
|
|
107
|
+
agent, see the [official HAProxy SPOE documentation](https://www.haproxy.org/download/3.3/doc/SPOE.txt).
|
|
108
|
+
|
|
109
|
+
## Local Development
|
|
110
|
+
|
|
111
|
+
### Running with Docker
|
|
112
|
+
|
|
113
|
+
A complete local development environment is provided using Docker Compose, including a sample SPOE agent,
|
|
114
|
+
HAProxy, and a test backend service.
|
|
115
|
+
|
|
116
|
+
**Quick start:**
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
cd docker
|
|
120
|
+
docker compose up --build
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This starts three services:
|
|
124
|
+
- **SPOA Agent** (`spoa`) - Sample SPOE Forge agent running on port 8500
|
|
125
|
+
- **Whoami** (`whoami`) - Simple backend service for testing
|
|
126
|
+
- **HAProxy** (`haproxy`) - Configured to communicate with the WhoAmI example BE Service, the SPOA agent, and is listening on port 8080
|
|
127
|
+
|
|
128
|
+
**Test the setup:**
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Open logs
|
|
132
|
+
docker compose logs
|
|
133
|
+
|
|
134
|
+
# Visit the dev url in your browser
|
|
135
|
+
http://localhost:8080
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Check both the docker logs and the `X-Test-Arg` header displayed on the WhoAmI page.
|
|
139
|
+
|
|
140
|
+
Make any updates to the HAProxy configs or the sample_server.py files in `./docker/` to support
|
|
141
|
+
your testing.
|
|
142
|
+
|
|
143
|
+
## Roadmap
|
|
144
|
+
|
|
145
|
+
Future enhancements under consideration with no timeline guaranteed:
|
|
146
|
+
|
|
147
|
+
- Middleware support
|
|
148
|
+
- Much more extended documentation and examples
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
Any and all contributions welcome.
|
|
157
|
+
|
|
158
|
+
## Support
|
|
159
|
+
|
|
160
|
+
For issues and questions, please [file an issue on GitHub](https://github.com/mwodonnell/spoe-forge/issues).
|
|
161
|
+
|
|
162
|
+
## Acknowledgments
|
|
163
|
+
|
|
164
|
+
Built to solve real-world production needs for HAProxy SPOA agents. Special thanks to the HAProxy team for
|
|
165
|
+
excellent documentation of the SPOE protocol.
|
|
166
|
+
|
|
167
|
+
Extra shoutout to [Christopher Faulet](https://github.com/capflam) for responding to some questions about a
|
|
168
|
+
few hiccups along the way.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# SPOE Forge
|
|
2
|
+
|
|
3
|
+
A pure Python framework for building SPOE (Stream Processing Offload Engine) agents that communicate
|
|
4
|
+
with HAProxy using the SPOA protocol.
|
|
5
|
+
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.python.org/downloads/)
|
|
8
|
+
[](https://coveralls.io/github/mwodonnell/spoe-forge?branch=unit-tests)
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
SPOE Forge provides a clean, decorator-based API for creating agents that process HAProxy messages and return
|
|
13
|
+
actions. Built with async/await throughout, it's designed for high-performance production environments. *Or at least as
|
|
14
|
+
performant as python will allow.*
|
|
15
|
+
|
|
16
|
+
### Why SPOE Forge?
|
|
17
|
+
|
|
18
|
+
Originally created to power a Google OAuth2 authentication backend for HAProxy, it became clear the project
|
|
19
|
+
could be converted to an abstracted framework. I noticed during the development of this project that there
|
|
20
|
+
was a lack of well-maintained, easily understood implementations of the SPOA protocol in python.
|
|
21
|
+
|
|
22
|
+
### Key Features
|
|
23
|
+
|
|
24
|
+
- **Simple decorator-based API** - Register message handlers with `@agent.message()`
|
|
25
|
+
- **Full SPOP protocol support** - Complete implementation of the SPOA protocol
|
|
26
|
+
- **Health check support** - Built-in HAProxy health check handling
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
Install from PyPI:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install spoe-forge
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### Basic Example
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from spoe_forge import (
|
|
42
|
+
SpoeForge,
|
|
43
|
+
AgentContext,
|
|
44
|
+
SetVarAction,
|
|
45
|
+
ActionScope
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Create an agent
|
|
49
|
+
agent = SpoeForge(name="my-agent", debug=False)
|
|
50
|
+
|
|
51
|
+
# Register a message handler
|
|
52
|
+
@agent.message("check-request")
|
|
53
|
+
def handle_request(ctx: AgentContext) -> list[SetVarAction]:
|
|
54
|
+
"""Process incoming request and set HAProxy variables"""
|
|
55
|
+
|
|
56
|
+
# Get message arguments from HAProxy
|
|
57
|
+
client_ip = ctx.get_arg("client_ip")
|
|
58
|
+
request_path = ctx.get_arg("path")
|
|
59
|
+
|
|
60
|
+
# Your business logic here
|
|
61
|
+
is_allowed = check_access(client_ip, request_path)
|
|
62
|
+
|
|
63
|
+
# Return actions to set HAProxy variables
|
|
64
|
+
return [
|
|
65
|
+
SetVarAction(
|
|
66
|
+
scope=ActionScope.TRANSACTION,
|
|
67
|
+
name="access_allowed",
|
|
68
|
+
value=is_allowed
|
|
69
|
+
)
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# Start the server
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
agent.run(host="0.0.0.0", port=12345)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### HAProxy Configuration
|
|
78
|
+
|
|
79
|
+
SPOE Forge works with HAProxy's SPOE configuration. For details on configuring HAProxy to communicate with your
|
|
80
|
+
agent, see the [official HAProxy SPOE documentation](https://www.haproxy.org/download/3.3/doc/SPOE.txt).
|
|
81
|
+
|
|
82
|
+
## Local Development
|
|
83
|
+
|
|
84
|
+
### Running with Docker
|
|
85
|
+
|
|
86
|
+
A complete local development environment is provided using Docker Compose, including a sample SPOE agent,
|
|
87
|
+
HAProxy, and a test backend service.
|
|
88
|
+
|
|
89
|
+
**Quick start:**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cd docker
|
|
93
|
+
docker compose up --build
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This starts three services:
|
|
97
|
+
- **SPOA Agent** (`spoa`) - Sample SPOE Forge agent running on port 8500
|
|
98
|
+
- **Whoami** (`whoami`) - Simple backend service for testing
|
|
99
|
+
- **HAProxy** (`haproxy`) - Configured to communicate with the WhoAmI example BE Service, the SPOA agent, and is listening on port 8080
|
|
100
|
+
|
|
101
|
+
**Test the setup:**
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Open logs
|
|
105
|
+
docker compose logs
|
|
106
|
+
|
|
107
|
+
# Visit the dev url in your browser
|
|
108
|
+
http://localhost:8080
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Check both the docker logs and the `X-Test-Arg` header displayed on the WhoAmI page.
|
|
112
|
+
|
|
113
|
+
Make any updates to the HAProxy configs or the sample_server.py files in `./docker/` to support
|
|
114
|
+
your testing.
|
|
115
|
+
|
|
116
|
+
## Roadmap
|
|
117
|
+
|
|
118
|
+
Future enhancements under consideration with no timeline guaranteed:
|
|
119
|
+
|
|
120
|
+
- Middleware support
|
|
121
|
+
- Much more extended documentation and examples
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
126
|
+
|
|
127
|
+
## Contributing
|
|
128
|
+
|
|
129
|
+
Any and all contributions welcome.
|
|
130
|
+
|
|
131
|
+
## Support
|
|
132
|
+
|
|
133
|
+
For issues and questions, please [file an issue on GitHub](https://github.com/mwodonnell/spoe-forge/issues).
|
|
134
|
+
|
|
135
|
+
## Acknowledgments
|
|
136
|
+
|
|
137
|
+
Built to solve real-world production needs for HAProxy SPOA agents. Special thanks to the HAProxy team for
|
|
138
|
+
excellent documentation of the SPOE protocol.
|
|
139
|
+
|
|
140
|
+
Extra shoutout to [Christopher Faulet](https://github.com/capflam) for responding to some questions about a
|
|
141
|
+
few hiccups along the way.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.9.17,<0.10.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[tool.uv.build-backend]
|
|
6
|
+
module-name = "spoe_forge"
|
|
7
|
+
module-root = ""
|
|
8
|
+
|
|
9
|
+
[project]
|
|
10
|
+
name = "spoe-forge"
|
|
11
|
+
version = "0.0.1"
|
|
12
|
+
description = "A pure Python framework for building HAProxy SPOE agents"
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Mike O'Donnell", email = "mike@devferret.com"},
|
|
15
|
+
]
|
|
16
|
+
readme = "README.md"
|
|
17
|
+
license = {text = "MIT"}
|
|
18
|
+
requires-python = ">=3.12"
|
|
19
|
+
keywords = ["haproxy", "spoe", "spoa", "spop"]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/mwodonnell/spoe-forge"
|
|
30
|
+
Repository = "https://github.com/mwodonnell/spoe-forge"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pre-commit>=4.1.0",
|
|
35
|
+
"ruff==0.14.2",
|
|
36
|
+
]
|
|
37
|
+
test = [
|
|
38
|
+
"pytest==9.0.1",
|
|
39
|
+
"pytest-asyncio==1.3.0",
|
|
40
|
+
"coveralls==4.0.2",
|
|
41
|
+
"coverage==7.13.0",
|
|
42
|
+
"pytest-cov==7.0.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
asyncio_mode = "auto"
|
|
48
|
+
addopts = [
|
|
49
|
+
"--verbose",
|
|
50
|
+
"--cov=spoe_forge",
|
|
51
|
+
"--cov-report=term-missing",
|
|
52
|
+
"--cov-report=html"
|
|
53
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""SPOE Forge - A Python framework for building HAProxy SPOA agents."""
|
|
2
|
+
|
|
3
|
+
from spoe_forge.spoe_forge import SpoeForge
|
|
4
|
+
from spoe_forge.spop.spop_types import SetVarAction, UnsetVarAction, Action
|
|
5
|
+
from spoe_forge.spop.constants import ActionScope
|
|
6
|
+
from spoe_forge.agent.context import AgentContext
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"SpoeForge",
|
|
10
|
+
"AgentContext",
|
|
11
|
+
"Action",
|
|
12
|
+
"ActionScope",
|
|
13
|
+
"SetVarAction",
|
|
14
|
+
"UnsetVarAction",
|
|
15
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from spoe_forge.spop.spop_types import SpoaDataType
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentContext:
|
|
9
|
+
"""
|
|
10
|
+
Context object passed to message handlers containing message data.
|
|
11
|
+
|
|
12
|
+
Provides access to message name and arguments from HAProxy NOTIFY frames.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str, args: dict[str, SpoaDataType]):
|
|
16
|
+
"""
|
|
17
|
+
Create a new agent context.
|
|
18
|
+
|
|
19
|
+
:param str message: Message name from NOTIFY frame
|
|
20
|
+
:param dict[str, SpoaDataType] args: Message arguments
|
|
21
|
+
"""
|
|
22
|
+
self._message = message
|
|
23
|
+
self._args = args
|
|
24
|
+
|
|
25
|
+
def get_arg(self, arg: str, default: SpoaDataType = None) -> SpoaDataType:
|
|
26
|
+
"""
|
|
27
|
+
Retrieve a message argument by name.
|
|
28
|
+
|
|
29
|
+
:param str arg: Argument name to retrieve
|
|
30
|
+
:param default: Default value to return if argument is not present.
|
|
31
|
+
:return: Argument value, or None if not found
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
return self._args[arg]
|
|
35
|
+
except KeyError:
|
|
36
|
+
return default
|
|
37
|
+
|
|
38
|
+
def get_args(self) -> dict[str, SpoaDataType]:
|
|
39
|
+
"""
|
|
40
|
+
Retrieve all message arguments.
|
|
41
|
+
|
|
42
|
+
:return: Dictionary mapping argument names to values
|
|
43
|
+
"""
|
|
44
|
+
return self._args
|
|
45
|
+
|
|
46
|
+
def has_arg(self, arg: str) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Check if an argument exists in the message.
|
|
49
|
+
|
|
50
|
+
:param str arg: Argument name to check
|
|
51
|
+
:return: True if argument exists, False otherwise
|
|
52
|
+
"""
|
|
53
|
+
return arg in self._args
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from spoe_forge.agent.context import AgentContext
|
|
6
|
+
from spoe_forge.agent.exceptions import SpoeAgentError
|
|
7
|
+
from spoe_forge.spop.spop_types import SpoaDataType, Action
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
MessageHandlerFunc = Callable[[AgentContext], list[Action]]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentRegistry:
|
|
15
|
+
"""
|
|
16
|
+
Internal registry mapping message names to handler functions.
|
|
17
|
+
|
|
18
|
+
Note: Users don't interact with this directly - it's used by the Agent class.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
"""Initialize a new agent registry."""
|
|
23
|
+
self._handlers: dict[str, MessageHandlerFunc] = {}
|
|
24
|
+
self._validation_cache: dict[str, bool] = {}
|
|
25
|
+
|
|
26
|
+
def register(self, message_name: str, handler: MessageHandlerFunc) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Register a handler for a message type.
|
|
29
|
+
|
|
30
|
+
:param str message_name: Message name to handle
|
|
31
|
+
:param MessageHandlerFunc handler: User-defined handler function
|
|
32
|
+
"""
|
|
33
|
+
if message_name in self._handlers:
|
|
34
|
+
logger.warning(f"Overwriting existing handler for message '{message_name}'")
|
|
35
|
+
|
|
36
|
+
self._handlers[message_name] = handler
|
|
37
|
+
logger.debug(f"Registered handler for message '{message_name}'")
|
|
38
|
+
|
|
39
|
+
async def handle_message(
|
|
40
|
+
self, message: str, args: dict[str, SpoaDataType]
|
|
41
|
+
) -> list[Action]:
|
|
42
|
+
"""
|
|
43
|
+
Route a message to its registered handler.
|
|
44
|
+
|
|
45
|
+
Wraps synchronous handlers with asyncio.to_thread for async compatibility.
|
|
46
|
+
|
|
47
|
+
:param str message: Message name to handle
|
|
48
|
+
:param dict[str, SpoaDataType] args: Message arguments
|
|
49
|
+
:return: List of actions from handler (empty if no handler registered)
|
|
50
|
+
:raises SpoeAgentError: If handler raises an exception
|
|
51
|
+
"""
|
|
52
|
+
handler = self._handlers.get(message)
|
|
53
|
+
|
|
54
|
+
if handler is None:
|
|
55
|
+
logger.warning(f"No handler registered for message '{message}'")
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
ctx = AgentContext(message, args)
|
|
59
|
+
try:
|
|
60
|
+
return await asyncio.to_thread(handler, ctx)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logger.error(
|
|
63
|
+
f"Error in handler for message '{message}': {e}", exc_info=True
|
|
64
|
+
)
|
|
65
|
+
# Convert to our own error - will be caught in our own handlers to
|
|
66
|
+
# cause a graceful kill of the connection
|
|
67
|
+
raise SpoeAgentError(e)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
_default_handler = logging.StreamHandler(sys.stderr)
|
|
6
|
+
_default_handler.setFormatter(
|
|
7
|
+
logging.Formatter("%(levelname)s | %(asctime)s | %(name)s | %(message)s")
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _has_level_handler(logger: logging.Logger) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
Check if there is a handler in the logging chain that will handle the
|
|
14
|
+
given logger's effective level.
|
|
15
|
+
|
|
16
|
+
This traverses the logger hierarchy to check if any parent logger
|
|
17
|
+
has a handler configured. Based on Flask's implementation.
|
|
18
|
+
|
|
19
|
+
:param logger: Logger to check
|
|
20
|
+
:return: True if a handler exists in the chain, False otherwise
|
|
21
|
+
"""
|
|
22
|
+
level = logger.getEffectiveLevel()
|
|
23
|
+
current = logger
|
|
24
|
+
|
|
25
|
+
while current:
|
|
26
|
+
if any(handler.level <= level for handler in current.handlers):
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
if not current.propagate:
|
|
30
|
+
break
|
|
31
|
+
|
|
32
|
+
current = current.parent
|
|
33
|
+
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def create_logger(debug: bool = False) -> logging.Logger:
|
|
38
|
+
"""
|
|
39
|
+
Get the SPOE Forge logger and configure it if needed.
|
|
40
|
+
|
|
41
|
+
This follows Flask's pattern:
|
|
42
|
+
- Uses 'spoe_forge' as the logger name
|
|
43
|
+
- Only adds default handler if no handler exists in the hierarchy
|
|
44
|
+
- Respects existing logging configuration
|
|
45
|
+
|
|
46
|
+
Internal function - called by spoe_forge.py at import time.
|
|
47
|
+
|
|
48
|
+
:return: Configured logger instance
|
|
49
|
+
"""
|
|
50
|
+
logger = logging.getLogger("spoe_forge")
|
|
51
|
+
|
|
52
|
+
# Only add handler if none exists in the hierarchy
|
|
53
|
+
if not _has_level_handler(logger):
|
|
54
|
+
logger.addHandler(_default_handler)
|
|
55
|
+
|
|
56
|
+
# Set default level if not already set
|
|
57
|
+
if not logger.level:
|
|
58
|
+
logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
|
59
|
+
|
|
60
|
+
return logger
|
|
File without changes
|