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.
@@ -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
+
@@ -0,0 +1,3 @@
1
+ # Processpype
2
+
3
+ Processpype is a framework for building and managing an application that is composed of services.
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)