processcube-etw-library 2026.1.22.120215b0__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.
- processcube_etw_library-2026.1.22.120215b0/.github/workflows/build_and_publish.yml +55 -0
- processcube_etw_library-2026.1.22.120215b0/.gitignore +8 -0
- processcube_etw_library-2026.1.22.120215b0/.python-version +1 -0
- processcube_etw_library-2026.1.22.120215b0/PKG-INFO +197 -0
- processcube_etw_library-2026.1.22.120215b0/README.md +184 -0
- processcube_etw_library-2026.1.22.120215b0/pyproject.toml +27 -0
- processcube_etw_library-2026.1.22.120215b0/setup.cfg +4 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/__init__.py +25 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/create_external_task_client.py +33 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/etw_app.py +98 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/health/__init__.py +23 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/health/built_in.py +34 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/health/check.py +45 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/health/handlers.py +44 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/health/models.py +43 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/health/registry.py +28 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/health/routes.py +27 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/identity_provider.py +87 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/server_config.py +30 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/settings.py +106 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library/typed_handler.py +49 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library.egg-info/PKG-INFO +197 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library.egg-info/SOURCES.txt +25 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library.egg-info/dependency_links.txt +1 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library.egg-info/requires.txt +4 -0
- processcube_etw_library-2026.1.22.120215b0/src/processcube_etw_library.egg-info/top_level.txt +1 -0
- processcube_etw_library-2026.1.22.120215b0/uv.lock +1604 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Build and Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-and-publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment:
|
|
16
|
+
name: pypi
|
|
17
|
+
permissions:
|
|
18
|
+
id-token: write
|
|
19
|
+
contents: read
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v6
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v6
|
|
25
|
+
with:
|
|
26
|
+
python-version-file: ".python-version"
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@v7
|
|
30
|
+
with:
|
|
31
|
+
enable-cache: true
|
|
32
|
+
version: "0.9.26"
|
|
33
|
+
|
|
34
|
+
- name: Set version
|
|
35
|
+
id: set_version
|
|
36
|
+
run: |
|
|
37
|
+
DATE_VERSION=$(date +'%Y.%m.%d.%H%M%S')
|
|
38
|
+
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
|
39
|
+
VERSION="${DATE_VERSION}"
|
|
40
|
+
else
|
|
41
|
+
VERSION="${DATE_VERSION}-beta"
|
|
42
|
+
fi
|
|
43
|
+
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
|
|
44
|
+
echo "Setting version to: ${VERSION}"
|
|
45
|
+
|
|
46
|
+
- name: Install dependencies
|
|
47
|
+
run: uv sync --locked
|
|
48
|
+
|
|
49
|
+
- name: Build
|
|
50
|
+
run: uv build
|
|
51
|
+
env:
|
|
52
|
+
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ steps.set_version.outputs.VERSION }}
|
|
53
|
+
|
|
54
|
+
- name: Publish to PyPI
|
|
55
|
+
run: uv publish --token ${{ secrets.PYPI_API_TOKEN }}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: processcube-etw-library
|
|
3
|
+
Version: 2026.1.22.120215b0
|
|
4
|
+
Summary: A library to create ETW apps with the ProcessCube platform.
|
|
5
|
+
Author-email: Jeremy Hill <jeremy.hill@profection.de>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: fastapi[standard]>=0.128.0
|
|
10
|
+
Requires-Dist: oauth2-client>=1.4.2
|
|
11
|
+
Requires-Dist: processcube-client>=5.0.0
|
|
12
|
+
Requires-Dist: tenacity>=9.1.2
|
|
13
|
+
|
|
14
|
+
# ProcessCube ETW Library
|
|
15
|
+
|
|
16
|
+
Build External Task Workers (ETW) for ProcessCube, featuring health checks, typed handlers, and environment-based configuration.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv add processcube-etw-library
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
### Environment Variables
|
|
27
|
+
|
|
28
|
+
The library uses environment variables for configuration. You can set these in your environment or in a `.env` file in your project root.
|
|
29
|
+
|
|
30
|
+
| Variable | Default | Description |
|
|
31
|
+
| ------------------------------------ | -------------------------------------- | ------------------------------------------------ |
|
|
32
|
+
| `PROCESSCUBE_ENGINE_URL` | `http://localhost:56000` | URL of the ProcessCube Engine |
|
|
33
|
+
| `PROCESSCUBE_AUTHORITY_URL` | Auto-discovered from engine | URL of the ProcessCube Authority (OAuth server) |
|
|
34
|
+
| `PROCESSCUBE_ETW_CLIENT_ID` | `test_etw` | OAuth client ID for the External Task Worker |
|
|
35
|
+
| `PROCESSCUBE_ETW_CLIENT_SECRET` | `3ef62eb3-fe49-4c2c-ba6f-73e4d234321b` | OAuth client secret for the External Task Worker |
|
|
36
|
+
| `PROCESSCUBE_ETW_CLIENT_SCOPES` | `engine_etw` | OAuth scopes for the External Task Worker |
|
|
37
|
+
| `MAX_GET_OAUTH_ACCESS_TOKEN_RETRIES` | `10` | Maximum retries for obtaining OAuth access token |
|
|
38
|
+
| `ENVIRONMENT` | `development` | Environment mode (`development` or `production`) |
|
|
39
|
+
|
|
40
|
+
#### Example `.env` File
|
|
41
|
+
|
|
42
|
+
```env
|
|
43
|
+
PROCESSCUBE_ENGINE_URL=http://localhost:56000
|
|
44
|
+
PROCESSCUBE_ETW_CLIENT_ID=my_etw_client
|
|
45
|
+
PROCESSCUBE_ETW_CLIENT_SECRET=my_secret_key
|
|
46
|
+
PROCESSCUBE_ETW_CLIENT_SCOPES=engine_etw
|
|
47
|
+
ENVIRONMENT=production
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Extending Settings
|
|
51
|
+
|
|
52
|
+
You can extend the base settings class to add your own environment variables:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from pydantic import Field
|
|
56
|
+
from processcube_etw_library.settings import ETWSettings, load_settings
|
|
57
|
+
|
|
58
|
+
class MyAppSettings(ETWSettings):
|
|
59
|
+
database_url: str = Field(default="sqlite:///app.db")
|
|
60
|
+
api_key: str = Field(default="")
|
|
61
|
+
|
|
62
|
+
# Load settings with your custom class
|
|
63
|
+
settings = load_settings(MyAppSettings)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### Start the ETW Application
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from processcube_etw_library import new_external_task_worker_app
|
|
72
|
+
|
|
73
|
+
# Create the application
|
|
74
|
+
app = new_external_task_worker_app()
|
|
75
|
+
|
|
76
|
+
# Run the application
|
|
77
|
+
app.run()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Subscribe to External Task Topics
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from processcube_etw_library import new_external_task_worker_app
|
|
84
|
+
|
|
85
|
+
app = new_external_task_worker_app()
|
|
86
|
+
|
|
87
|
+
def my_handler(task):
|
|
88
|
+
# Process the task
|
|
89
|
+
result = {"processed": True}
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
# Subscribe to a topic
|
|
93
|
+
app.subscribe_to_external_task_for_topic("my-topic", my_handler)
|
|
94
|
+
|
|
95
|
+
app.run()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Typed Handlers
|
|
99
|
+
|
|
100
|
+
Use typed handlers for automatic payload validation with Pydantic models:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from pydantic import BaseModel
|
|
104
|
+
from processcube_etw_library import new_external_task_worker_app
|
|
105
|
+
|
|
106
|
+
class MyInput(BaseModel):
|
|
107
|
+
name: str
|
|
108
|
+
value: int
|
|
109
|
+
|
|
110
|
+
class MyOutput(BaseModel):
|
|
111
|
+
result: str
|
|
112
|
+
|
|
113
|
+
app = new_external_task_worker_app()
|
|
114
|
+
|
|
115
|
+
def my_typed_handler(payload: MyInput) -> MyOutput:
|
|
116
|
+
return MyOutput(result=f"Processed {payload.name} with value {payload.value}")
|
|
117
|
+
|
|
118
|
+
app.subscribe_to_external_task_for_topic_typed("my-typed-topic", my_typed_handler)
|
|
119
|
+
|
|
120
|
+
app.run()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Add a Custom Health Check
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from processcube_etw_library import new_external_task_worker_app
|
|
127
|
+
from processcube_etw_library.health import HealthCheck, create_url_health_check
|
|
128
|
+
|
|
129
|
+
app = new_external_task_worker_app()
|
|
130
|
+
|
|
131
|
+
# Add a URL-based health check
|
|
132
|
+
app.add_health_check(
|
|
133
|
+
HealthCheck(
|
|
134
|
+
create_url_health_check("http://my-service:8080/health"),
|
|
135
|
+
service_name="My Service",
|
|
136
|
+
tags=["backend", "api"],
|
|
137
|
+
comments=["Checks if My Service is reachable"],
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Add a custom health check function
|
|
142
|
+
def check_database():
|
|
143
|
+
# Return True if healthy, False otherwise
|
|
144
|
+
try:
|
|
145
|
+
# Your database check logic here
|
|
146
|
+
return True
|
|
147
|
+
except Exception:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
app.add_health_check(
|
|
151
|
+
HealthCheck(
|
|
152
|
+
check_database,
|
|
153
|
+
service_name="Database",
|
|
154
|
+
tags=["backend", "database"],
|
|
155
|
+
comments=["Checks database connectivity"],
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
app.run()
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Managing Health Checks
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# Get all registered health checks
|
|
166
|
+
checks = app.get_health_checks()
|
|
167
|
+
|
|
168
|
+
# Get a specific health check by service name
|
|
169
|
+
db_check = app.get_health_check("Database")
|
|
170
|
+
|
|
171
|
+
# Remove a health check
|
|
172
|
+
app.remove_health_check("Database")
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Disabling Built-in Health Checks
|
|
176
|
+
|
|
177
|
+
By default, the library registers health checks for the ProcessCube Engine and Authority. You can disable these:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
app = new_external_task_worker_app(built_in_health_checks=False)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Health Endpoints
|
|
184
|
+
|
|
185
|
+
The application exposes health endpoints at `/healthyz` and `/readyz` that return the status of all registered health checks.
|
|
186
|
+
To check if the application is running without performing health checks, use `/livez`.
|
|
187
|
+
|
|
188
|
+
## Server Configuration
|
|
189
|
+
|
|
190
|
+
The server configuration is determined by the `ENVIRONMENT` variable:
|
|
191
|
+
|
|
192
|
+
| Setting | Development | Production |
|
|
193
|
+
| ---------- | ----------- | ---------- |
|
|
194
|
+
| Host | `0.0.0.0` | `0.0.0.0` |
|
|
195
|
+
| Port | `8000` | `8000` |
|
|
196
|
+
| Log Level | `debug` | `warning` |
|
|
197
|
+
| Access Log | `true` | `false` |
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# ProcessCube ETW Library
|
|
2
|
+
|
|
3
|
+
Build External Task Workers (ETW) for ProcessCube, featuring health checks, typed handlers, and environment-based configuration.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv add processcube-etw-library
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configuration
|
|
12
|
+
|
|
13
|
+
### Environment Variables
|
|
14
|
+
|
|
15
|
+
The library uses environment variables for configuration. You can set these in your environment or in a `.env` file in your project root.
|
|
16
|
+
|
|
17
|
+
| Variable | Default | Description |
|
|
18
|
+
| ------------------------------------ | -------------------------------------- | ------------------------------------------------ |
|
|
19
|
+
| `PROCESSCUBE_ENGINE_URL` | `http://localhost:56000` | URL of the ProcessCube Engine |
|
|
20
|
+
| `PROCESSCUBE_AUTHORITY_URL` | Auto-discovered from engine | URL of the ProcessCube Authority (OAuth server) |
|
|
21
|
+
| `PROCESSCUBE_ETW_CLIENT_ID` | `test_etw` | OAuth client ID for the External Task Worker |
|
|
22
|
+
| `PROCESSCUBE_ETW_CLIENT_SECRET` | `3ef62eb3-fe49-4c2c-ba6f-73e4d234321b` | OAuth client secret for the External Task Worker |
|
|
23
|
+
| `PROCESSCUBE_ETW_CLIENT_SCOPES` | `engine_etw` | OAuth scopes for the External Task Worker |
|
|
24
|
+
| `MAX_GET_OAUTH_ACCESS_TOKEN_RETRIES` | `10` | Maximum retries for obtaining OAuth access token |
|
|
25
|
+
| `ENVIRONMENT` | `development` | Environment mode (`development` or `production`) |
|
|
26
|
+
|
|
27
|
+
#### Example `.env` File
|
|
28
|
+
|
|
29
|
+
```env
|
|
30
|
+
PROCESSCUBE_ENGINE_URL=http://localhost:56000
|
|
31
|
+
PROCESSCUBE_ETW_CLIENT_ID=my_etw_client
|
|
32
|
+
PROCESSCUBE_ETW_CLIENT_SECRET=my_secret_key
|
|
33
|
+
PROCESSCUBE_ETW_CLIENT_SCOPES=engine_etw
|
|
34
|
+
ENVIRONMENT=production
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Extending Settings
|
|
38
|
+
|
|
39
|
+
You can extend the base settings class to add your own environment variables:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from pydantic import Field
|
|
43
|
+
from processcube_etw_library.settings import ETWSettings, load_settings
|
|
44
|
+
|
|
45
|
+
class MyAppSettings(ETWSettings):
|
|
46
|
+
database_url: str = Field(default="sqlite:///app.db")
|
|
47
|
+
api_key: str = Field(default="")
|
|
48
|
+
|
|
49
|
+
# Load settings with your custom class
|
|
50
|
+
settings = load_settings(MyAppSettings)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Usage
|
|
54
|
+
|
|
55
|
+
### Start the ETW Application
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from processcube_etw_library import new_external_task_worker_app
|
|
59
|
+
|
|
60
|
+
# Create the application
|
|
61
|
+
app = new_external_task_worker_app()
|
|
62
|
+
|
|
63
|
+
# Run the application
|
|
64
|
+
app.run()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Subscribe to External Task Topics
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from processcube_etw_library import new_external_task_worker_app
|
|
71
|
+
|
|
72
|
+
app = new_external_task_worker_app()
|
|
73
|
+
|
|
74
|
+
def my_handler(task):
|
|
75
|
+
# Process the task
|
|
76
|
+
result = {"processed": True}
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
# Subscribe to a topic
|
|
80
|
+
app.subscribe_to_external_task_for_topic("my-topic", my_handler)
|
|
81
|
+
|
|
82
|
+
app.run()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Typed Handlers
|
|
86
|
+
|
|
87
|
+
Use typed handlers for automatic payload validation with Pydantic models:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from pydantic import BaseModel
|
|
91
|
+
from processcube_etw_library import new_external_task_worker_app
|
|
92
|
+
|
|
93
|
+
class MyInput(BaseModel):
|
|
94
|
+
name: str
|
|
95
|
+
value: int
|
|
96
|
+
|
|
97
|
+
class MyOutput(BaseModel):
|
|
98
|
+
result: str
|
|
99
|
+
|
|
100
|
+
app = new_external_task_worker_app()
|
|
101
|
+
|
|
102
|
+
def my_typed_handler(payload: MyInput) -> MyOutput:
|
|
103
|
+
return MyOutput(result=f"Processed {payload.name} with value {payload.value}")
|
|
104
|
+
|
|
105
|
+
app.subscribe_to_external_task_for_topic_typed("my-typed-topic", my_typed_handler)
|
|
106
|
+
|
|
107
|
+
app.run()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Add a Custom Health Check
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from processcube_etw_library import new_external_task_worker_app
|
|
114
|
+
from processcube_etw_library.health import HealthCheck, create_url_health_check
|
|
115
|
+
|
|
116
|
+
app = new_external_task_worker_app()
|
|
117
|
+
|
|
118
|
+
# Add a URL-based health check
|
|
119
|
+
app.add_health_check(
|
|
120
|
+
HealthCheck(
|
|
121
|
+
create_url_health_check("http://my-service:8080/health"),
|
|
122
|
+
service_name="My Service",
|
|
123
|
+
tags=["backend", "api"],
|
|
124
|
+
comments=["Checks if My Service is reachable"],
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Add a custom health check function
|
|
129
|
+
def check_database():
|
|
130
|
+
# Return True if healthy, False otherwise
|
|
131
|
+
try:
|
|
132
|
+
# Your database check logic here
|
|
133
|
+
return True
|
|
134
|
+
except Exception:
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
app.add_health_check(
|
|
138
|
+
HealthCheck(
|
|
139
|
+
check_database,
|
|
140
|
+
service_name="Database",
|
|
141
|
+
tags=["backend", "database"],
|
|
142
|
+
comments=["Checks database connectivity"],
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
app.run()
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Managing Health Checks
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# Get all registered health checks
|
|
153
|
+
checks = app.get_health_checks()
|
|
154
|
+
|
|
155
|
+
# Get a specific health check by service name
|
|
156
|
+
db_check = app.get_health_check("Database")
|
|
157
|
+
|
|
158
|
+
# Remove a health check
|
|
159
|
+
app.remove_health_check("Database")
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Disabling Built-in Health Checks
|
|
163
|
+
|
|
164
|
+
By default, the library registers health checks for the ProcessCube Engine and Authority. You can disable these:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
app = new_external_task_worker_app(built_in_health_checks=False)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Health Endpoints
|
|
171
|
+
|
|
172
|
+
The application exposes health endpoints at `/healthyz` and `/readyz` that return the status of all registered health checks.
|
|
173
|
+
To check if the application is running without performing health checks, use `/livez`.
|
|
174
|
+
|
|
175
|
+
## Server Configuration
|
|
176
|
+
|
|
177
|
+
The server configuration is determined by the `ENVIRONMENT` variable:
|
|
178
|
+
|
|
179
|
+
| Setting | Development | Production |
|
|
180
|
+
| ---------- | ----------- | ---------- |
|
|
181
|
+
| Host | `0.0.0.0` | `0.0.0.0` |
|
|
182
|
+
| Port | `8000` | `8000` |
|
|
183
|
+
| Log Level | `debug` | `warning` |
|
|
184
|
+
| Access Log | `true` | `false` |
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel", "setuptools-scm>=8"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "processcube-etw-library"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A library to create ETW apps with the ProcessCube platform."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Jeremy Hill", email = "jeremy.hill@profection.de" }]
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"fastapi[standard]>=0.128.0",
|
|
15
|
+
"oauth2-client>=1.4.2",
|
|
16
|
+
"processcube-client>=5.0.0",
|
|
17
|
+
"tenacity>=9.1.2",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[tool.uv]
|
|
21
|
+
override-dependencies = ["nest-asyncio>=1.6.0"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
packages = ["processcube_etw_library"]
|
|
25
|
+
package-dir = { "" = "src" }
|
|
26
|
+
|
|
27
|
+
[tool.setuptools_scm]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Prevent nest_asyncio from patching asyncio.run() in processcube_client - it breaks uvicorn's loop_factory
|
|
2
|
+
import nest_asyncio
|
|
3
|
+
nest_asyncio.apply = lambda *args, **kwargs: None
|
|
4
|
+
|
|
5
|
+
from .health import (
|
|
6
|
+
create_url_health_check,
|
|
7
|
+
HealthCheck,
|
|
8
|
+
HealthCheckModel,
|
|
9
|
+
HealthCheckRegistry,
|
|
10
|
+
HealthConditionInfo,
|
|
11
|
+
LivezResponse,
|
|
12
|
+
)
|
|
13
|
+
from .etw_app import new_external_task_worker_app
|
|
14
|
+
from .settings import load_settings, load_settings
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"create_url_health_check",
|
|
18
|
+
"HealthCheck",
|
|
19
|
+
"HealthCheckModel",
|
|
20
|
+
"HealthCheckRegistry",
|
|
21
|
+
"HealthConditionInfo",
|
|
22
|
+
"LivezResponse",
|
|
23
|
+
"new_external_task_worker_app",
|
|
24
|
+
"load_settings",
|
|
25
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# # Prevent nest_asyncio from patching asyncio.run() in processcube_client - it breaks uvicorn's loop_factory
|
|
2
|
+
# import nest_asyncio
|
|
3
|
+
# nest_asyncio.apply = lambda *args, **kwargs: None
|
|
4
|
+
|
|
5
|
+
from processcube_client.external_task import ExternalTaskClient
|
|
6
|
+
|
|
7
|
+
from .identity_provider import IdentityProvider
|
|
8
|
+
from .settings import load_settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_external_task_client() -> ExternalTaskClient:
|
|
12
|
+
settings = load_settings()
|
|
13
|
+
authority_url = settings.processcube_authority_url
|
|
14
|
+
engine_url = settings.processcube_engine_url
|
|
15
|
+
client_name = settings.processcube_etw_client_id
|
|
16
|
+
client_secret = settings.processcube_etw_client_secret
|
|
17
|
+
client_scopes = settings.processcube_etw_client_scopes
|
|
18
|
+
max_get_oauth_access_token_retries = settings.max_get_oauth_access_token_retries
|
|
19
|
+
|
|
20
|
+
identity_provider = IdentityProvider(
|
|
21
|
+
authority_url,
|
|
22
|
+
client_name,
|
|
23
|
+
client_secret,
|
|
24
|
+
client_scopes,
|
|
25
|
+
max_get_oauth_access_token_retries,
|
|
26
|
+
)
|
|
27
|
+
client = ExternalTaskClient(
|
|
28
|
+
engine_url,
|
|
29
|
+
identity=identity_provider,
|
|
30
|
+
install_signals=False,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return client
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from typing import Callable, Optional
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
import uvicorn
|
|
8
|
+
|
|
9
|
+
from .health import (
|
|
10
|
+
add_built_in_health_checks,
|
|
11
|
+
HealthCheck,
|
|
12
|
+
HealthCheckRegistry,
|
|
13
|
+
setup_health_routes
|
|
14
|
+
)
|
|
15
|
+
from .create_external_task_client import create_external_task_client
|
|
16
|
+
from .server_config import get_server_config
|
|
17
|
+
from .typed_handler import create_typed_handler_wrapper
|
|
18
|
+
from processcube_client.external_task import ExternalTaskClient
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExternalTaskWorkerApp:
|
|
22
|
+
_etw_client: ExternalTaskClient
|
|
23
|
+
_health_registry: HealthCheckRegistry
|
|
24
|
+
_executor: ThreadPoolExecutor
|
|
25
|
+
_etw_future: Optional[asyncio.Future]
|
|
26
|
+
_app: FastAPI
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self, etw_client: ExternalTaskClient, built_in_health_checks: bool = True
|
|
30
|
+
):
|
|
31
|
+
self._executor = ThreadPoolExecutor(max_workers=1)
|
|
32
|
+
self._etw_future = None
|
|
33
|
+
self._etw_client = etw_client
|
|
34
|
+
self._app = FastAPI(lifespan=self._lifespan)
|
|
35
|
+
self._health_registry = HealthCheckRegistry()
|
|
36
|
+
if built_in_health_checks:
|
|
37
|
+
add_built_in_health_checks(self._health_registry)
|
|
38
|
+
setup_health_routes(self._app, self._health_registry)
|
|
39
|
+
|
|
40
|
+
@asynccontextmanager
|
|
41
|
+
async def _lifespan(self, app: FastAPI):
|
|
42
|
+
loop = asyncio.get_running_loop()
|
|
43
|
+
self._etw_future = loop.run_in_executor(self._executor, self._etw_client.start)
|
|
44
|
+
|
|
45
|
+
yield
|
|
46
|
+
|
|
47
|
+
loop = asyncio.get_running_loop()
|
|
48
|
+
await loop.run_in_executor(None, self._etw_client.stop)
|
|
49
|
+
|
|
50
|
+
if self._etw_future is not None and not self._etw_future.done():
|
|
51
|
+
self._etw_future.cancel()
|
|
52
|
+
try:
|
|
53
|
+
await self._etw_future
|
|
54
|
+
except asyncio.CancelledError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
self._executor.shutdown(wait=False)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def fastapi_app(self) -> FastAPI:
|
|
61
|
+
return self._app
|
|
62
|
+
|
|
63
|
+
def run(self) -> None:
|
|
64
|
+
config = get_server_config()
|
|
65
|
+
uvicorn.run(self._app, **config)
|
|
66
|
+
|
|
67
|
+
def subscribe_to_external_task_for_topic(
|
|
68
|
+
self, topic: str, handler: Callable, **options
|
|
69
|
+
) -> None:
|
|
70
|
+
self._etw_client.subscribe_to_external_task_topic(topic, handler, **options)
|
|
71
|
+
|
|
72
|
+
def subscribe_to_external_task_for_topic_typed(
|
|
73
|
+
self, topic: str, handler: Callable, **options
|
|
74
|
+
) -> None:
|
|
75
|
+
wrapper = create_typed_handler_wrapper(handler)
|
|
76
|
+
self._etw_client.subscribe_to_external_task_topic(topic, wrapper, **options)
|
|
77
|
+
|
|
78
|
+
def add_health_check(self, check: HealthCheck) -> None:
|
|
79
|
+
self._health_registry.register(check)
|
|
80
|
+
|
|
81
|
+
def remove_health_check(self, service_name: str) -> bool:
|
|
82
|
+
return self._health_registry.unregister(service_name)
|
|
83
|
+
|
|
84
|
+
def get_health_checks(self) -> list[HealthCheck]:
|
|
85
|
+
return self._health_registry.get_all()
|
|
86
|
+
|
|
87
|
+
def get_health_check(self, service_name: str) -> Optional[HealthCheck]:
|
|
88
|
+
return self._health_registry.get_by_name(service_name)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def new_external_task_worker_app(
|
|
92
|
+
built_in_health_checks: bool = True,
|
|
93
|
+
) -> ExternalTaskWorkerApp:
|
|
94
|
+
external_task_client = create_external_task_client()
|
|
95
|
+
|
|
96
|
+
return ExternalTaskWorkerApp(
|
|
97
|
+
external_task_client, built_in_health_checks=built_in_health_checks
|
|
98
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .check import HealthCheck, create_url_health_check
|
|
2
|
+
from .handlers import health_check
|
|
3
|
+
from .models import (
|
|
4
|
+
HealthCheckEntityModel,
|
|
5
|
+
HealthCheckModel,
|
|
6
|
+
HealthConditionInfo,
|
|
7
|
+
LivezResponse,
|
|
8
|
+
)
|
|
9
|
+
from .registry import HealthCheckRegistry
|
|
10
|
+
from .built_in import add_built_in_health_checks
|
|
11
|
+
from .routes import setup_health_routes
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"add_built_in_health_checks",
|
|
15
|
+
"create_url_health_check",
|
|
16
|
+
"health_check",
|
|
17
|
+
"HealthCheck",
|
|
18
|
+
"HealthCheckEntityModel",
|
|
19
|
+
"HealthCheckModel",
|
|
20
|
+
"HealthCheckRegistry",
|
|
21
|
+
"HealthConditionInfo",
|
|
22
|
+
"LivezResponse",
|
|
23
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from ..settings import load_settings
|
|
2
|
+
from .check import HealthCheck, create_url_health_check
|
|
3
|
+
from .registry import HealthCheckRegistry
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def add_built_in_health_checks(registry: HealthCheckRegistry) -> None:
|
|
7
|
+
settings = load_settings()
|
|
8
|
+
|
|
9
|
+
engine_url = (
|
|
10
|
+
settings.processcube_engine_url.strip("/")
|
|
11
|
+
+ "/atlas_engine/api/v1/info"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
authority_url = (
|
|
15
|
+
settings.processcube_authority_url.strip("/")
|
|
16
|
+
+ "/.well-known/openid-configuration"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
registry.register(
|
|
20
|
+
HealthCheck(
|
|
21
|
+
create_url_health_check(engine_url),
|
|
22
|
+
service_name="ProcessCube Engine",
|
|
23
|
+
tags=["core", "backend"],
|
|
24
|
+
comments=["Checks if the ProcessCube Engine is reachable"],
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
registry.register(
|
|
28
|
+
HealthCheck(
|
|
29
|
+
create_url_health_check(authority_url),
|
|
30
|
+
service_name="ProcessCube Authority",
|
|
31
|
+
tags=["core", "auth"],
|
|
32
|
+
comments=["Checks if the ProcessCube Authority is reachable"],
|
|
33
|
+
)
|
|
34
|
+
)
|