src.credential_configurations.ssi_agent_credential_configurations_client_adapter

SSI-Agent Adapter for credential configurations operations.

  1"""SSI-Agent Adapter for credential configurations operations."""
  2
  3from dataclasses import asdict, dataclass
  4from typing import override
  5
  6import msgspec
  7
  8from src.lib.http_client import HttpClient, RequestsHttpClient
  9
 10from .credential_configurations_client_port import (
 11    CredentialConfigurationNotFound,
 12    CredentialConfigurationsClientError,
 13    CredentialConfigurationsClientPort,
 14)
 15from .models import (
 16    CredentialConfiguration,
 17    Display,
 18)
 19
 20
 21# Response model for issuer metadata
 22@dataclass
 23class _SsiAgentIssuerMetadata:
 24    """SSI agent issuer metadata response."""
 25
 26    credential_issuer: str
 27    credential_endpoint: str
 28    credential_configurations_supported: dict[str, CredentialConfiguration]
 29
 30
 31@dataclass
 32class _SsiAgentDisplay:
 33    """Display information for a credential configuration."""
 34
 35    name: str | None = None
 36    locale: str | None = None
 37    logo: dict[str, str | None] | None = None
 38
 39    @staticmethod
 40    def from_configuration(configuration: Display) -> "_SsiAgentDisplay":
 41        return _SsiAgentDisplay(
 42            name=configuration.name,
 43            logo=configuration.logo,
 44            locale=configuration.locale,
 45        )
 46
 47
 48@dataclass
 49class _SsiAgentAddPayload:
 50    """Payload for Create (and therefore update) credential configuration on service"""
 51
 52    credential_configuration_id: str | None = None
 53    format: str | None = None
 54    type: list[str] | None = None
 55    display: list[_SsiAgentDisplay] | None = None
 56
 57    @staticmethod
 58    def from_credential_configuration(
 59        configuration: CredentialConfiguration,
 60    ) -> "_SsiAgentAddPayload":
 61        assert configuration.credential_definition is not None, (
 62            "`.credential_definition.type` is required, and must be a list of strings"
 63        )
 64        type_list = configuration.credential_definition.type
 65
 66        if configuration.credential_metadata.display is None:
 67            displays = []
 68        else:
 69            displays = [
 70                _SsiAgentDisplay.from_configuration(display)
 71                for display in configuration.credential_metadata.display
 72            ]
 73
 74        return _SsiAgentAddPayload(
 75            credential_configuration_id=configuration.credential_configuration_id,
 76            format=configuration.format,
 77            type=type_list,
 78            display=displays,
 79        )
 80
 81
 82class SsiAgentCredentialConfigurationsClientAdapter(CredentialConfigurationsClientPort):
 83    """Adapter for SSI Agent credential configurations API."""
 84
 85    _ssi_agent_admin_base_url: str
 86    _ssi_agent_issuer_base_url: str
 87    _http_client: HttpClient
 88
 89    def __init__(
 90        self,
 91        ssi_agent_url: str,
 92        http_client: HttpClient | None = None,
 93    ) -> None:
 94        """Initialize the adapter.
 95
 96        Args:
 97            ssi_agent_url: The admin base URL of the SSI agent.
 98            http_client: The HTTP client to use for requests.
 99                Defaults to requests module.
100        """
101        self._ssi_agent_admin_base_url = ssi_agent_url.rstrip("/")
102        self._ssi_agent_issuer_base_url = ssi_agent_url.rstrip("/")
103        if http_client is not None:
104            self._http_client = http_client
105        else:
106            self._http_client = RequestsHttpClient()
107
108    @override
109    def create(self, configuration: CredentialConfiguration) -> CredentialConfiguration:
110        """Create a new credential configuration.
111
112        Args:
113            configuration: The credential configuration to create.
114
115        Returns:
116            The created credential configuration.
117
118        Raises:
119            CredentialConfigurationsClientError: When upstream service returns an error.
120            ValueError: When the credential configuration ID is not set.
121        """
122        payload = _SsiAgentAddPayload.from_credential_configuration(configuration)
123
124        # For SSI Agent, POST to /v0/credential-configurations with existing
125        # ID updates it
126        response = self._http_client.post(
127            f"{self._ssi_agent_admin_base_url}/v0/credential-configurations",
128            json=asdict(payload),
129        )
130
131        if response.status_code == 404:
132            raise CredentialConfigurationNotFound(
133                f"Configuration {configuration.credential_configuration_id} not found"
134            )
135
136        if 400 <= response.status_code < 600:
137            raise CredentialConfigurationsClientError(
138                f"Upstream error: {response.status_code} - {response.text}"
139            )
140
141        # Rather than returning the input, we re-fetch it, so we get the result as it is
142        # merged and enriched with the issuer config.
143        configuration = self.get(configuration.credential_configuration_id)
144        return configuration
145
146    @override
147    def get(self, configuration_id: str) -> CredentialConfiguration:
148        """Retrieve a credential configuration by ID.
149
150        Args:
151            configuration_id: The unique credential configuration identifier.
152
153        Returns:
154            The matching CredentialConfiguration.
155
156        Raises:
157            CredentialConfigurationNotFound: When not found.
158            CredentialConfigurationsClientError: When upstream service returns an error.
159        """
160        # Get issuer metadata which contains all credential configurations
161        all = self.list()
162
163        # Find by credential_configuration_id
164        for config in all:
165            if config.credential_configuration_id == configuration_id:
166                return config
167
168        raise CredentialConfigurationNotFound(configuration_id)
169
170    @override
171    def list(self) -> list[CredentialConfiguration]:
172        """List all credential configurations.
173
174        Returns:
175            A list of all credential configurations.
176
177        Raises:
178            CredentialConfigurationsClientError: When upstream service returns an error.
179        """
180        response = self._http_client.get(
181            f"{self._ssi_agent_admin_base_url}/.well-known/openid-credential-issuer",
182        )
183
184        if 400 <= response.status_code < 600:
185            raise CredentialConfigurationsClientError(
186                f"Upstream error: {response.status_code} - {response.text}"
187            )
188
189        try:
190            metadata: _SsiAgentIssuerMetadata = msgspec.json.decode(
191                response.content, type=_SsiAgentIssuerMetadata
192            )
193        except msgspec.DecodeError as e:
194            raise CredentialConfigurationsClientError(
195                f"Invalid response from upstream: {e}"
196            ) from e
197
198        configurations: list[CredentialConfiguration] = []
199        for (
200            id,
201            configuration,
202        ) in metadata.credential_configurations_supported.items():
203            configuration.credential_configuration_id = id
204            configurations.append(configuration)
205
206        return configurations
207
208    @override
209    def update(self, configuration: CredentialConfiguration) -> CredentialConfiguration:
210        """Update an existing credential configuration.
211
212        Implemented by "create" with an existing id.
213        See self.create()
214        """
215        return self.create(configuration)
216
217    @override
218    def delete(self, configuration_id: str) -> None:
219        """Delete a credential configuration.
220
221        Args:
222            configuration_id: The unique credential configuration identifier.
223
224        Raises:
225            CredentialConfigurationNotFound: When not found.
226            CredentialConfigurationsClientError: When upstream service returns an error.
227        """
228        raise NotImplementedError(
229            "Delete is not implemented for credential configurations"
230        )
class SsiAgentCredentialConfigurationsClientAdapter(src.credential_configurations.credential_configurations_client_port.CredentialConfigurationsClientPort):
 83class SsiAgentCredentialConfigurationsClientAdapter(CredentialConfigurationsClientPort):
 84    """Adapter for SSI Agent credential configurations API."""
 85
 86    _ssi_agent_admin_base_url: str
 87    _ssi_agent_issuer_base_url: str
 88    _http_client: HttpClient
 89
 90    def __init__(
 91        self,
 92        ssi_agent_url: str,
 93        http_client: HttpClient | None = None,
 94    ) -> None:
 95        """Initialize the adapter.
 96
 97        Args:
 98            ssi_agent_url: The admin base URL of the SSI agent.
 99            http_client: The HTTP client to use for requests.
100                Defaults to requests module.
101        """
102        self._ssi_agent_admin_base_url = ssi_agent_url.rstrip("/")
103        self._ssi_agent_issuer_base_url = ssi_agent_url.rstrip("/")
104        if http_client is not None:
105            self._http_client = http_client
106        else:
107            self._http_client = RequestsHttpClient()
108
109    @override
110    def create(self, configuration: CredentialConfiguration) -> CredentialConfiguration:
111        """Create a new credential configuration.
112
113        Args:
114            configuration: The credential configuration to create.
115
116        Returns:
117            The created credential configuration.
118
119        Raises:
120            CredentialConfigurationsClientError: When upstream service returns an error.
121            ValueError: When the credential configuration ID is not set.
122        """
123        payload = _SsiAgentAddPayload.from_credential_configuration(configuration)
124
125        # For SSI Agent, POST to /v0/credential-configurations with existing
126        # ID updates it
127        response = self._http_client.post(
128            f"{self._ssi_agent_admin_base_url}/v0/credential-configurations",
129            json=asdict(payload),
130        )
131
132        if response.status_code == 404:
133            raise CredentialConfigurationNotFound(
134                f"Configuration {configuration.credential_configuration_id} not found"
135            )
136
137        if 400 <= response.status_code < 600:
138            raise CredentialConfigurationsClientError(
139                f"Upstream error: {response.status_code} - {response.text}"
140            )
141
142        # Rather than returning the input, we re-fetch it, so we get the result as it is
143        # merged and enriched with the issuer config.
144        configuration = self.get(configuration.credential_configuration_id)
145        return configuration
146
147    @override
148    def get(self, configuration_id: str) -> CredentialConfiguration:
149        """Retrieve a credential configuration by ID.
150
151        Args:
152            configuration_id: The unique credential configuration identifier.
153
154        Returns:
155            The matching CredentialConfiguration.
156
157        Raises:
158            CredentialConfigurationNotFound: When not found.
159            CredentialConfigurationsClientError: When upstream service returns an error.
160        """
161        # Get issuer metadata which contains all credential configurations
162        all = self.list()
163
164        # Find by credential_configuration_id
165        for config in all:
166            if config.credential_configuration_id == configuration_id:
167                return config
168
169        raise CredentialConfigurationNotFound(configuration_id)
170
171    @override
172    def list(self) -> list[CredentialConfiguration]:
173        """List all credential configurations.
174
175        Returns:
176            A list of all credential configurations.
177
178        Raises:
179            CredentialConfigurationsClientError: When upstream service returns an error.
180        """
181        response = self._http_client.get(
182            f"{self._ssi_agent_admin_base_url}/.well-known/openid-credential-issuer",
183        )
184
185        if 400 <= response.status_code < 600:
186            raise CredentialConfigurationsClientError(
187                f"Upstream error: {response.status_code} - {response.text}"
188            )
189
190        try:
191            metadata: _SsiAgentIssuerMetadata = msgspec.json.decode(
192                response.content, type=_SsiAgentIssuerMetadata
193            )
194        except msgspec.DecodeError as e:
195            raise CredentialConfigurationsClientError(
196                f"Invalid response from upstream: {e}"
197            ) from e
198
199        configurations: list[CredentialConfiguration] = []
200        for (
201            id,
202            configuration,
203        ) in metadata.credential_configurations_supported.items():
204            configuration.credential_configuration_id = id
205            configurations.append(configuration)
206
207        return configurations
208
209    @override
210    def update(self, configuration: CredentialConfiguration) -> CredentialConfiguration:
211        """Update an existing credential configuration.
212
213        Implemented by "create" with an existing id.
214        See self.create()
215        """
216        return self.create(configuration)
217
218    @override
219    def delete(self, configuration_id: str) -> None:
220        """Delete a credential configuration.
221
222        Args:
223            configuration_id: The unique credential configuration identifier.
224
225        Raises:
226            CredentialConfigurationNotFound: When not found.
227            CredentialConfigurationsClientError: When upstream service returns an error.
228        """
229        raise NotImplementedError(
230            "Delete is not implemented for credential configurations"
231        )

Adapter for SSI Agent credential configurations API.

SsiAgentCredentialConfigurationsClientAdapter( ssi_agent_url: str, http_client: src.lib.http_client.HttpClient | None = None)
 90    def __init__(
 91        self,
 92        ssi_agent_url: str,
 93        http_client: HttpClient | None = None,
 94    ) -> None:
 95        """Initialize the adapter.
 96
 97        Args:
 98            ssi_agent_url: The admin base URL of the SSI agent.
 99            http_client: The HTTP client to use for requests.
100                Defaults to requests module.
101        """
102        self._ssi_agent_admin_base_url = ssi_agent_url.rstrip("/")
103        self._ssi_agent_issuer_base_url = ssi_agent_url.rstrip("/")
104        if http_client is not None:
105            self._http_client = http_client
106        else:
107            self._http_client = RequestsHttpClient()

Initialize the adapter.

Args: ssi_agent_url: The admin base URL of the SSI agent. http_client: The HTTP client to use for requests. Defaults to requests module.

109    @override
110    def create(self, configuration: CredentialConfiguration) -> CredentialConfiguration:
111        """Create a new credential configuration.
112
113        Args:
114            configuration: The credential configuration to create.
115
116        Returns:
117            The created credential configuration.
118
119        Raises:
120            CredentialConfigurationsClientError: When upstream service returns an error.
121            ValueError: When the credential configuration ID is not set.
122        """
123        payload = _SsiAgentAddPayload.from_credential_configuration(configuration)
124
125        # For SSI Agent, POST to /v0/credential-configurations with existing
126        # ID updates it
127        response = self._http_client.post(
128            f"{self._ssi_agent_admin_base_url}/v0/credential-configurations",
129            json=asdict(payload),
130        )
131
132        if response.status_code == 404:
133            raise CredentialConfigurationNotFound(
134                f"Configuration {configuration.credential_configuration_id} not found"
135            )
136
137        if 400 <= response.status_code < 600:
138            raise CredentialConfigurationsClientError(
139                f"Upstream error: {response.status_code} - {response.text}"
140            )
141
142        # Rather than returning the input, we re-fetch it, so we get the result as it is
143        # merged and enriched with the issuer config.
144        configuration = self.get(configuration.credential_configuration_id)
145        return configuration

Create a new credential configuration.

Args: configuration: The credential configuration to create.

Returns: The created credential configuration.

Raises: CredentialConfigurationsClientError: When upstream service returns an error. ValueError: When the credential configuration ID is not set.

@override
def get( self, configuration_id: str) -> src.credential_configurations.models.CredentialConfiguration:
147    @override
148    def get(self, configuration_id: str) -> CredentialConfiguration:
149        """Retrieve a credential configuration by ID.
150
151        Args:
152            configuration_id: The unique credential configuration identifier.
153
154        Returns:
155            The matching CredentialConfiguration.
156
157        Raises:
158            CredentialConfigurationNotFound: When not found.
159            CredentialConfigurationsClientError: When upstream service returns an error.
160        """
161        # Get issuer metadata which contains all credential configurations
162        all = self.list()
163
164        # Find by credential_configuration_id
165        for config in all:
166            if config.credential_configuration_id == configuration_id:
167                return config
168
169        raise CredentialConfigurationNotFound(configuration_id)

Retrieve a credential configuration by ID.

Args: configuration_id: The unique credential configuration identifier.

Returns: The matching CredentialConfiguration.

Raises: CredentialConfigurationNotFound: When not found. CredentialConfigurationsClientError: When upstream service returns an error.

@override
def list(unknown):
171    @override
172    def list(self) -> list[CredentialConfiguration]:
173        """List all credential configurations.
174
175        Returns:
176            A list of all credential configurations.
177
178        Raises:
179            CredentialConfigurationsClientError: When upstream service returns an error.
180        """
181        response = self._http_client.get(
182            f"{self._ssi_agent_admin_base_url}/.well-known/openid-credential-issuer",
183        )
184
185        if 400 <= response.status_code < 600:
186            raise CredentialConfigurationsClientError(
187                f"Upstream error: {response.status_code} - {response.text}"
188            )
189
190        try:
191            metadata: _SsiAgentIssuerMetadata = msgspec.json.decode(
192                response.content, type=_SsiAgentIssuerMetadata
193            )
194        except msgspec.DecodeError as e:
195            raise CredentialConfigurationsClientError(
196                f"Invalid response from upstream: {e}"
197            ) from e
198
199        configurations: list[CredentialConfiguration] = []
200        for (
201            id,
202            configuration,
203        ) in metadata.credential_configurations_supported.items():
204            configuration.credential_configuration_id = id
205            configurations.append(configuration)
206
207        return configurations

List all credential configurations.

Returns: A list of all credential configurations.

Raises: CredentialConfigurationsClientError: When upstream service returns an error.

209    @override
210    def update(self, configuration: CredentialConfiguration) -> CredentialConfiguration:
211        """Update an existing credential configuration.
212
213        Implemented by "create" with an existing id.
214        See self.create()
215        """
216        return self.create(configuration)

Update an existing credential configuration.

Implemented by "create" with an existing id. See self.create()

@override
def delete(self, configuration_id: str) -> None:
218    @override
219    def delete(self, configuration_id: str) -> None:
220        """Delete a credential configuration.
221
222        Args:
223            configuration_id: The unique credential configuration identifier.
224
225        Raises:
226            CredentialConfigurationNotFound: When not found.
227            CredentialConfigurationsClientError: When upstream service returns an error.
228        """
229        raise NotImplementedError(
230            "Delete is not implemented for credential configurations"
231        )

Delete a credential configuration.

Args: configuration_id: The unique credential configuration identifier.

Raises: CredentialConfigurationNotFound: When not found. CredentialConfigurationsClientError: When upstream service returns an error.