src.offers.ssi_agent_offers_client_adapter
SSI-Agent Adapter for offer operations.
1"""SSI-Agent Adapter for offer operations.""" 2 3from dataclasses import asdict, dataclass 4from typing import override 5 6import msgspec 7 8from src.awards.models import Award 9from src.lib.http_client import HttpClient, RequestsHttpClient 10 11from .models import Offer 12from .offers_client_port import ( 13 OfferNotFound, 14 OffersClientError, 15 OffersClientPort, 16) 17 18 19@dataclass 20class _CredentialOffer: 21 credential_issuer: str 22 credential_configuration_ids: list[str] 23 grants: dict[str, dict[str, str]] 24 25 26@dataclass 27class _SsiAgentOfferResponse: 28 """SSI agent response on fetching a single offer from the admin api""" 29 30 id: str 31 grant_types: list[str] 32 credential_offer_uri: dict[str, str] 33 credential_offer: dict[str, _CredentialOffer] 34 subject_id: str | None 35 credential_ids: list[str] 36 form_url_encoded_credential_offer: str 37 pre_authorized_code: str 38 credential_response: str | None 39 status: str 40 tx_code: str | None 41 delivery_options: str | None 42 43 44class SsiAgentOffersClientAdapter(OffersClientPort): 45 """Adapter for SSI Agent offers API.""" 46 47 _ssi_agent_admin_base_url: str 48 _http_client: HttpClient 49 _credential_configuration_id: str 50 51 def __init__( 52 self, 53 ssi_agent_url: str, 54 credential_configuration_id: str, 55 http_client: HttpClient | None = None, 56 ) -> None: 57 """Initialize the adapter. 58 59 Args: 60 ssi_agent_url: The admin base URL of the SSI agent. 61 credential_configuration_id: The credential configuration ID to use 62 for offers. 63 http_client: The HTTP client to use for requests. 64 Defaults to requests module. 65 """ 66 self._ssi_agent_admin_base_url = ssi_agent_url.rstrip("/") 67 self._credential_configuration_id = credential_configuration_id 68 if http_client is not None: 69 self._http_client = http_client 70 else: 71 self._http_client = RequestsHttpClient() 72 73 @override 74 def create(self, offer_id: str, award: Award) -> str: 75 """Create an offer in the SSI agent. 76 77 Args: 78 offer_id: The offer identifier to create. 79 award: The award to issue as a credential. 80 81 Returns: 82 The credential offer URI. 83 """ 84 self._create_credential_for_subject(offer_id, award) 85 offer_uri = self._create_offer(offer_id) 86 return offer_uri 87 88 @override 89 def get(self, offer_id: str) -> Offer: 90 """Retrieve an offer from the SSI agent. 91 92 Args: 93 offer_id: The offer identifier to retrieve. 94 95 Returns: 96 The Offer object with the URI. 97 98 Raises: 99 OfferNotFound: When the offer is not found in the upstream service. 100 OffersClientError: When upstream returns an error or invalid response. 101 """ 102 response = self._http_client.get( 103 f"{self._ssi_agent_admin_base_url}/v0/offers/{offer_id}", 104 ) 105 106 if response.status_code == 404: 107 raise OfferNotFound(f"Offer {offer_id} not found") 108 109 if 400 <= response.status_code < 600: 110 raise OffersClientError( 111 f"Upstream error: {response.status_code} - {response.content.decode()}" 112 ) 113 114 try: 115 response_data: _SsiAgentOfferResponse = msgspec.json.decode( 116 response.content, type=_SsiAgentOfferResponse 117 ) 118 except msgspec.DecodeError as e: 119 raise OffersClientError(f"Invalid response from upstream: {e}") from e 120 121 uri: str = response_data.form_url_encoded_credential_offer 122 123 return Offer( 124 offer_id=offer_id, 125 award_id="", 126 uri=uri, 127 ) 128 129 def _create_credential_for_subject(self, offer_id: str, award: Award) -> None: 130 response = self._http_client.post( 131 f"{self._ssi_agent_admin_base_url}/v0/credentials", 132 json={ 133 "offerId": offer_id, 134 "credential": asdict(award), 135 "credentialConfigurationId": self._credential_configuration_id, 136 "expiresAt": "3025-10-24 11:34:00Z", 137 }, 138 ) 139 140 if 400 <= response.status_code < 600: 141 raise OffersClientError( 142 f"Upstream error: {response.status_code} - {response.content.decode()}" 143 ) 144 145 def _create_offer(self, offer_id: str) -> str: 146 response = self._http_client.post( 147 f"{self._ssi_agent_admin_base_url}/v0/offers", 148 json={ 149 "offerId": offer_id, 150 "credentialConfigurationIds": [self._credential_configuration_id], 151 }, 152 ) 153 154 if 400 <= response.status_code < 600: 155 raise OffersClientError( 156 f"Upstream error: {response.status_code} - {response.content.decode()}" 157 ) 158 159 return response.text
45class SsiAgentOffersClientAdapter(OffersClientPort): 46 """Adapter for SSI Agent offers API.""" 47 48 _ssi_agent_admin_base_url: str 49 _http_client: HttpClient 50 _credential_configuration_id: str 51 52 def __init__( 53 self, 54 ssi_agent_url: str, 55 credential_configuration_id: str, 56 http_client: HttpClient | None = None, 57 ) -> None: 58 """Initialize the adapter. 59 60 Args: 61 ssi_agent_url: The admin base URL of the SSI agent. 62 credential_configuration_id: The credential configuration ID to use 63 for offers. 64 http_client: The HTTP client to use for requests. 65 Defaults to requests module. 66 """ 67 self._ssi_agent_admin_base_url = ssi_agent_url.rstrip("/") 68 self._credential_configuration_id = credential_configuration_id 69 if http_client is not None: 70 self._http_client = http_client 71 else: 72 self._http_client = RequestsHttpClient() 73 74 @override 75 def create(self, offer_id: str, award: Award) -> str: 76 """Create an offer in the SSI agent. 77 78 Args: 79 offer_id: The offer identifier to create. 80 award: The award to issue as a credential. 81 82 Returns: 83 The credential offer URI. 84 """ 85 self._create_credential_for_subject(offer_id, award) 86 offer_uri = self._create_offer(offer_id) 87 return offer_uri 88 89 @override 90 def get(self, offer_id: str) -> Offer: 91 """Retrieve an offer from the SSI agent. 92 93 Args: 94 offer_id: The offer identifier to retrieve. 95 96 Returns: 97 The Offer object with the URI. 98 99 Raises: 100 OfferNotFound: When the offer is not found in the upstream service. 101 OffersClientError: When upstream returns an error or invalid response. 102 """ 103 response = self._http_client.get( 104 f"{self._ssi_agent_admin_base_url}/v0/offers/{offer_id}", 105 ) 106 107 if response.status_code == 404: 108 raise OfferNotFound(f"Offer {offer_id} not found") 109 110 if 400 <= response.status_code < 600: 111 raise OffersClientError( 112 f"Upstream error: {response.status_code} - {response.content.decode()}" 113 ) 114 115 try: 116 response_data: _SsiAgentOfferResponse = msgspec.json.decode( 117 response.content, type=_SsiAgentOfferResponse 118 ) 119 except msgspec.DecodeError as e: 120 raise OffersClientError(f"Invalid response from upstream: {e}") from e 121 122 uri: str = response_data.form_url_encoded_credential_offer 123 124 return Offer( 125 offer_id=offer_id, 126 award_id="", 127 uri=uri, 128 ) 129 130 def _create_credential_for_subject(self, offer_id: str, award: Award) -> None: 131 response = self._http_client.post( 132 f"{self._ssi_agent_admin_base_url}/v0/credentials", 133 json={ 134 "offerId": offer_id, 135 "credential": asdict(award), 136 "credentialConfigurationId": self._credential_configuration_id, 137 "expiresAt": "3025-10-24 11:34:00Z", 138 }, 139 ) 140 141 if 400 <= response.status_code < 600: 142 raise OffersClientError( 143 f"Upstream error: {response.status_code} - {response.content.decode()}" 144 ) 145 146 def _create_offer(self, offer_id: str) -> str: 147 response = self._http_client.post( 148 f"{self._ssi_agent_admin_base_url}/v0/offers", 149 json={ 150 "offerId": offer_id, 151 "credentialConfigurationIds": [self._credential_configuration_id], 152 }, 153 ) 154 155 if 400 <= response.status_code < 600: 156 raise OffersClientError( 157 f"Upstream error: {response.status_code} - {response.content.decode()}" 158 ) 159 160 return response.text
Adapter for SSI Agent offers API.
52 def __init__( 53 self, 54 ssi_agent_url: str, 55 credential_configuration_id: str, 56 http_client: HttpClient | None = None, 57 ) -> None: 58 """Initialize the adapter. 59 60 Args: 61 ssi_agent_url: The admin base URL of the SSI agent. 62 credential_configuration_id: The credential configuration ID to use 63 for offers. 64 http_client: The HTTP client to use for requests. 65 Defaults to requests module. 66 """ 67 self._ssi_agent_admin_base_url = ssi_agent_url.rstrip("/") 68 self._credential_configuration_id = credential_configuration_id 69 if http_client is not None: 70 self._http_client = http_client 71 else: 72 self._http_client = RequestsHttpClient()
Initialize the adapter.
Args: ssi_agent_url: The admin base URL of the SSI agent. credential_configuration_id: The credential configuration ID to use for offers. http_client: The HTTP client to use for requests. Defaults to requests module.
74 @override 75 def create(self, offer_id: str, award: Award) -> str: 76 """Create an offer in the SSI agent. 77 78 Args: 79 offer_id: The offer identifier to create. 80 award: The award to issue as a credential. 81 82 Returns: 83 The credential offer URI. 84 """ 85 self._create_credential_for_subject(offer_id, award) 86 offer_uri = self._create_offer(offer_id) 87 return offer_uri
Create an offer in the SSI agent.
Args: offer_id: The offer identifier to create. award: The award to issue as a credential.
Returns: The credential offer URI.
89 @override 90 def get(self, offer_id: str) -> Offer: 91 """Retrieve an offer from the SSI agent. 92 93 Args: 94 offer_id: The offer identifier to retrieve. 95 96 Returns: 97 The Offer object with the URI. 98 99 Raises: 100 OfferNotFound: When the offer is not found in the upstream service. 101 OffersClientError: When upstream returns an error or invalid response. 102 """ 103 response = self._http_client.get( 104 f"{self._ssi_agent_admin_base_url}/v0/offers/{offer_id}", 105 ) 106 107 if response.status_code == 404: 108 raise OfferNotFound(f"Offer {offer_id} not found") 109 110 if 400 <= response.status_code < 600: 111 raise OffersClientError( 112 f"Upstream error: {response.status_code} - {response.content.decode()}" 113 ) 114 115 try: 116 response_data: _SsiAgentOfferResponse = msgspec.json.decode( 117 response.content, type=_SsiAgentOfferResponse 118 ) 119 except msgspec.DecodeError as e: 120 raise OffersClientError(f"Invalid response from upstream: {e}") from e 121 122 uri: str = response_data.form_url_encoded_credential_offer 123 124 return Offer( 125 offer_id=offer_id, 126 award_id="", 127 uri=uri, 128 )
Retrieve an offer from the SSI agent.
Args: offer_id: The offer identifier to retrieve.
Returns: The Offer object with the URI.
Raises: OfferNotFound: When the offer is not found in the upstream service. OffersClientError: When upstream returns an error or invalid response.