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
class SsiAgentOffersClientAdapter(src.offers.offers_client_port.OffersClientPort):
 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.

@override
def get(self, offer_id: str) -> src.offers.models.Offer:
 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.