Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ee4b53e
feat(platform): support external token providers and simplify caching
akunft Mar 26, 2026
813c81a
fixup! feat(platform): support external token providers and simplify …
akunft Mar 30, 2026
00be537
fixup! feat(platform): support external token providers and simplify …
akunft Mar 30, 2026
9474ded
fixup! feat(platform): support external token providers and simplify …
akunft Mar 31, 2026
dd946fe
fixup! feat(platform): support external token providers and simplify …
akunft Mar 31, 2026
0f5af27
fixup! feat(platform): support external token providers and simplify …
akunft Apr 7, 2026
e15f7e8
fixup! feat(platform): support external token providers and simplify …
akunft Apr 10, 2026
9cfc32f
Potential fix for pull request finding
akunft Apr 10, 2026
2f7a91b
Merge branch 'main' into feat/enable_external_token_provider
helmut-hoffer-von-ankershoffen Apr 26, 2026
c0c3670
test(platform): extract test constants and add resource guard test [P…
helmut-hoffer-von-ankershoffen Apr 26, 2026
372d0cf
docs(requirements): add SHR-PLATFORM-1 + SWR-PLATFORM-1-1 for externa…
helmut-hoffer-von-ankershoffen Apr 26, 2026
7f37146
fix(tests): satisfy SonarQube python:S7632 by simplifying noqa syntax…
helmut-hoffer-von-ankershoffen Apr 26, 2026
4097283
fix(tests): remove redundant wall-clock assertion that flaked on Wind…
helmut-hoffer-von-ankershoffen Apr 26, 2026
d9ab794
Merge branch 'main' into feat/enable_external_token_provider
helmut-hoffer-von-ankershoffen Apr 26, 2026
c082df5
ci: re-trigger CI after skip:test:long_running label removal [PYSDK-112]
helmut-hoffer-von-ankershoffen Apr 26, 2026
033e3e1
ci: trigger fresh run with full test matrix [PYSDK-112]
helmut-hoffer-von-ankershoffen Apr 26, 2026
888a147
docs: temporarily comment out risks
olivermeyer Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions specifications/SPEC_PLATFORM_SERVICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The Platform Module shall:
- **[FR-11]** Download and verify file integrity using CRC32C checksums for run artifacts
- **[FR-12]** Generate signed URLs for secure Google Cloud Storage access
- **[FR-13]** Provide user and organization information retrieval with sensitive data masking options
- **[FR-14]** Support external token providers to bypass internal OAuth 2.0 flows for machine-to-machine, service account, or custom token lifecycle scenarios.

### 1.3 Non-Functional Requirements

Expand All @@ -44,7 +45,6 @@ The Platform Module shall:
### 1.4 Constraints and Limitations

- OAuth 2.0 dependency: Requires external Auth0 service for authentication, creating external dependency
- Browser dependency: Interactive flow requires web browser availability, limiting headless deployment options
- Network dependency: Requires internet connectivity for initial authentication and token validation
- Platform-specific: Designed specifically for Aignostics Platform API integration

Expand All @@ -58,6 +58,7 @@ The Platform Module shall:
platform/
├── _service.py # Core service implementation with health monitoring
├── _client.py # API client factory and configuration management
├── _api.py # Authenticated API wrapper (_AuthenticatedApi, _AuthenticatedResource)
├── _authentication.py # OAuth flows and token management
├── _cli.py # Command-line interface for user operations
├── _settings.py # Environment-specific configuration management
Expand Down Expand Up @@ -88,7 +89,7 @@ platform/

- **Factory Pattern**: `Client.get_api_client()` creates configured API clients based on environment settings
- **Service Layer Pattern**: Business logic encapsulated in service classes with clean separation from API details
- **Strategy Pattern**: Multiple authentication flows (Authorization Code vs Device Flow) selected based on environment capabilities
- **Strategy Pattern**: Multiple authentication flows (Authorization Code vs Device Flow) selected based on environment capabilities; external token provider as a fully independent alternative strategy
- **Template Method Pattern**: Base authentication flow with specific implementations for different OAuth grant types

---
Expand All @@ -107,10 +108,10 @@ platform/

### 3.2 Outputs

| Output Type | Destination | Format/Type | Success Criteria | Code Location |
| ---------------- | ------------------- | ---------------------- | --------------------------------------------------- | ---------------------------------------------------- |
| JWT Access Token | Token cache/memory | String | Valid JWT with required claims and unexpired | `_authentication.py::get_token()` return value |
| API Client | Client applications | PublicApi object | Authenticated and configured for target environment | `_client.py::Client.get_api_client()` factory method |
| Output Type | Destination | Format/Type | Success Criteria | Code Location |
| ---------------- | ------------------- | ---------------------------- | --------------------------------------------------- | ---------------------------------------------------- |
| JWT Access Token | Token cache/memory | String | Valid JWT with required claims and unexpired | `_authentication.py::get_token()` return value |
| API Client | Client applications | `Client` object | Authenticated and configured for target environment | `_client.py::Client.__init__()` constructor |
Copy link
Copy Markdown
Collaborator Author

@akunft akunft Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olivermeyer IMO, the type is correct: The user gets a client object, not the authenticatedAPI. If we want to align with the other code locations by using a factory class method, we would need to change the factory (I would not do that).

Unless you have major concerns, I would leave it as is

| User Information | CLI/Application | UserInfo/Me objects | Complete user and organization data | `_service.py::Service.get_user_info()` method |
| Health Status | Monitoring systems | Health object | Accurate service and dependency status | `_service.py::Service.health()` method |
| Downloaded Files | Local filesystem | Binary/structured data | Verified checksums and complete downloads | `_utils.py` download functions and `ApplicationRun` |
Comment on lines +111 to 117
Expand Down Expand Up @@ -171,7 +172,9 @@ UserInfo:

```mermaid
graph TD
A[User Request] --> B{Token Cached?}
A[User Request] --> X{External Token Provider?}
X -->|Yes| I[Create API Client with External Provider]
X -->|No| B{Token Cached?}
B -->|Yes| C[Use Cached Token]
B -->|No| D[OAuth Authentication]

Expand Down Expand Up @@ -202,7 +205,11 @@ graph TD
class Client:
"""Main client for interacting with the Aignostics Platform API."""

def __init__(self, cache_token: bool = True) -> None:
def __init__(
self,
cache_token: bool = True,
token_provider: Callable[[], str] | None = None,
) -> None:
"""Initializes authenticated API client with resource accessors."""

def me(self) -> Me:
Expand All @@ -215,7 +222,10 @@ class Client:
"""Creates ApplicationRun instance for existing run."""

@staticmethod
def get_api_client(cache_token: bool = True) -> PublicApi:
def get_api_client(
cache_token: bool = True,
token_provider: Callable[[], str] | None = None,
) -> _AuthenticatedApi: # internal subclass of PublicApi; exposes token_provider attribute
"""Creates authenticated API client with proper configuration."""
```

Expand All @@ -242,10 +252,10 @@ class Service(BaseService):
```

```python
class Applications:
class Applications(_AuthenticatedResource):
"""Resource class for managing applications."""

def __init__(self, api: PublicApi) -> None:
def __init__(self, api: _AuthenticatedApi) -> None:
"""Initializes the Applications resource with the API client."""

def list(self) -> Iterator[Application]:
Expand All @@ -257,11 +267,11 @@ class Applications:
```

```python
class Versions:
class Versions(_AuthenticatedResource):
"""Resource class for managing application versions."""

def __init__(self, api: PublicApi) -> None:
"""Initializes the Versions resource with the API client."""
# Constructor inherited from _AuthenticatedResource
# def __init__(self, api: _AuthenticatedApi) -> None:

def list(self, application: Application | str) -> Iterator[ApplicationVersion]:
"""Find all versions for a specific application."""
Expand All @@ -277,11 +287,11 @@ class Versions:
```

```python
class Runs:
class Runs(_AuthenticatedResource):
"""Resource class for managing application runs."""

def __init__(self, api: PublicApi) -> None:
"""Initializes the Runs resource with the API client."""
# Constructor inherited from _AuthenticatedResource
# def __init__(self, api: _AuthenticatedApi) -> None:

def create(self, application_version: str, items: list[ItemCreationRequest]) -> ApplicationRun:
"""Creates a new application run."""
Expand All @@ -297,10 +307,10 @@ class Runs:
```

```python
class ApplicationRun:
class ApplicationRun(_AuthenticatedResource):
"""Represents a single application run."""

def __init__(self, api: PublicApi, application_run_id: str) -> None:
def __init__(self, api: _AuthenticatedApi, application_run_id: str) -> None:
"""Initializes an ApplicationRun instance."""

@classmethod
Expand Down
Loading
Loading