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