processpype 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.
- processpype-0.1.0/LICENSE +21 -0
- processpype-0.1.0/PKG-INFO +24 -0
- processpype-0.1.0/README.md +3 -0
- processpype-0.1.0/processpype/__init__.py +0 -0
- processpype-0.1.0/processpype/core/README.md +232 -0
- processpype-0.1.0/processpype/core/__init__.py +13 -0
- processpype-0.1.0/processpype/core/application.py +231 -0
- processpype-0.1.0/processpype/core/configuration/__init__.py +14 -0
- processpype-0.1.0/processpype/core/configuration/manager.py +126 -0
- processpype-0.1.0/processpype/core/configuration/models.py +43 -0
- processpype-0.1.0/processpype/core/configuration/providers.py +109 -0
- processpype-0.1.0/processpype/core/logfire.py +78 -0
- processpype-0.1.0/processpype/core/manager.py +173 -0
- processpype-0.1.0/processpype/core/models.py +62 -0
- processpype-0.1.0/processpype/core/router.py +148 -0
- processpype-0.1.0/processpype/core/service/__init__.py +7 -0
- processpype-0.1.0/processpype/core/service/manager.py +25 -0
- processpype-0.1.0/processpype/core/service/router.py +30 -0
- processpype-0.1.0/processpype/core/service/service.py +129 -0
- processpype-0.1.0/processpype/core/system.py +46 -0
- processpype-0.1.0/processpype/services/__init__.py +0 -0
- processpype-0.1.0/processpype/services/monitoring/__init__.py +5 -0
- processpype-0.1.0/processpype/services/monitoring/manager.py +70 -0
- processpype-0.1.0/processpype/services/monitoring/router.py +36 -0
- processpype-0.1.0/processpype/services/monitoring/service.py +61 -0
- processpype-0.1.0/pyproject.toml +78 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Gianluca Pagliara
|
|
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,24 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: processpype
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A modular application framework with built-in FastAPI integration and pluggable services
|
|
5
|
+
Author: Gianluca Pagliara
|
|
6
|
+
Author-email: pagliara.gianluca@gmail.com
|
|
7
|
+
Requires-Python: >=3.13,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
10
|
+
Requires-Dist: fastapi (>=0.115.6,<0.116.0)
|
|
11
|
+
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
12
|
+
Requires-Dist: logfire[fastapi] (>=2.11.1,<3.0.0)
|
|
13
|
+
Requires-Dist: psutil (>=6.1.1,<7.0.0)
|
|
14
|
+
Requires-Dist: pydantic (>=2.10.4,<3.0.0)
|
|
15
|
+
Requires-Dist: pytz (>=2024.2,<2025.0)
|
|
16
|
+
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
|
17
|
+
Requires-Dist: telethon (>=1.38.1,<2.0.0)
|
|
18
|
+
Requires-Dist: uvicorn (>=0.34.0,<0.35.0)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Processpype
|
|
22
|
+
|
|
23
|
+
Processpype is a framework for building and managing an application that is composed of services.
|
|
24
|
+
|
|
File without changes
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# ProcessPype Core Module
|
|
2
|
+
|
|
3
|
+
The core module provides the fundamental building blocks for creating and managing services in the ProcessPype framework. This document outlines the architecture, components, and guides for implementing new services.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
### Core Components
|
|
8
|
+
|
|
9
|
+
#### 1. Application (`application.py`)
|
|
10
|
+
The central orchestrator that manages the lifecycle of the application and its services.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from processpype.core import Application
|
|
14
|
+
|
|
15
|
+
app = await Application.create("config.yaml")
|
|
16
|
+
await app.start()
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Key features:
|
|
20
|
+
- Async context manager support
|
|
21
|
+
- FastAPI integration
|
|
22
|
+
- Service lifecycle management
|
|
23
|
+
- Configuration management
|
|
24
|
+
|
|
25
|
+
#### 2. Application Manager (`manager.py`)
|
|
26
|
+
Handles service registration, state management, and lifecycle operations.
|
|
27
|
+
|
|
28
|
+
Key responsibilities:
|
|
29
|
+
- Service registration and retrieval
|
|
30
|
+
- Service state management
|
|
31
|
+
- Service startup/shutdown orchestration
|
|
32
|
+
|
|
33
|
+
#### 3. Router (`router.py`)
|
|
34
|
+
Provides REST API endpoints for application and service management.
|
|
35
|
+
|
|
36
|
+
Available endpoints:
|
|
37
|
+
- `GET /` - Application status
|
|
38
|
+
- `GET /services` - List registered services
|
|
39
|
+
- `POST /services/{service_name}/start` - Start a service
|
|
40
|
+
- `POST /services/{service_name}/stop` - Stop a service
|
|
41
|
+
|
|
42
|
+
#### 4. Models (`models.py`)
|
|
43
|
+
Core data models and enums for the application.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from processpype.core.models import ServiceState
|
|
47
|
+
|
|
48
|
+
# Available states
|
|
49
|
+
ServiceState.STOPPED
|
|
50
|
+
ServiceState.STARTING
|
|
51
|
+
ServiceState.RUNNING
|
|
52
|
+
ServiceState.STOPPING
|
|
53
|
+
ServiceState.ERROR
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### 5. Configuration (`config/`)
|
|
57
|
+
Handles application and service configuration management using Pydantic models.
|
|
58
|
+
|
|
59
|
+
## Implementing New Services
|
|
60
|
+
|
|
61
|
+
### 1. Basic Service Structure
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from processpype.core.service import Service
|
|
65
|
+
from processpype.core.models import ServiceState
|
|
66
|
+
|
|
67
|
+
class MyService(Service):
|
|
68
|
+
def __init__(self, name: str | None = None):
|
|
69
|
+
super().__init__(name or "my_service")
|
|
70
|
+
|
|
71
|
+
async def start(self) -> None:
|
|
72
|
+
self.set_state(ServiceState.STARTING)
|
|
73
|
+
# Initialize your service
|
|
74
|
+
self.set_state(ServiceState.RUNNING)
|
|
75
|
+
|
|
76
|
+
async def stop(self) -> None:
|
|
77
|
+
self.set_state(ServiceState.STOPPING)
|
|
78
|
+
# Cleanup resources
|
|
79
|
+
self.set_state(ServiceState.STOPPED)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Adding Configuration
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from pydantic import BaseModel
|
|
86
|
+
from processpype.core.config.models import ServiceConfiguration
|
|
87
|
+
|
|
88
|
+
class MyServiceConfig(ServiceConfiguration):
|
|
89
|
+
custom_field: str
|
|
90
|
+
port: int = 8080
|
|
91
|
+
|
|
92
|
+
class MyService(Service):
|
|
93
|
+
def configure(self, config: MyServiceConfig) -> None:
|
|
94
|
+
self._config = config
|
|
95
|
+
# Apply configuration
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 3. Adding API Routes
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from fastapi import APIRouter
|
|
102
|
+
|
|
103
|
+
class MyService(Service):
|
|
104
|
+
def __init__(self, name: str | None = None):
|
|
105
|
+
super().__init__(name or "my_service")
|
|
106
|
+
self._router = APIRouter(prefix=f"/services/{self.name}")
|
|
107
|
+
self._setup_routes()
|
|
108
|
+
|
|
109
|
+
def _setup_routes(self) -> None:
|
|
110
|
+
@self._router.get("/status")
|
|
111
|
+
async def get_status():
|
|
112
|
+
return {"state": self.state}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 4. Error Handling
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from processpype.core.models import ServiceState
|
|
119
|
+
|
|
120
|
+
class MyService(Service):
|
|
121
|
+
async def start(self) -> None:
|
|
122
|
+
try:
|
|
123
|
+
self.set_state(ServiceState.STARTING)
|
|
124
|
+
# Initialize
|
|
125
|
+
self.set_state(ServiceState.RUNNING)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
self.set_error(str(e))
|
|
128
|
+
self.set_state(ServiceState.ERROR)
|
|
129
|
+
raise
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Service Lifecycle
|
|
133
|
+
|
|
134
|
+
1. **Registration**
|
|
135
|
+
```python
|
|
136
|
+
app = await Application.create("config.yaml")
|
|
137
|
+
service = app.register_service(MyService)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
2. **Configuration**
|
|
141
|
+
```yaml
|
|
142
|
+
# config.yaml
|
|
143
|
+
services:
|
|
144
|
+
my_service:
|
|
145
|
+
enabled: true
|
|
146
|
+
custom_field: "value"
|
|
147
|
+
port: 8080
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
3. **Startup**
|
|
151
|
+
- Service state transitions: STOPPED → STARTING → RUNNING
|
|
152
|
+
- Configuration is applied
|
|
153
|
+
- Resources are initialized
|
|
154
|
+
- API routes are registered
|
|
155
|
+
|
|
156
|
+
4. **Runtime**
|
|
157
|
+
- Service handles requests
|
|
158
|
+
- Maintains state
|
|
159
|
+
- Reports health status
|
|
160
|
+
|
|
161
|
+
5. **Shutdown**
|
|
162
|
+
- Service state transitions: RUNNING → STOPPING → STOPPED
|
|
163
|
+
- Resources are cleaned up
|
|
164
|
+
- API routes are unregistered
|
|
165
|
+
|
|
166
|
+
## Best Practices
|
|
167
|
+
|
|
168
|
+
1. **State Management**
|
|
169
|
+
- Always use `set_state()` for state transitions
|
|
170
|
+
- Handle errors appropriately with `set_error()`
|
|
171
|
+
- Check state before operations
|
|
172
|
+
|
|
173
|
+
2. **Configuration**
|
|
174
|
+
- Use Pydantic models for configuration
|
|
175
|
+
- Provide sensible defaults
|
|
176
|
+
- Validate configuration in `configure()`
|
|
177
|
+
|
|
178
|
+
3. **Resource Management**
|
|
179
|
+
- Initialize resources in `start()`
|
|
180
|
+
- Clean up resources in `stop()`
|
|
181
|
+
- Use async context managers when possible
|
|
182
|
+
|
|
183
|
+
4. **Error Handling**
|
|
184
|
+
- Catch and handle exceptions appropriately
|
|
185
|
+
- Set service state to ERROR on failures
|
|
186
|
+
- Provide meaningful error messages
|
|
187
|
+
|
|
188
|
+
5. **API Design**
|
|
189
|
+
- Use FastAPI best practices
|
|
190
|
+
- Prefix routes with service name
|
|
191
|
+
- Provide OpenAPI documentation
|
|
192
|
+
|
|
193
|
+
## Logging
|
|
194
|
+
|
|
195
|
+
The framework uses structured logging via `logfire.py`:
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from processpype.core.logfire import get_service_logger
|
|
199
|
+
|
|
200
|
+
class MyService(Service):
|
|
201
|
+
def __init__(self, name: str | None = None):
|
|
202
|
+
super().__init__(name or "my_service")
|
|
203
|
+
self.logger = get_service_logger(self.name)
|
|
204
|
+
|
|
205
|
+
async def start(self) -> None:
|
|
206
|
+
self.logger.info("Starting service", extra={"config": self._config.dict()})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Testing Services
|
|
210
|
+
|
|
211
|
+
1. **Unit Tests**
|
|
212
|
+
```python
|
|
213
|
+
async def test_my_service():
|
|
214
|
+
service = MyService()
|
|
215
|
+
await service.start()
|
|
216
|
+
assert service.state == ServiceState.RUNNING
|
|
217
|
+
await service.stop()
|
|
218
|
+
assert service.state == ServiceState.STOPPED
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
2. **Integration Tests**
|
|
222
|
+
```python
|
|
223
|
+
from fastapi.testclient import TestClient
|
|
224
|
+
|
|
225
|
+
async def test_my_service_api():
|
|
226
|
+
app = await Application.create()
|
|
227
|
+
service = app.register_service(MyService)
|
|
228
|
+
client = TestClient(app.api)
|
|
229
|
+
|
|
230
|
+
response = client.get("/services/my_service/status")
|
|
231
|
+
assert response.status_code == 200
|
|
232
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Core module for ProcessPype."""
|
|
2
|
+
|
|
3
|
+
from .application import Application
|
|
4
|
+
from .models import ApplicationStatus, ServiceState, ServiceStatus
|
|
5
|
+
from .service.service import Service
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Application",
|
|
9
|
+
"Service",
|
|
10
|
+
"ServiceState",
|
|
11
|
+
"ServiceStatus",
|
|
12
|
+
"ApplicationStatus",
|
|
13
|
+
]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Core application class for ProcessPype."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import uvicorn
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
|
|
10
|
+
from processpype.core.manager import ApplicationManager
|
|
11
|
+
from processpype.core.system import setup_timezone
|
|
12
|
+
|
|
13
|
+
from .configuration import ConfigurationManager
|
|
14
|
+
from .configuration.models import ApplicationConfiguration
|
|
15
|
+
from .logfire import get_service_logger, setup_logfire
|
|
16
|
+
from .models import ServiceState
|
|
17
|
+
from .router import ApplicationRouter
|
|
18
|
+
from .service import Service
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Application:
|
|
22
|
+
"""Core application with built-in FastAPI integration."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config: ApplicationConfiguration):
|
|
25
|
+
"""Initialize the application.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
config: Application configuration
|
|
29
|
+
"""
|
|
30
|
+
self._config = config
|
|
31
|
+
self._initialized = False
|
|
32
|
+
self._lock = asyncio.Lock()
|
|
33
|
+
self._manager: ApplicationManager | None = None
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
async def create(
|
|
37
|
+
cls, config_file: str | None = None, **kwargs: Any
|
|
38
|
+
) -> "Application":
|
|
39
|
+
"""Create application instance with configuration from file and/or kwargs.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
config_file: Optional path to configuration file
|
|
43
|
+
**kwargs: Configuration overrides
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Application instance
|
|
47
|
+
"""
|
|
48
|
+
config = await ConfigurationManager.load_application_config(
|
|
49
|
+
config_file=config_file, **kwargs
|
|
50
|
+
)
|
|
51
|
+
return cls(config)
|
|
52
|
+
|
|
53
|
+
# === Properties ===
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_initialized(self) -> bool:
|
|
57
|
+
"""Check if the application is initialized."""
|
|
58
|
+
return self._initialized
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def config(self) -> ApplicationConfiguration:
|
|
62
|
+
"""Get application configuration."""
|
|
63
|
+
return self._config
|
|
64
|
+
|
|
65
|
+
# === Lifecycle ===
|
|
66
|
+
|
|
67
|
+
async def start(self) -> None:
|
|
68
|
+
"""Start the application and API server."""
|
|
69
|
+
if not self.is_initialized:
|
|
70
|
+
await self.initialize()
|
|
71
|
+
|
|
72
|
+
if not self._manager:
|
|
73
|
+
raise RuntimeError("Application manager not initialized")
|
|
74
|
+
|
|
75
|
+
self._manager.set_state(ServiceState.STARTING)
|
|
76
|
+
self.logger.info(
|
|
77
|
+
f"Starting application on {self._config.host}:{self._config.port}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Start enabled services
|
|
81
|
+
await self._manager.start_enabled_services()
|
|
82
|
+
|
|
83
|
+
# Start uvicorn server
|
|
84
|
+
config = uvicorn.Config(
|
|
85
|
+
self.api,
|
|
86
|
+
host=self._config.host,
|
|
87
|
+
port=self._config.port,
|
|
88
|
+
log_level="debug" if self._config.debug else "info",
|
|
89
|
+
)
|
|
90
|
+
server = uvicorn.Server(config)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
self._manager.set_state(ServiceState.RUNNING)
|
|
94
|
+
await server.serve()
|
|
95
|
+
except Exception as e:
|
|
96
|
+
self._manager.set_state(ServiceState.ERROR)
|
|
97
|
+
self.logger.error(f"Failed to start application: {e}")
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
async def stop(self) -> None:
|
|
101
|
+
"""Stop the application and all services."""
|
|
102
|
+
if not self.is_initialized or not self._manager:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
self._manager.set_state(ServiceState.STOPPING)
|
|
106
|
+
self.logger.info("Stopping application")
|
|
107
|
+
|
|
108
|
+
# Stop all services
|
|
109
|
+
await self._manager.stop_all_services()
|
|
110
|
+
self._manager.set_state(ServiceState.STOPPED)
|
|
111
|
+
|
|
112
|
+
async def __aenter__(self) -> "Application":
|
|
113
|
+
"""Async context manager entry."""
|
|
114
|
+
await self.initialize()
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
async def __aexit__(
|
|
118
|
+
self,
|
|
119
|
+
exc_type: type[BaseException] | None,
|
|
120
|
+
exc_val: BaseException | None,
|
|
121
|
+
exc_tb: TracebackType | None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Async context manager exit."""
|
|
124
|
+
await self.stop()
|
|
125
|
+
|
|
126
|
+
# === Initialization ===
|
|
127
|
+
|
|
128
|
+
async def initialize(self) -> None:
|
|
129
|
+
"""Initialize the application asynchronously."""
|
|
130
|
+
async with self._lock:
|
|
131
|
+
if self.is_initialized:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
setup_timezone()
|
|
135
|
+
|
|
136
|
+
# Initialize FastAPI
|
|
137
|
+
self.api = FastAPI(title=self._config.title, version=self._config.version)
|
|
138
|
+
|
|
139
|
+
# Setup logging
|
|
140
|
+
setup_logfire(
|
|
141
|
+
self.api,
|
|
142
|
+
token=self._config.logfire_key,
|
|
143
|
+
environment=self._config.environment,
|
|
144
|
+
)
|
|
145
|
+
self.logger = get_service_logger("application")
|
|
146
|
+
|
|
147
|
+
# Initialize manager
|
|
148
|
+
self.initialize_manager()
|
|
149
|
+
|
|
150
|
+
# Setup core routes
|
|
151
|
+
self.initialize_router()
|
|
152
|
+
self.logger.info(
|
|
153
|
+
"Application initialized",
|
|
154
|
+
extra={
|
|
155
|
+
"host": self._config.host,
|
|
156
|
+
"port": self._config.port,
|
|
157
|
+
"version": self._config.version,
|
|
158
|
+
"environment": self._config.environment,
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self._initialized = True
|
|
163
|
+
|
|
164
|
+
def initialize_manager(self) -> None:
|
|
165
|
+
"""Initialize the application manager."""
|
|
166
|
+
self._manager = ApplicationManager(self.logger, self._config)
|
|
167
|
+
self._manager.set_state(ServiceState.INITIALIZED)
|
|
168
|
+
|
|
169
|
+
def initialize_router(self) -> None:
|
|
170
|
+
if self._manager is None:
|
|
171
|
+
raise RuntimeError("Application manager not initialized")
|
|
172
|
+
|
|
173
|
+
router = ApplicationRouter(
|
|
174
|
+
get_version=lambda: self._config.version,
|
|
175
|
+
get_state=lambda: self._manager.state
|
|
176
|
+
if self._manager
|
|
177
|
+
else ServiceState.STOPPED,
|
|
178
|
+
get_services=lambda: self._manager.services if self._manager else {},
|
|
179
|
+
start_service=self._manager.start_service,
|
|
180
|
+
stop_service=self._manager.stop_service,
|
|
181
|
+
)
|
|
182
|
+
self.api.include_router(router)
|
|
183
|
+
|
|
184
|
+
# === Service Management ===
|
|
185
|
+
|
|
186
|
+
def register_service(
|
|
187
|
+
self, service_class: type[Service], name: str | None = None
|
|
188
|
+
) -> Service:
|
|
189
|
+
"""Register a new service.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
service_class: Service class to register
|
|
193
|
+
name: Optional service name override
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
The registered service instance
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
RuntimeError: If application is not initialized
|
|
200
|
+
ValueError: If service name is already registered
|
|
201
|
+
"""
|
|
202
|
+
if not self.is_initialized or not self._manager:
|
|
203
|
+
raise RuntimeError(
|
|
204
|
+
"Application must be initialized before registering services"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
service = self._manager.register_service(service_class, name)
|
|
208
|
+
if service.router:
|
|
209
|
+
self.api.include_router(service.router)
|
|
210
|
+
|
|
211
|
+
return service
|
|
212
|
+
|
|
213
|
+
def get_service(self, name: str) -> Service | None:
|
|
214
|
+
"""Get a service by name."""
|
|
215
|
+
if not self._manager:
|
|
216
|
+
return None
|
|
217
|
+
return self._manager.get_service(name)
|
|
218
|
+
|
|
219
|
+
async def start_service(self, service_name: str) -> None:
|
|
220
|
+
"""Start a service by name."""
|
|
221
|
+
if not self.is_initialized or not self._manager:
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
"Application must be initialized before starting services"
|
|
224
|
+
)
|
|
225
|
+
await self._manager.start_service(service_name)
|
|
226
|
+
|
|
227
|
+
async def stop_service(self, service_name: str) -> None:
|
|
228
|
+
"""Stop a service by name."""
|
|
229
|
+
if not self._manager:
|
|
230
|
+
return
|
|
231
|
+
await self._manager.stop_service(service_name)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Configuration management for ProcessPype."""
|
|
2
|
+
|
|
3
|
+
from .manager import ConfigurationManager
|
|
4
|
+
from .models import ConfigurationModel, ServiceConfiguration
|
|
5
|
+
from .providers import ConfigurationProvider, EnvProvider, FileProvider
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ConfigurationManager",
|
|
9
|
+
"ConfigurationModel",
|
|
10
|
+
"ServiceConfiguration",
|
|
11
|
+
"ConfigurationProvider",
|
|
12
|
+
"EnvProvider",
|
|
13
|
+
"FileProvider",
|
|
14
|
+
]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Configuration manager for ProcessPype."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from processpype.core.configuration.models import ApplicationConfiguration
|
|
7
|
+
|
|
8
|
+
from .providers import ConfigurationProvider, EnvProvider, FileProvider
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConfigurationManager:
|
|
12
|
+
"""Configuration manager for ProcessPype."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
"""Initialize manager."""
|
|
16
|
+
self._providers: list[ConfigurationProvider] = []
|
|
17
|
+
self._config: dict[str, Any] = {}
|
|
18
|
+
self._initialized = False
|
|
19
|
+
self._lock = asyncio.Lock()
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
async def load_application_config(
|
|
23
|
+
cls, config_file: str | None = None, **kwargs: Any
|
|
24
|
+
) -> ApplicationConfiguration:
|
|
25
|
+
"""Load application configuration from file and/or kwargs.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
config_file: Optional path to configuration file
|
|
29
|
+
**kwargs: Configuration overrides
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
ApplicationConfiguration instance
|
|
33
|
+
"""
|
|
34
|
+
# Create base configuration from kwargs
|
|
35
|
+
config = ApplicationConfiguration(**kwargs)
|
|
36
|
+
|
|
37
|
+
# If no config file, return the kwargs-based config
|
|
38
|
+
if not config_file:
|
|
39
|
+
return config
|
|
40
|
+
|
|
41
|
+
# Create manager and setup providers
|
|
42
|
+
manager = cls()
|
|
43
|
+
if config_file:
|
|
44
|
+
await manager.add_provider(FileProvider(config_file))
|
|
45
|
+
await manager.add_provider(EnvProvider())
|
|
46
|
+
|
|
47
|
+
# Initialize and load configuration
|
|
48
|
+
await manager.initialize()
|
|
49
|
+
|
|
50
|
+
# Return loaded configuration
|
|
51
|
+
return manager.get_model(ApplicationConfiguration)
|
|
52
|
+
|
|
53
|
+
def has_providers(self) -> bool:
|
|
54
|
+
"""Check if there are any providers registered.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if there are providers registered, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
return len(self._providers) > 0
|
|
60
|
+
|
|
61
|
+
async def add_provider(self, provider: ConfigurationProvider) -> None:
|
|
62
|
+
"""Add configuration provider.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
provider: Configuration provider
|
|
66
|
+
"""
|
|
67
|
+
async with self._lock:
|
|
68
|
+
self._providers.append(provider)
|
|
69
|
+
if self._initialized:
|
|
70
|
+
# Load configuration from the new provider
|
|
71
|
+
provider_config = await provider.load()
|
|
72
|
+
self._config.update(provider_config)
|
|
73
|
+
|
|
74
|
+
async def initialize(self) -> None:
|
|
75
|
+
"""Initialize configuration manager."""
|
|
76
|
+
if self._initialized:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
async with self._lock:
|
|
80
|
+
if not self._initialized: # Double-check inside lock
|
|
81
|
+
# Load configuration from all providers in reverse order
|
|
82
|
+
config: dict[str, Any] = {}
|
|
83
|
+
for provider in reversed(self._providers):
|
|
84
|
+
provider_config = await provider.load()
|
|
85
|
+
config.update(provider_config)
|
|
86
|
+
self._config = config
|
|
87
|
+
self._initialized = True
|
|
88
|
+
|
|
89
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
90
|
+
"""Get configuration value.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
key: Configuration key
|
|
94
|
+
default: Default value if key not found
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Configuration value
|
|
98
|
+
"""
|
|
99
|
+
return self._config.get(key, default)
|
|
100
|
+
|
|
101
|
+
def get_model(
|
|
102
|
+
self, model: type[ApplicationConfiguration]
|
|
103
|
+
) -> ApplicationConfiguration:
|
|
104
|
+
"""Get configuration as model.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
model: Configuration model class
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Configuration model instance
|
|
111
|
+
"""
|
|
112
|
+
return model.model_validate(self._config)
|
|
113
|
+
|
|
114
|
+
async def set(self, key: str, value: Any, save: bool = True) -> None:
|
|
115
|
+
"""Set configuration value.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
key: Configuration key
|
|
119
|
+
value: Configuration value
|
|
120
|
+
save: Whether to save to providers
|
|
121
|
+
"""
|
|
122
|
+
async with self._lock:
|
|
123
|
+
self._config[key] = value
|
|
124
|
+
if save:
|
|
125
|
+
for provider in self._providers:
|
|
126
|
+
await provider.save(self._config)
|