processcube-etw-library 2025.8.22__tar.gz → 2026.1.19b0__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.
Files changed (35) hide show
  1. processcube_etw_library-2026.1.19b0/.github/workflows/build_and_publish.yml +61 -0
  2. processcube_etw_library-2026.1.19b0/.gitignore +7 -0
  3. processcube_etw_library-2026.1.19b0/.python-version +1 -0
  4. processcube_etw_library-2026.1.19b0/PKG-INFO +24 -0
  5. processcube_etw_library-2026.1.19b0/README.md +11 -0
  6. processcube_etw_library-2026.1.19b0/pyproject.toml +27 -0
  7. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/__init__.py +19 -0
  8. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/create_external_task_client.py +44 -0
  9. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/etw_app.py +115 -0
  10. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/health/__init__.py +23 -0
  11. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/health/built_in.py +32 -0
  12. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/health/check.py +45 -0
  13. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/health/handlers.py +44 -0
  14. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/health/models.py +43 -0
  15. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/health/registry.py +28 -0
  16. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/health/routes.py +27 -0
  17. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/identity_provider.py +87 -0
  18. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/server_config.py +27 -0
  19. processcube_etw_library-2026.1.19b0/src/processcube_etw_library/typed_handler.py +49 -0
  20. processcube_etw_library-2026.1.19b0/src/processcube_etw_library.egg-info/PKG-INFO +24 -0
  21. processcube_etw_library-2026.1.19b0/src/processcube_etw_library.egg-info/SOURCES.txt +24 -0
  22. processcube_etw_library-2026.1.19b0/src/processcube_etw_library.egg-info/requires.txt +4 -0
  23. processcube_etw_library-2026.1.19b0/src/processcube_etw_library.egg-info/top_level.txt +1 -0
  24. processcube_etw_library-2026.1.19b0/uv.lock +1604 -0
  25. processcube_etw_library-2025.8.22/PKG-INFO +0 -15
  26. processcube_etw_library-2025.8.22/README.md +0 -0
  27. processcube_etw_library-2025.8.22/pyproject.toml +0 -27
  28. processcube_etw_library-2025.8.22/src/processcube/__init__.py +0 -0
  29. processcube_etw_library-2025.8.22/src/processcube/identity_provider.py +0 -63
  30. processcube_etw_library-2025.8.22/src/processcube_etw_library.egg-info/PKG-INFO +0 -15
  31. processcube_etw_library-2025.8.22/src/processcube_etw_library.egg-info/SOURCES.txt +0 -9
  32. processcube_etw_library-2025.8.22/src/processcube_etw_library.egg-info/requires.txt +0 -7
  33. processcube_etw_library-2025.8.22/src/processcube_etw_library.egg-info/top_level.txt +0 -1
  34. {processcube_etw_library-2025.8.22 → processcube_etw_library-2026.1.19b0}/setup.cfg +0 -0
  35. {processcube_etw_library-2025.8.22 → processcube_etw_library-2026.1.19b0}/src/processcube_etw_library.egg-info/dependency_links.txt +0 -0
@@ -0,0 +1,61 @@
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: Install uv
24
+ uses: astral-sh/setup-uv@v7
25
+ with:
26
+ enable-cache: true
27
+ version: "0.9.26"
28
+
29
+ - name: Set up Python
30
+ uses: actions/setup-python@v6
31
+ with:
32
+ python-version-file: ".python-version"
33
+
34
+ - name: Set version
35
+ run: |
36
+ DATE_VERSION=$(date +'%Y.%m.%d')
37
+ if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
38
+ VERSION="${DATE_VERSION}"
39
+ else
40
+ VERSION="${DATE_VERSION}-beta"
41
+ fi
42
+ echo "VERSION=${VERSION}" >> $GITHUB_ENV
43
+ echo "Setting version to: ${VERSION}"
44
+
45
+ - name: Debug GitHub context
46
+ run: |
47
+ echo "GITHUB EVENT: ${{ github.event_name }}"
48
+ echo "GITHUB REF: ${{ github.ref }}"
49
+ echo "GITHUB BASE REF: ${{ github.base_ref }}"
50
+ echo "GITHUB HEAD REF: ${{ github.head_ref }}"
51
+
52
+ - name: Install dependencies
53
+ run: uv sync --locked
54
+
55
+ - name: Build
56
+ run: uv build
57
+ env:
58
+ SETUPTOOLS_SCM_PRETEND_VERSION: ${{ env.VERSION }}
59
+
60
+ - name: Publish to PyPI
61
+ run: uv publish --token ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,7 @@
1
+ .DS_Store
2
+ .env
3
+ .env*
4
+ .venv/
5
+ __pycache__/
6
+ *.xlsx
7
+ *.egg-info/
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: processcube-etw-library
3
+ Version: 2026.1.19b0
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
+ ## Installation
17
+
18
+ ## Usage
19
+
20
+ ### Start the ETW application
21
+
22
+ ### Subscribe to External Task Topics
23
+
24
+ ### Add a custom health check
@@ -0,0 +1,11 @@
1
+ # ProcessCube ETW Library
2
+
3
+ ## Installation
4
+
5
+ ## Usage
6
+
7
+ ### Start the ETW application
8
+
9
+ ### Subscribe to External Task Topics
10
+
11
+ ### Add a custom health check
@@ -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,19 @@
1
+ from .health import (
2
+ create_url_health_check,
3
+ HealthCheck,
4
+ HealthCheckModel,
5
+ HealthCheckRegistry,
6
+ HealthConditionInfo,
7
+ LivezResponse,
8
+ )
9
+ from .etw_app import new_external_task_worker_app
10
+
11
+ __all__ = [
12
+ "create_url_health_check",
13
+ "HealthCheck",
14
+ "HealthCheckModel",
15
+ "HealthCheckRegistry",
16
+ "HealthConditionInfo",
17
+ "LivezResponse",
18
+ "new_external_task_worker_app",
19
+ ]
@@ -0,0 +1,44 @@
1
+ from os import getenv
2
+
3
+ # Prevent nest_asyncio from patching asyncio.run() in processcube_client - it breaks uvicorn's loop_factory
4
+ import nest_asyncio
5
+ nest_asyncio.apply = lambda *args, **kwargs: None
6
+
7
+ from processcube_client.external_task import ExternalTaskClient
8
+ from processcube_client.app_info import AppInfoClient
9
+
10
+ from .identity_provider import IdentityProvider
11
+
12
+
13
+ def _determine_authority_url(engine_url: str) -> str:
14
+ if authority := getenv("PROCESSCUBE_AUTHORITY_URL"):
15
+ return authority
16
+
17
+ app_info_client = AppInfoClient(engine_url)
18
+ authority_url = app_info_client.get_authority()
19
+ return authority_url # type: ignore
20
+
21
+
22
+ def create_external_task_client(
23
+ engine_url: str,
24
+ client_name: str,
25
+ client_secret: str,
26
+ client_scopes: str,
27
+ max_get_oauth_access_token_retries: int,
28
+ ) -> ExternalTaskClient:
29
+ authority_url = _determine_authority_url(engine_url)
30
+
31
+ identity_provider = IdentityProvider(
32
+ authority_url,
33
+ client_name,
34
+ client_secret,
35
+ client_scopes,
36
+ max_get_oauth_access_token_retries,
37
+ )
38
+ client = ExternalTaskClient(
39
+ engine_url,
40
+ identity=identity_provider,
41
+ install_signals=False,
42
+ )
43
+
44
+ return client
@@ -0,0 +1,115 @@
1
+ import asyncio
2
+ from concurrent.futures import ThreadPoolExecutor
3
+ from contextlib import asynccontextmanager
4
+ import os
5
+ from typing import Callable, Optional
6
+
7
+ from fastapi import FastAPI
8
+ import uvicorn
9
+
10
+ from .health import (
11
+ add_built_in_health_checks,
12
+ HealthCheck,
13
+ HealthCheckRegistry,
14
+ setup_health_routes
15
+ )
16
+ from .create_external_task_client import create_external_task_client
17
+ from .server_config import get_server_config
18
+ from .typed_handler import create_typed_handler_wrapper
19
+ from processcube_client.external_task import ExternalTaskClient
20
+
21
+
22
+ class ExternalTaskWorkerApp:
23
+ _etw_client: ExternalTaskClient
24
+ _health_registry: HealthCheckRegistry
25
+ _executor: ThreadPoolExecutor
26
+ _etw_future: Optional[asyncio.Future]
27
+ _app: FastAPI
28
+
29
+ def __init__(
30
+ self, etw_client: ExternalTaskClient, built_in_health_checks: bool = True
31
+ ):
32
+ self._executor = ThreadPoolExecutor(max_workers=1)
33
+ self._etw_future = None
34
+ self._etw_client = etw_client
35
+ self._app = FastAPI(lifespan=self._lifespan)
36
+ self._health_registry = HealthCheckRegistry()
37
+ if built_in_health_checks:
38
+ add_built_in_health_checks(self._health_registry)
39
+ setup_health_routes(self._app, self._health_registry)
40
+
41
+ @asynccontextmanager
42
+ async def _lifespan(self, app: FastAPI):
43
+ loop = asyncio.get_running_loop()
44
+ self._etw_future = loop.run_in_executor(self._executor, self._etw_client.start)
45
+
46
+ yield
47
+
48
+ loop = asyncio.get_running_loop()
49
+ await loop.run_in_executor(None, self._etw_client.stop)
50
+
51
+ if self._etw_future is not None and not self._etw_future.done():
52
+ self._etw_future.cancel()
53
+ try:
54
+ await self._etw_future
55
+ except asyncio.CancelledError:
56
+ pass
57
+
58
+ self._executor.shutdown(wait=False)
59
+
60
+ @property
61
+ def fastapi_app(self) -> FastAPI:
62
+ return self._app
63
+
64
+ def run(self) -> None:
65
+ config = get_server_config()
66
+ uvicorn.run(self._app, **config)
67
+
68
+ def subscribe_to_external_task_for_topic(
69
+ self, topic: str, handler: Callable, **options
70
+ ) -> None:
71
+ self._etw_client.subscribe_to_external_task_topic(topic, handler, **options)
72
+
73
+ def subscribe_to_external_task_for_topic_typed(
74
+ self, topic: str, handler: Callable, **options
75
+ ) -> None:
76
+ wrapper = create_typed_handler_wrapper(handler)
77
+ self._etw_client.subscribe_to_external_task_topic(topic, wrapper, **options)
78
+
79
+ def add_health_check(self, check: HealthCheck) -> None:
80
+ self._health_registry.register(check)
81
+
82
+ def remove_health_check(self, service_name: str) -> bool:
83
+ return self._health_registry.unregister(service_name)
84
+
85
+ def get_health_checks(self) -> list[HealthCheck]:
86
+ return self._health_registry.get_all()
87
+
88
+ def get_health_check(self, service_name: str) -> Optional[HealthCheck]:
89
+ return self._health_registry.get_by_name(service_name)
90
+
91
+
92
+ def new_external_task_worker_app(
93
+ built_in_health_checks: bool = True,
94
+ ) -> ExternalTaskWorkerApp:
95
+ engine_url = os.getenv("PROCESSCUBE_ENGINE_URL", "http://localhost:56000")
96
+ etw_client_name = os.getenv("PROCESSCUBE_ETW_CLIENT_ID", "test_etw")
97
+ etw_client_secret = os.getenv(
98
+ "PROCESSCUBE_ETW_CLIENT_SECRET", "3ef62eb3-fe49-4c2c-ba6f-73e4d234321b"
99
+ )
100
+ etw_client_scopes = os.getenv("PROCESSCUBE_ETW_CLIENT_SCOPES", "engine_etw")
101
+ max_get_oauth_access_token_retries = int(
102
+ os.getenv("MAX_GET_OAUTH_ACCESS_TOKEN_RETRIES", 10)
103
+ )
104
+
105
+ external_task_client = create_external_task_client(
106
+ engine_url,
107
+ etw_client_name,
108
+ etw_client_secret,
109
+ etw_client_scopes,
110
+ max_get_oauth_access_token_retries,
111
+ )
112
+
113
+ return ExternalTaskWorkerApp(
114
+ external_task_client, built_in_health_checks=built_in_health_checks
115
+ )
@@ -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,32 @@
1
+ import os
2
+
3
+ from .check import HealthCheck, create_url_health_check
4
+ from .registry import HealthCheckRegistry
5
+
6
+
7
+ def add_built_in_health_checks(registry: HealthCheckRegistry) -> None:
8
+ engine_url = (
9
+ os.getenv("PROCESSCUBE_ENGINE_URL", "http://localhost:56000").strip("/")
10
+ + "/atlas_engine/api/v1/info"
11
+ )
12
+ authority_url = (
13
+ os.getenv("PROCESSCUBE_AUTHORITY_URL", "http://localhost:56020").strip("/")
14
+ + "/.well-known/openid-configuration"
15
+ )
16
+
17
+ registry.register(
18
+ HealthCheck(
19
+ create_url_health_check(engine_url),
20
+ service_name="ProcessCube Engine",
21
+ tags=["core", "backend"],
22
+ comments=["Checks if the ProcessCube Engine is reachable"],
23
+ )
24
+ )
25
+ registry.register(
26
+ HealthCheck(
27
+ create_url_health_check(authority_url),
28
+ service_name="ProcessCube Authority",
29
+ tags=["core", "auth"],
30
+ comments=["Checks if the ProcessCube Authority is reachable"],
31
+ )
32
+ )
@@ -0,0 +1,45 @@
1
+ from collections.abc import Awaitable, Callable
2
+ from datetime import timedelta
3
+ from time import perf_counter
4
+ import inspect
5
+
6
+ import httpx
7
+
8
+ from .models import HealthCheckResult
9
+
10
+
11
+ type HealthCheckCondition = Callable[[], bool | Awaitable[bool]]
12
+
13
+ class HealthCheck:
14
+ def __init__(
15
+ self,
16
+ condition: HealthCheckCondition,
17
+ service_name: str,
18
+ tags: list[str] | None = None,
19
+ comments: list[str] | None = None,
20
+ ):
21
+ self.condition = condition
22
+ self.service_name = service_name
23
+ self.tags = tags or []
24
+ self.comments = comments or []
25
+
26
+ async def run(self) -> HealthCheckResult:
27
+ start = perf_counter()
28
+ try:
29
+ result = self.condition()
30
+ if inspect.isawaitable(result):
31
+ result = await result
32
+ healthy = bool(result)
33
+ except Exception:
34
+ healthy = False
35
+ elapsed = perf_counter() - start
36
+ return HealthCheckResult(healthy=healthy, time_taken=timedelta(seconds=elapsed))
37
+
38
+
39
+ def create_url_health_check(url: str, timeout: float = 5.0) -> HealthCheckCondition:
40
+ async def check():
41
+ async with httpx.AsyncClient() as client:
42
+ response = await client.get(url, timeout=timeout)
43
+ return True if response.status_code == 200 else False
44
+
45
+ return check
@@ -0,0 +1,44 @@
1
+ import asyncio
2
+ from datetime import timedelta
3
+ from time import perf_counter
4
+
5
+ from fastapi.responses import JSONResponse
6
+
7
+ from .check import HealthCheck
8
+ from .models import (
9
+ HealthCheckEntityModel,
10
+ HealthCheckModel,
11
+ )
12
+ from .registry import HealthCheckRegistry
13
+
14
+
15
+ async def _run_health_checks(checks: list[HealthCheck]) -> HealthCheckModel:
16
+ total_start = perf_counter()
17
+ results = await asyncio.gather(*(check.run() for check in checks))
18
+ total_elapsed = perf_counter() - total_start
19
+
20
+ entities = []
21
+ all_healthy = True
22
+ for check, result in zip(checks, results):
23
+ if not result.healthy:
24
+ all_healthy = False
25
+ entity = HealthCheckEntityModel(
26
+ service=check.service_name,
27
+ status="healthy" if result.healthy else "unhealthy",
28
+ time_taken=result.time_taken,
29
+ tags=check.tags,
30
+ comments=check.comments,
31
+ )
32
+ entities.append(entity)
33
+
34
+ return HealthCheckModel(
35
+ status="healthy" if all_healthy else "unhealthy",
36
+ total_time_taken=timedelta(seconds=total_elapsed),
37
+ entities=entities,
38
+ )
39
+
40
+
41
+ async def health_check(registry: HealthCheckRegistry) -> HealthCheckModel:
42
+ health_result = await _run_health_checks(registry.get_all())
43
+ status_code = 200 if health_result.status == "healthy" else 503
44
+ return JSONResponse(content=health_result.model_dump(), status_code=status_code)
@@ -0,0 +1,43 @@
1
+ from datetime import timedelta
2
+
3
+ from pydantic import BaseModel, Field, field_serializer
4
+
5
+
6
+ class HealthCheckEntityModel(BaseModel):
7
+ service: str
8
+ status: str = "healthy"
9
+ time_taken: timedelta = Field(default=timedelta())
10
+ tags: list[str] = Field(default_factory=list)
11
+ comments: list[str] = Field(default_factory=list)
12
+
13
+ @field_serializer("time_taken")
14
+ def serialize_time_taken(self, time: timedelta) -> str:
15
+ return str(time)
16
+
17
+
18
+ class HealthCheckModel(BaseModel):
19
+ status: str = "healthy"
20
+ total_time_taken: timedelta = Field(default=timedelta())
21
+ entities: list[HealthCheckEntityModel] = Field(default_factory=list)
22
+
23
+ @field_serializer("total_time_taken")
24
+ def serialize_total_time_taken(self, time: timedelta) -> str:
25
+ return str(time)
26
+
27
+
28
+ class LivezResponse(BaseModel):
29
+ status: str = Field(default="alive", description="Liveness status")
30
+
31
+
32
+ class HealthConditionInfo(BaseModel):
33
+ service_name: str = Field(..., description="Name of the service")
34
+ tags: list[str] = Field(default_factory=list, description="Tags for categorization")
35
+ comments: list[str] = Field(default_factory=list, description="Comments describing the check")
36
+
37
+ class HealthCheckResult(BaseModel):
38
+ healthy: bool = Field(..., description="Indicates if the health check passed")
39
+ time_taken: timedelta = Field(..., description="Time taken to perform the health check")
40
+
41
+ @field_serializer("time_taken")
42
+ def serialize_time_taken(self, time: timedelta) -> str:
43
+ return str(time)
@@ -0,0 +1,28 @@
1
+ from .check import HealthCheck
2
+
3
+
4
+ class HealthCheckRegistry:
5
+ def __init__(self):
6
+ self._checks: list[HealthCheck] = []
7
+
8
+ def register(self, check: HealthCheck) -> None:
9
+ existing_names = {c.service_name for c in self._checks}
10
+ if check.service_name in existing_names:
11
+ raise ValueError(f"Health check with service name '{check.service_name}' already exists")
12
+ self._checks.append(check)
13
+
14
+ def unregister(self, service_name: str) -> bool:
15
+ for i, check in enumerate(self._checks):
16
+ if check.service_name == service_name:
17
+ self._checks.pop(i)
18
+ return True
19
+ return False
20
+
21
+ def get_all(self) -> list[HealthCheck]:
22
+ return self._checks.copy()
23
+
24
+ def get_by_name(self, service_name: str) -> HealthCheck | None:
25
+ for check in self._checks:
26
+ if check.service_name == service_name:
27
+ return check
28
+ return None
@@ -0,0 +1,27 @@
1
+ from fastapi import FastAPI
2
+
3
+ from .registry import HealthCheckRegistry
4
+ from .handlers import health_check
5
+ from .models import (
6
+ HealthCheckModel,
7
+ LivezResponse,
8
+ )
9
+
10
+
11
+ def setup_health_routes(app: FastAPI, registry: HealthCheckRegistry) -> None:
12
+ @app.get(
13
+ "/healthyz",
14
+ response_model=HealthCheckModel,
15
+ responses={503: {"model": HealthCheckModel}},
16
+ )
17
+ @app.get(
18
+ "/readyz",
19
+ response_model=HealthCheckModel,
20
+ responses={503: {"model": HealthCheckModel}},
21
+ )
22
+ async def health_check_route() -> HealthCheckModel:
23
+ return await health_check(registry)
24
+
25
+ @app.get("/livez", response_model=LivezResponse)
26
+ def livez() -> LivezResponse:
27
+ return LivezResponse(status="alive")
@@ -0,0 +1,87 @@
1
+ import logging
2
+ import sys
3
+
4
+ from oauth2_client.credentials_manager import CredentialManager, ServiceInformation
5
+ from tenacity import (
6
+ retry,
7
+ stop_after_attempt,
8
+ wait_exponential,
9
+ retry_if_exception_type,
10
+ )
11
+ from requests import exceptions
12
+
13
+ logger = logging.getLogger("processcube.oauth")
14
+
15
+
16
+ def exit_after_retries(retry_state):
17
+ logger.error(
18
+ f"Failed to obtain OAuth access token after {retry_state} attempts. Exiting."
19
+ )
20
+ sys.exit(1)
21
+
22
+
23
+ def log_failed_attempt(retry_state):
24
+ logger.warning(
25
+ f"Attempt {retry_state.attempt_number} to obtain OAuth access token failed: {retry_state.outcome.exception()}"
26
+ )
27
+
28
+
29
+ class IdentityProvider:
30
+
31
+ def __init__(
32
+ self,
33
+ authority_url: str,
34
+ client_name: str,
35
+ client_secret: str,
36
+ client_scopes: str,
37
+ max_get_oauth_access_token_retries: int,
38
+ ):
39
+ self._authority_url = authority_url
40
+ self._client_name = client_name
41
+ self._client_secret = client_secret
42
+ self._client_scopes = client_scopes
43
+ self._max_get_oauth_access_token_retries = max_get_oauth_access_token_retries
44
+
45
+ self._access_token_caller = self._prepare_get_access_token_caller()
46
+ logger.debug(
47
+ f"Prepare identity provider with (authority_url={authority_url}, client_name={client_name}, client_secret=***, client_scopes={client_scopes})."
48
+ )
49
+
50
+ def __call__(self):
51
+ return self._access_token_caller()
52
+
53
+ def _prepare_get_access_token_caller(self):
54
+
55
+ @retry(
56
+ stop=stop_after_attempt(self._max_get_oauth_access_token_retries),
57
+ wait=wait_exponential(multiplier=1, min=2, max=30),
58
+ retry=retry_if_exception_type(exceptions.ConnectionError),
59
+ after=log_failed_attempt,
60
+ retry_error_callback=exit_after_retries,
61
+ )
62
+ def get_access_token(authority_url, client_name, client_secret, client_scopes):
63
+ logger.debug(
64
+ f"Get access token from ProcessCube (authority_url={authority_url}, client_name={client_name}, client_secret=***, client_scopes={client_scopes})."
65
+ )
66
+
67
+ client_scopes = client_scopes.split(" ")
68
+
69
+ service_information = ServiceInformation(
70
+ f"{authority_url}/auth",
71
+ f"{authority_url}/token",
72
+ client_name,
73
+ client_secret,
74
+ client_scopes,
75
+ )
76
+ manager = CredentialManager(service_information)
77
+
78
+ manager.init_with_client_credentials()
79
+
80
+ return {"token": manager._access_token}
81
+
82
+ return lambda: get_access_token(
83
+ self._authority_url,
84
+ self._client_name,
85
+ self._client_secret,
86
+ self._client_scopes,
87
+ )
@@ -0,0 +1,27 @@
1
+ from os import getenv
2
+ from typing import TypedDict
3
+
4
+
5
+ class ServerConfig(TypedDict, total=False):
6
+ host: str
7
+ port: int
8
+ log_level: str
9
+ access_log: bool
10
+ reload: bool
11
+
12
+
13
+ def get_server_config() -> ServerConfig:
14
+ if getenv("ENVIRONMENT") == "production":
15
+ return ServerConfig(
16
+ host="0.0.0.0",
17
+ port=8000,
18
+ log_level="warning",
19
+ access_log=False,
20
+ )
21
+ else:
22
+ return ServerConfig(
23
+ host="0.0.0.0",
24
+ port=8000,
25
+ log_level="debug",
26
+ access_log=True,
27
+ )