diff --git a/apps/core/views.py b/apps/core/views.py index a052e5e..cbe01eb 100644 --- a/apps/core/views.py +++ b/apps/core/views.py @@ -1,3 +1,5 @@ +import logging + from django.contrib.auth.decorators import login_required from django.shortcuts import render from django.utils.decorators import method_decorator @@ -16,6 +18,8 @@ from apps.core.serializers import ConfigSerializer from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements +logger = logging.getLogger(__name__) + # @method_decorator(login_required, name='dispatch') class HomeView(TemplateView): template_name = 'core/home.html' diff --git a/apps/gooyal_oauth2/rest_framework.py b/apps/gooyal_oauth2/rest_framework.py index b9fec63..eb510f8 100644 --- a/apps/gooyal_oauth2/rest_framework.py +++ b/apps/gooyal_oauth2/rest_framework.py @@ -1,19 +1,93 @@ import logging +from django.core.exceptions import ImproperlyConfigured from oauth2_provider.contrib.rest_framework import TokenMatchesOASRequirements, OAuth2Authentication from rest_framework.permissions import ( - IsAuthenticated + IsAuthenticated, BasePermission ) -log = logging.getLogger("oauth2_provider") +logger = logging.getLogger("oauth2_provider") class IsAuthenticatedOrTokenMatchesOASRequirements(TokenMatchesOASRequirements): def has_permission(self, request, view): + logger.debug(f'try to authenticate {request} for {view} in IsAuthenticatedOrTokenMatchesOASRequirements') is_authenticated = IsAuthenticated().has_permission(request, view) + logger.debug(f'is_authenticated: {is_authenticated}') oauth2authenticated = False if is_authenticated: oauth2authenticated = isinstance(request.successful_authenticator, OAuth2Authentication) + logger.debug(f'oauth2authenticated: {oauth2authenticated}') + token_has_scope = TokenMatchesOASRequirements() - return (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view) + logger.debug(f'token_has_scope: {token_has_scope}') + + result = (is_authenticated and not oauth2authenticated) or token_has_scope.has_permission(request, view) + logger.debug(f'authentication result: {result}') + return result + + +class TokenMatchesViewSetActions(BasePermission): + """ + :attr:action_required_scopes: dict keyed by view set action name with value: iterable action scope lists + + This fulfills the [Open API Specification (OAS; formerly Swagger)](https://www.openapis.org/) + list of alternative Security Requirements Objects for oauth2 or openIdConnect: + When a list of Security Requirement Objects is defined on the Open API object or Operation Object, + only one of Security Requirement Objects in the list needs to be satisfied to authorize the request. + [1](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securityRequirementObject) + + For each method, a list of lists of allowed scopes is tried in order and the first to match succeeds. + + @example + required_action_scopes = { + 'list': [['read']], + 'create': [['create1','scope2'], ['alt-scope3'], ['alt-scope4','alt-scope5']], + } + + TODO: DRY: subclass TokenHasScope and iterate over values of required_scope? + """ + + def has_permission(self, request, view): + token = request.auth + + if not token: + return False + + if hasattr(token, "scope"): # OAuth 2 + required_action_scopes = self.get_required_action_scopes(request, view) + + # TODO: use action map instead to analyze method + action_map = view.action_map + a = view.action + + if a in required_action_scopes: + logger.debug( + "Required scopes alternatives to access resource: {0}".format( + required_action_scopes[a] + ) + ) + for alt in required_action_scopes[a]: + if token.is_valid(alt): + return True + return False + else: + logger.warning("no scope action defined for action {0}".format(a)) + return False + + assert False, ( + "TokenMatchesViewSetActions requires the" + "`oauth2_provider.rest_framework.OAuth2Authentication` authentication " + "class to be used." + ) + + def get_required_action_scopes(self, request, view): + try: + return getattr(view, "required_action_scopes") + except AttributeError: + raise ImproperlyConfigured( + "TokenMatchesViewSetActions requires the view to" + " define the required_action_scopes attribute" + ) + diff --git a/apps/users/serializers.py b/apps/users/serializers.py index e4bf1cc..773c310 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -156,3 +156,9 @@ class SessionSerializer(serializers.ModelSerializer): model = AccessToken fields = ('uuid', 'created', "detail", "is_current") read_only_fields = ['uuid', 'created', "detail", "is_current" ] + + +# TODO: better solution +# this is a proxy api for submit event for promotion +class ReferralSetSerializer(serializers.Serializer): + referral = serializers.UUIDField() diff --git a/apps/users/views.py b/apps/users/views.py index 9607ec6..1bf7abe 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -1,3 +1,4 @@ +from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.contrib.auth.views import LoginView @@ -9,19 +10,25 @@ from django.utils.decorators import method_decorator from django.views.generic import DetailView, UpdateView from django_filters.rest_framework import DjangoFilterBackend from oauth2_provider.contrib.rest_framework import IsAuthenticatedOrTokenHasScope -from rest_framework import generics, status, permissions +from rest_framework import generics, status, permissions, mixins +from rest_framework.decorators import action +from rest_framework.generics import get_object_or_404 from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.viewsets import GenericViewSet -from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements +from apps.gooyal_oauth2.rest_framework import IsAuthenticatedOrTokenMatchesOASRequirements, TokenMatchesViewSetActions +from apps.gooyal_oauth2.throttling import get_application from apps.users.filters import UserFilter from apps.users.forms import OTPAuthenticationForm, ProfileUpdateForm from apps.users.models import User from apps.users.provinces_and_cities import State from apps.users.serializers import PublicUserSerializer, AccountSerializer, RequestOTPSerializer, RequestOTTSerializer, \ - ChangePasswordSerializer, UserInquirySerializer, SessionSerializer + ChangePasswordSerializer, UserInquirySerializer, SessionSerializer, ReferralSetSerializer +from utils.clients.promotions_client import promote_user from utils.throttles import RequestOTPDayRateThrottle, RequestOTPMinRateThrottle +from utils import exceptions UserModel = get_user_model() @@ -47,6 +54,7 @@ class UserPublicRetrieveView(generics.RetrieveAPIView): "GET": [["accounts.profile:retrieve"]], } + class UserDetailedRetrieveView(generics.RetrieveAPIView): permission_classes = [IsAuthenticatedOrTokenMatchesOASRequirements] queryset = User.objects.all() @@ -56,6 +64,7 @@ class UserDetailedRetrieveView(generics.RetrieveAPIView): "GET": [["accounts.profile:detailed_retrieve"]], } + class UserCurrentAvatarUrlView(generics.RetrieveAPIView): permission_classes = [AllowAny] queryset = User.objects.all() @@ -92,6 +101,7 @@ class RequestOTPView(generics.CreateAPIView): required_scopes = [] throttle_classes = [RequestOTPMinRateThrottle, RequestOTPDayRateThrottle] + class RequestOTTView(generics.CreateAPIView): permission_classes = [IsAuthenticatedOrTokenHasScope] required_scopes = ['accounts.account:request_ott'] @@ -197,3 +207,56 @@ class UserSessionListView(generics.ListAPIView): from apps.gooyal_oauth2.models import AccessToken return AccessToken.objects.filter(user=self.request.user).all() + +class UserViewSet( + # mixins.CreateModelMixin, + GenericViewSet +): + serializer_class = AccountSerializer + + permission_classes = [TokenMatchesViewSetActions] + required_action_scopes = { + "set_referral": [["promotions.application.user-plan:promote"]], + "retrieve": [["promotions.application.user-plan:list-retrieve"]], + "list": [["promotions.application.user-plan:list-retrieve"]], + } + + # TODO: better solution + # this is a proxy api for submit event in promotion service + @action(detail=False, methods=['POST'], serializer_class=ReferralSetSerializer) + def set_referral(self, request, *args, **kwargs): + user = self.request.user + application = get_application(self.request) + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + referral = serializer.validated_data.get("referral") + referral_user = get_object_or_404(User.objects.all(), pk=referral) + + data = { + "referral": referral, + "application": application.pk, + "user": user.pk, + } + plan = settings.PROMOTIONS_REFERRAL_PLAN + + result = promote_user(user, plan, 'accounts::submit_referral', data) + # result_json = { + # "promotions": [ + # { + # "uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + # "event": { + # "label": "string", + # "data": "string", + # "user": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + # "application": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + # }, + # "promotion_amount": 2147483647, + # "state": 1, + # "user": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + # } + # ] + # } + if not result: + raise exceptions.Conflict('can not set referral') + + return Response(serializer.validated_data) diff --git a/main/settings.py b/main/settings.py index 3dd9f8f..922f371 100644 --- a/main/settings.py +++ b/main/settings.py @@ -382,3 +382,7 @@ MINIO_POLICY_HOOKS: List[Tuple[str, dict]] = [] MINIO_BUCKET_CHECK_ON_SAVE = True # Default: True // Creates bucket if missing, then save MOBIN_SMS_TOKEN = config('MOBIN_SMS_TOKEN', default='') + +PROMOTIONS_BASE_PUBLIC_URL = config('PROMOTIONS_BASE_PUBLIC_URL', default=None, cast=str) +PROMOTIONS_BASE_PRIVATE_URL = config('PROMOTIONS_BASE_PRIVATE_URL', default=None, cast=str) +PROMOTIONS_REFERRAL_PLAN = config('PROMOTIONS_REFERRAL_PLAN', default=None, cast=str) diff --git a/utils/clients/gooyal_promotions_client/__init__.py b/utils/clients/gooyal_promotions_client/__init__.py new file mode 100644 index 0000000..83cbf89 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/__init__.py @@ -0,0 +1,8 @@ +"""A client library for accessing service""" + +from .client import AuthenticatedClient, Client + +__all__ = ( + "AuthenticatedClient", + "Client", +) diff --git a/utils/clients/gooyal_promotions_client/api/__init__.py b/utils/clients/gooyal_promotions_client/api/__init__.py new file mode 100644 index 0000000..81f9fa2 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/__init__.py @@ -0,0 +1 @@ +"""Contains methods for accessing the API""" diff --git a/utils/clients/gooyal_promotions_client/api/api/__init__.py b/utils/clients/gooyal_promotions_client/api/api/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/api/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_event_create.py b/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_event_create.py new file mode 100644 index 0000000..0d396b9 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_event_create.py @@ -0,0 +1,167 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.event import Event +from ...types import Response + + +def _get_kwargs( + user_uuid: str, + *, + body: Event, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/v2/promotions/application/{user_uuid}/event/".format( + user_uuid=quote(str(user_uuid), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Event | None: + if response.status_code == 201: + response_201 = Event.from_dict(response.json()) + + return response_201 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Event]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + user_uuid: str, + *, + client: AuthenticatedClient, + body: Event, +) -> Response[Event]: + """ + Args: + user_uuid (str): + body (Event): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Event] + """ + + kwargs = _get_kwargs( + user_uuid=user_uuid, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + user_uuid: str, + *, + client: AuthenticatedClient, + body: Event, +) -> Event | None: + """ + Args: + user_uuid (str): + body (Event): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Event + """ + + return sync_detailed( + user_uuid=user_uuid, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + user_uuid: str, + *, + client: AuthenticatedClient, + body: Event, +) -> Response[Event]: + """ + Args: + user_uuid (str): + body (Event): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Event] + """ + + kwargs = _get_kwargs( + user_uuid=user_uuid, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + user_uuid: str, + *, + client: AuthenticatedClient, + body: Event, +) -> Event | None: + """ + Args: + user_uuid (str): + body (Event): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Event + """ + + return ( + await asyncio_detailed( + user_uuid=user_uuid, + client=client, + body=body, + ) + ).parsed diff --git a/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_plan_list.py b/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_plan_list.py new file mode 100644 index 0000000..6010aed --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_plan_list.py @@ -0,0 +1,185 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.paginated_promotion_list import PaginatedPromotionList +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + user_uuid: str, + *, + limit: int | Unset = UNSET, + offset: int | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["limit"] = limit + + params["offset"] = offset + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/promotions/application/{user_uuid}/plan/".format( + user_uuid=quote(str(user_uuid), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> PaginatedPromotionList | None: + if response.status_code == 200: + response_200 = PaginatedPromotionList.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[PaginatedPromotionList]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + user_uuid: str, + *, + client: AuthenticatedClient, + limit: int | Unset = UNSET, + offset: int | Unset = UNSET, +) -> Response[PaginatedPromotionList]: + """ + Args: + user_uuid (str): + limit (int | Unset): + offset (int | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[PaginatedPromotionList] + """ + + kwargs = _get_kwargs( + user_uuid=user_uuid, + limit=limit, + offset=offset, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + user_uuid: str, + *, + client: AuthenticatedClient, + limit: int | Unset = UNSET, + offset: int | Unset = UNSET, +) -> PaginatedPromotionList | None: + """ + Args: + user_uuid (str): + limit (int | Unset): + offset (int | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + PaginatedPromotionList + """ + + return sync_detailed( + user_uuid=user_uuid, + client=client, + limit=limit, + offset=offset, + ).parsed + + +async def asyncio_detailed( + user_uuid: str, + *, + client: AuthenticatedClient, + limit: int | Unset = UNSET, + offset: int | Unset = UNSET, +) -> Response[PaginatedPromotionList]: + """ + Args: + user_uuid (str): + limit (int | Unset): + offset (int | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[PaginatedPromotionList] + """ + + kwargs = _get_kwargs( + user_uuid=user_uuid, + limit=limit, + offset=offset, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + user_uuid: str, + *, + client: AuthenticatedClient, + limit: int | Unset = UNSET, + offset: int | Unset = UNSET, +) -> PaginatedPromotionList | None: + """ + Args: + user_uuid (str): + limit (int | Unset): + offset (int | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + PaginatedPromotionList + """ + + return ( + await asyncio_detailed( + user_uuid=user_uuid, + client=client, + limit=limit, + offset=offset, + ) + ).parsed diff --git a/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_plan_promote_create.py b/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_plan_promote_create.py new file mode 100644 index 0000000..31d2731 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_plan_promote_create.py @@ -0,0 +1,181 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.promote import Promote +from ...types import Response + + +def _get_kwargs( + user_uuid: str, + id: str, + *, + body: Promote, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/api/v2/promotions/application/{user_uuid}/plan/{id}/promote/".format( + user_uuid=quote(str(user_uuid), safe=""), + id=quote(str(id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Promote | None: + if response.status_code == 200: + response_200 = Promote.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Promote]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + user_uuid: str, + id: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Response[Promote]: + """ + Args: + user_uuid (str): + id (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Promote] + """ + + kwargs = _get_kwargs( + user_uuid=user_uuid, + id=id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + user_uuid: str, + id: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Promote | None: + """ + Args: + user_uuid (str): + id (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Promote + """ + + return sync_detailed( + user_uuid=user_uuid, + id=id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + user_uuid: str, + id: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Response[Promote]: + """ + Args: + user_uuid (str): + id (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Promote] + """ + + kwargs = _get_kwargs( + user_uuid=user_uuid, + id=id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + user_uuid: str, + id: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Promote | None: + """ + Args: + user_uuid (str): + id (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Promote + """ + + return ( + await asyncio_detailed( + user_uuid=user_uuid, + id=id, + client=client, + body=body, + ) + ).parsed diff --git a/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_plan_retrieve.py b/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_plan_retrieve.py new file mode 100644 index 0000000..8d34f82 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/api/api_v2_promotions_application_plan_retrieve.py @@ -0,0 +1,161 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.promotion import Promotion +from ...types import Response + + +def _get_kwargs( + user_uuid: str, + id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/api/v2/promotions/application/{user_uuid}/plan/{id}/".format( + user_uuid=quote(str(user_uuid), safe=""), + id=quote(str(id), safe=""), + ), + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Promotion | None: + if response.status_code == 200: + response_200 = Promotion.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Promotion]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + user_uuid: str, + id: str, + *, + client: AuthenticatedClient, +) -> Response[Promotion]: + """ + Args: + user_uuid (str): + id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Promotion] + """ + + kwargs = _get_kwargs( + user_uuid=user_uuid, + id=id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + user_uuid: str, + id: str, + *, + client: AuthenticatedClient, +) -> Promotion | None: + """ + Args: + user_uuid (str): + id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Promotion + """ + + return sync_detailed( + user_uuid=user_uuid, + id=id, + client=client, + ).parsed + + +async def asyncio_detailed( + user_uuid: str, + id: str, + *, + client: AuthenticatedClient, +) -> Response[Promotion]: + """ + Args: + user_uuid (str): + id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Promotion] + """ + + kwargs = _get_kwargs( + user_uuid=user_uuid, + id=id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + user_uuid: str, + id: str, + *, + client: AuthenticatedClient, +) -> Promotion | None: + """ + Args: + user_uuid (str): + id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Promotion + """ + + return ( + await asyncio_detailed( + user_uuid=user_uuid, + id=id, + client=client, + ) + ).parsed diff --git a/utils/clients/gooyal_promotions_client/api/promotions/__init__.py b/utils/clients/gooyal_promotions_client/api/promotions/__init__.py new file mode 100644 index 0000000..2d7c0b2 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/promotions/__init__.py @@ -0,0 +1 @@ +"""Contains endpoint functions for accessing the API""" diff --git a/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v1_events_submit_create.py b/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v1_events_submit_create.py new file mode 100644 index 0000000..1d5e168 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v1_events_submit_create.py @@ -0,0 +1,151 @@ +from http import HTTPStatus +from typing import Any + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.promote import Promote +from ...types import Response + + +def _get_kwargs( + *, + body: Promote, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/promotions/api/v1/events/submit", + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Promote | None: + if response.status_code == 201: + response_201 = Promote.from_dict(response.json()) + + return response_201 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Promote]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient, + body: Promote, +) -> Response[Promote]: + """ + Args: + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Promote] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + *, + client: AuthenticatedClient, + body: Promote, +) -> Promote | None: + """ + Args: + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Promote + """ + + return sync_detailed( + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + *, + client: AuthenticatedClient, + body: Promote, +) -> Response[Promote]: + """ + Args: + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Promote] + """ + + kwargs = _get_kwargs( + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + *, + client: AuthenticatedClient, + body: Promote, +) -> Promote | None: + """ + Args: + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Promote + """ + + return ( + await asyncio_detailed( + client=client, + body=body, + ) + ).parsed diff --git a/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v1_plans_create.py b/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v1_plans_create.py new file mode 100644 index 0000000..86047c4 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v1_plans_create.py @@ -0,0 +1,167 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.promote import Promote +from ...types import Response + + +def _get_kwargs( + plan: str, + *, + body: Promote, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/promotions/api/v1/plans/{plan}/".format( + plan=quote(str(plan), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Promote | None: + if response.status_code == 201: + response_201 = Promote.from_dict(response.json()) + + return response_201 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Promote]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + plan: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Response[Promote]: + """ + Args: + plan (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Promote] + """ + + kwargs = _get_kwargs( + plan=plan, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + plan: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Promote | None: + """ + Args: + plan (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Promote + """ + + return sync_detailed( + plan=plan, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + plan: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Response[Promote]: + """ + Args: + plan (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Promote] + """ + + kwargs = _get_kwargs( + plan=plan, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + plan: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Promote | None: + """ + Args: + plan (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Promote + """ + + return ( + await asyncio_detailed( + plan=plan, + client=client, + body=body, + ) + ).parsed diff --git a/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v1_plans_promotins_list.py b/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v1_plans_promotins_list.py new file mode 100644 index 0000000..8bd42eb --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v1_plans_promotins_list.py @@ -0,0 +1,185 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.paginated_promotion_list import PaginatedPromotionList +from ...types import UNSET, Response, Unset + + +def _get_kwargs( + plan: str, + *, + limit: int | Unset = UNSET, + offset: int | Unset = UNSET, +) -> dict[str, Any]: + + params: dict[str, Any] = {} + + params["limit"] = limit + + params["offset"] = offset + + params = {k: v for k, v in params.items() if v is not UNSET and v is not None} + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/promotions/api/v1/plans/{plan}/promotins".format( + plan=quote(str(plan), safe=""), + ), + "params": params, + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> PaginatedPromotionList | None: + if response.status_code == 200: + response_200 = PaginatedPromotionList.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response( + *, client: AuthenticatedClient | Client, response: httpx.Response +) -> Response[PaginatedPromotionList]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + plan: str, + *, + client: AuthenticatedClient, + limit: int | Unset = UNSET, + offset: int | Unset = UNSET, +) -> Response[PaginatedPromotionList]: + """ + Args: + plan (str): + limit (int | Unset): + offset (int | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[PaginatedPromotionList] + """ + + kwargs = _get_kwargs( + plan=plan, + limit=limit, + offset=offset, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + plan: str, + *, + client: AuthenticatedClient, + limit: int | Unset = UNSET, + offset: int | Unset = UNSET, +) -> PaginatedPromotionList | None: + """ + Args: + plan (str): + limit (int | Unset): + offset (int | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + PaginatedPromotionList + """ + + return sync_detailed( + plan=plan, + client=client, + limit=limit, + offset=offset, + ).parsed + + +async def asyncio_detailed( + plan: str, + *, + client: AuthenticatedClient, + limit: int | Unset = UNSET, + offset: int | Unset = UNSET, +) -> Response[PaginatedPromotionList]: + """ + Args: + plan (str): + limit (int | Unset): + offset (int | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[PaginatedPromotionList] + """ + + kwargs = _get_kwargs( + plan=plan, + limit=limit, + offset=offset, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + plan: str, + *, + client: AuthenticatedClient, + limit: int | Unset = UNSET, + offset: int | Unset = UNSET, +) -> PaginatedPromotionList | None: + """ + Args: + plan (str): + limit (int | Unset): + offset (int | Unset): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + PaginatedPromotionList + """ + + return ( + await asyncio_detailed( + plan=plan, + client=client, + limit=limit, + offset=offset, + ) + ).parsed diff --git a/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v2_plans_promote_create.py b/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v2_plans_promote_create.py new file mode 100644 index 0000000..3971e75 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v2_plans_promote_create.py @@ -0,0 +1,167 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.promote import Promote +from ...types import Response + + +def _get_kwargs( + id: str, + *, + body: Promote, +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/promotions/api/v2/plans/{id}/promote/".format( + id=quote(str(id), safe=""), + ), + } + + _kwargs["json"] = body.to_dict() + + headers["Content-Type"] = "application/json" + + _kwargs["headers"] = headers + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Promote | None: + if response.status_code == 200: + response_200 = Promote.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[Promote]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + id: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Response[Promote]: + """ + Args: + id (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Promote] + """ + + kwargs = _get_kwargs( + id=id, + body=body, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + id: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Promote | None: + """ + Args: + id (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Promote + """ + + return sync_detailed( + id=id, + client=client, + body=body, + ).parsed + + +async def asyncio_detailed( + id: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Response[Promote]: + """ + Args: + id (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Promote] + """ + + kwargs = _get_kwargs( + id=id, + body=body, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + id: str, + *, + client: AuthenticatedClient, + body: Promote, +) -> Promote | None: + """ + Args: + id (str): + body (Promote): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Promote + """ + + return ( + await asyncio_detailed( + id=id, + client=client, + body=body, + ) + ).parsed diff --git a/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v2_plans_retrieve.py b/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v2_plans_retrieve.py new file mode 100644 index 0000000..8bb0ee1 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/api/promotions/promotions_api_v2_plans_retrieve.py @@ -0,0 +1,147 @@ +from http import HTTPStatus +from typing import Any +from urllib.parse import quote + +import httpx + +from ... import errors +from ...client import AuthenticatedClient, Client +from ...models.user_plan import UserPlan +from ...types import Response + + +def _get_kwargs( + id: str, +) -> dict[str, Any]: + + _kwargs: dict[str, Any] = { + "method": "get", + "url": "/promotions/api/v2/plans/{id}/".format( + id=quote(str(id), safe=""), + ), + } + + return _kwargs + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> UserPlan | None: + if response.status_code == 200: + response_200 = UserPlan.from_dict(response.json()) + + return response_200 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[UserPlan]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + id: str, + *, + client: AuthenticatedClient, +) -> Response[UserPlan]: + """ + Args: + id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[UserPlan] + """ + + kwargs = _get_kwargs( + id=id, + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + + +def sync( + id: str, + *, + client: AuthenticatedClient, +) -> UserPlan | None: + """ + Args: + id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + UserPlan + """ + + return sync_detailed( + id=id, + client=client, + ).parsed + + +async def asyncio_detailed( + id: str, + *, + client: AuthenticatedClient, +) -> Response[UserPlan]: + """ + Args: + id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[UserPlan] + """ + + kwargs = _get_kwargs( + id=id, + ) + + response = await client.get_async_httpx_client().request(**kwargs) + + return _build_response(client=client, response=response) + + +async def asyncio( + id: str, + *, + client: AuthenticatedClient, +) -> UserPlan | None: + """ + Args: + id (str): + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + UserPlan + """ + + return ( + await asyncio_detailed( + id=id, + client=client, + ) + ).parsed diff --git a/utils/clients/gooyal_promotions_client/client.py b/utils/clients/gooyal_promotions_client/client.py new file mode 100644 index 0000000..1b7055a --- /dev/null +++ b/utils/clients/gooyal_promotions_client/client.py @@ -0,0 +1,268 @@ +import ssl +from typing import Any + +import httpx +from attrs import define, evolve, field + + +@define +class Client: + """A class for keeping track of data related to the API + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + def with_headers(self, headers: dict[str, str]) -> "Client": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "Client": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "Client": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "Client": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "Client": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "Client": + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "Client": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) + + +@define +class AuthenticatedClient: + """A Client which has been authenticated for use on secured endpoints + + The following are accepted as keyword arguments and will be used to construct httpx Clients internally: + + ``base_url``: The base URL for the API, all requests are made to a relative path to this URL + + ``cookies``: A dictionary of cookies to be sent with every request + + ``headers``: A dictionary of headers to be sent with every request + + ``timeout``: The maximum amount of a time a request can take. API functions will raise + httpx.TimeoutException if this is exceeded. + + ``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production, + but can be set to False for testing purposes. + + ``follow_redirects``: Whether or not to follow redirects. Default value is False. + + ``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor. + + + Attributes: + raise_on_unexpected_status: Whether or not to raise an errors.UnexpectedStatus if the API returns a + status code that was not documented in the source OpenAPI document. Can also be provided as a keyword + argument to the constructor. + token: The token to use for authentication + prefix: The prefix to use for the Authorization header + auth_header_name: The name of the Authorization header + """ + + raise_on_unexpected_status: bool = field(default=False, kw_only=True) + _base_url: str = field(alias="base_url") + _cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies") + _headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers") + _timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout") + _verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl") + _follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects") + _httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args") + _client: httpx.Client | None = field(default=None, init=False) + _async_client: httpx.AsyncClient | None = field(default=None, init=False) + + token: str + prefix: str = "Bearer" + auth_header_name: str = "Authorization" + + def with_headers(self, headers: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional headers""" + if self._client is not None: + self._client.headers.update(headers) + if self._async_client is not None: + self._async_client.headers.update(headers) + return evolve(self, headers={**self._headers, **headers}) + + def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient": + """Get a new client matching this one with additional cookies""" + if self._client is not None: + self._client.cookies.update(cookies) + if self._async_client is not None: + self._async_client.cookies.update(cookies) + return evolve(self, cookies={**self._cookies, **cookies}) + + def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient": + """Get a new client matching this one with a new timeout configuration""" + if self._client is not None: + self._client.timeout = timeout + if self._async_client is not None: + self._async_client.timeout = timeout + return evolve(self, timeout=timeout) + + def set_httpx_client(self, client: httpx.Client) -> "AuthenticatedClient": + """Manually set the underlying httpx.Client + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._client = client + return self + + def get_httpx_client(self) -> httpx.Client: + """Get the underlying httpx.Client, constructing a new one if not previously set""" + if self._client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._client = httpx.Client( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._client + + def __enter__(self) -> "AuthenticatedClient": + """Enter a context manager for self.client—you cannot enter twice (see httpx docs)""" + self.get_httpx_client().__enter__() + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for internal httpx.Client (see httpx docs)""" + self.get_httpx_client().__exit__(*args, **kwargs) + + def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> "AuthenticatedClient": + """Manually set the underlying httpx.AsyncClient + + **NOTE**: This will override any other settings on the client, including cookies, headers, and timeout. + """ + self._async_client = async_client + return self + + def get_async_httpx_client(self) -> httpx.AsyncClient: + """Get the underlying httpx.AsyncClient, constructing a new one if not previously set""" + if self._async_client is None: + self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token + self._async_client = httpx.AsyncClient( + base_url=self._base_url, + cookies=self._cookies, + headers=self._headers, + timeout=self._timeout, + verify=self._verify_ssl, + follow_redirects=self._follow_redirects, + **self._httpx_args, + ) + return self._async_client + + async def __aenter__(self) -> "AuthenticatedClient": + """Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)""" + await self.get_async_httpx_client().__aenter__() + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + """Exit a context manager for underlying httpx.AsyncClient (see httpx docs)""" + await self.get_async_httpx_client().__aexit__(*args, **kwargs) diff --git a/utils/clients/gooyal_promotions_client/errors.py b/utils/clients/gooyal_promotions_client/errors.py new file mode 100644 index 0000000..5f92e76 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/errors.py @@ -0,0 +1,16 @@ +"""Contains shared errors types that can be raised from API functions""" + + +class UnexpectedStatus(Exception): + """Raised by api functions when the response status an undocumented status and Client.raise_on_unexpected_status is True""" + + def __init__(self, status_code: int, content: bytes): + self.status_code = status_code + self.content = content + + super().__init__( + f"Unexpected status code: {status_code}\n\nResponse content:\n{content.decode(errors='ignore')}" + ) + + +__all__ = ["UnexpectedStatus"] diff --git a/utils/clients/gooyal_promotions_client/models/__init__.py b/utils/clients/gooyal_promotions_client/models/__init__.py new file mode 100644 index 0000000..a94b32c --- /dev/null +++ b/utils/clients/gooyal_promotions_client/models/__init__.py @@ -0,0 +1,19 @@ +"""Contains all the data models used in inputs/outputs""" + +from .event import Event +from .paginated_promotion_list import PaginatedPromotionList +from .promote import Promote +from .promotion import Promotion +from .state_enum import StateEnum +from .user_plan import UserPlan +from .user_recipient import UserRecipient + +__all__ = ( + "Event", + "PaginatedPromotionList", + "Promote", + "Promotion", + "StateEnum", + "UserPlan", + "UserRecipient", +) diff --git a/utils/clients/gooyal_promotions_client/models/event.py b/utils/clients/gooyal_promotions_client/models/event.py new file mode 100644 index 0000000..27e671f --- /dev/null +++ b/utils/clients/gooyal_promotions_client/models/event.py @@ -0,0 +1,120 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, cast +from uuid import UUID + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="Event") + + +@_attrs_define +class Event: + """ + Attributes: + label (str): + data (Any): + user (None | UUID): + application (None | UUID): + """ + + label: str + data: Any + user: None | UUID + application: None | UUID + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + label = self.label + + data = self.data + + user: None | str + if isinstance(self.user, UUID): + user = str(self.user) + else: + user = self.user + + application: None | str + if isinstance(self.application, UUID): + application = str(self.application) + else: + application = self.application + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "label": label, + "data": data, + "user": user, + "application": application, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + label = d.pop("label") + + data = d.pop("data") + + def _parse_user(data: object) -> None | UUID: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + user_type_0 = UUID(data) + + return user_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | UUID, data) + + user = _parse_user(d.pop("user")) + + def _parse_application(data: object) -> None | UUID: + if data is None: + return data + try: + if not isinstance(data, str): + raise TypeError() + application_type_0 = UUID(data) + + return application_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | UUID, data) + + application = _parse_application(d.pop("application")) + + event = cls( + label=label, + data=data, + user=user, + application=application, + ) + + event.additional_properties = d + return event + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/utils/clients/gooyal_promotions_client/models/paginated_promotion_list.py b/utils/clients/gooyal_promotions_client/models/paginated_promotion_list.py new file mode 100644 index 0000000..a8e6606 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/models/paginated_promotion_list.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.promotion import Promotion + + +T = TypeVar("T", bound="PaginatedPromotionList") + + +@_attrs_define +class PaginatedPromotionList: + """ + Attributes: + count (int): Example: 123. + results (list[Promotion]): + next_ (None | str | Unset): Example: http://api.example.org/accounts/?offset=400&limit=100. + previous (None | str | Unset): Example: http://api.example.org/accounts/?offset=200&limit=100. + """ + + count: int + results: list[Promotion] + next_: None | str | Unset = UNSET + previous: None | str | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + count = self.count + + results = [] + for results_item_data in self.results: + results_item = results_item_data.to_dict() + results.append(results_item) + + next_: None | str | Unset + if isinstance(self.next_, Unset): + next_ = UNSET + else: + next_ = self.next_ + + previous: None | str | Unset + if isinstance(self.previous, Unset): + previous = UNSET + else: + previous = self.previous + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "count": count, + "results": results, + } + ) + if next_ is not UNSET: + field_dict["next"] = next_ + if previous is not UNSET: + field_dict["previous"] = previous + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.promotion import Promotion + + d = dict(src_dict) + count = d.pop("count") + + results = [] + _results = d.pop("results") + for results_item_data in _results: + results_item = Promotion.from_dict(results_item_data) + + results.append(results_item) + + def _parse_next_(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + next_ = _parse_next_(d.pop("next", UNSET)) + + def _parse_previous(data: object) -> None | str | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(None | str | Unset, data) + + previous = _parse_previous(d.pop("previous", UNSET)) + + paginated_promotion_list = cls( + count=count, + results=results, + next_=next_, + previous=previous, + ) + + paginated_promotion_list.additional_properties = d + return paginated_promotion_list + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/utils/clients/gooyal_promotions_client/models/promote.py b/utils/clients/gooyal_promotions_client/models/promote.py new file mode 100644 index 0000000..c36fa8e --- /dev/null +++ b/utils/clients/gooyal_promotions_client/models/promote.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.promotion import Promotion + + +T = TypeVar("T", bound="Promote") + + +@_attrs_define +class Promote: + """ + Attributes: + promotions (list[Promotion]): + label (str): + data (Any): + """ + + promotions: list[Promotion] + label: str + data: Any + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + promotions = [] + for promotions_item_data in self.promotions: + promotions_item = promotions_item_data.to_dict() + promotions.append(promotions_item) + + label = self.label + + data = self.data + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "promotions": promotions, + "label": label, + "data": data, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.promotion import Promotion + + d = dict(src_dict) + promotions = [] + _promotions = d.pop("promotions") + for promotions_item_data in _promotions: + promotions_item = Promotion.from_dict(promotions_item_data) + + promotions.append(promotions_item) + + label = d.pop("label") + + data = d.pop("data") + + promote = cls( + promotions=promotions, + label=label, + data=data, + ) + + promote.additional_properties = d + return promote + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/utils/clients/gooyal_promotions_client/models/promotion.py b/utils/clients/gooyal_promotions_client/models/promotion.py new file mode 100644 index 0000000..270c595 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/models/promotion.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast +from uuid import UUID + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..models.state_enum import StateEnum +from ..types import UNSET, Unset + +if TYPE_CHECKING: + from ..models.event import Event + + +T = TypeVar("T", bound="Promotion") + + +@_attrs_define +class Promotion: + """ + Attributes: + uuid (UUID): + event (Event): + user (UUID): + promotion_amount (int | None | Unset): + state (StateEnum | Unset): * `1` - created + * `2` - delayed + * `3` - pending + * `4` - incomplete + * `5` - success + * `6` - failed + * `7` - expected_failure + """ + + uuid: UUID + event: Event + user: UUID + promotion_amount: int | None | Unset = UNSET + state: StateEnum | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + uuid = str(self.uuid) + + event = self.event.to_dict() + + user = str(self.user) + + promotion_amount: int | None | Unset + if isinstance(self.promotion_amount, Unset): + promotion_amount = UNSET + else: + promotion_amount = self.promotion_amount + + state: int | Unset = UNSET + if not isinstance(self.state, Unset): + state = self.state.value + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "uuid": uuid, + "event": event, + "user": user, + } + ) + if promotion_amount is not UNSET: + field_dict["promotion_amount"] = promotion_amount + if state is not UNSET: + field_dict["state"] = state + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.event import Event + + d = dict(src_dict) + uuid = UUID(d.pop("uuid")) + + event = Event.from_dict(d.pop("event")) + + user = UUID(d.pop("user")) + + def _parse_promotion_amount(data: object) -> int | None | Unset: + if data is None: + return data + if isinstance(data, Unset): + return data + return cast(int | None | Unset, data) + + promotion_amount = _parse_promotion_amount(d.pop("promotion_amount", UNSET)) + + _state = d.pop("state", UNSET) + state: StateEnum | Unset + if isinstance(_state, Unset): + state = UNSET + else: + state = StateEnum(_state) + + promotion = cls( + uuid=uuid, + event=event, + user=user, + promotion_amount=promotion_amount, + state=state, + ) + + promotion.additional_properties = d + return promotion + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/utils/clients/gooyal_promotions_client/models/state_enum.py b/utils/clients/gooyal_promotions_client/models/state_enum.py new file mode 100644 index 0000000..dbe21ab --- /dev/null +++ b/utils/clients/gooyal_promotions_client/models/state_enum.py @@ -0,0 +1,14 @@ +from enum import IntEnum + + +class StateEnum(IntEnum): + VALUE_1 = 1 + VALUE_2 = 2 + VALUE_3 = 3 + VALUE_4 = 4 + VALUE_5 = 5 + VALUE_6 = 6 + VALUE_7 = 7 + + def __str__(self) -> str: + return str(self.value) diff --git a/utils/clients/gooyal_promotions_client/models/user_plan.py b/utils/clients/gooyal_promotions_client/models/user_plan.py new file mode 100644 index 0000000..1d9a1d2 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/models/user_plan.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, TypeVar, cast + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +if TYPE_CHECKING: + from ..models.user_recipient import UserRecipient + + +T = TypeVar("T", bound="UserPlan") + + +@_attrs_define +class UserPlan: + """ + Attributes: + title (str): + banner (None | str): + description (str): + description_details (Any): + recipients (list[UserRecipient]): + """ + + title: str + banner: None | str + description: str + description_details: Any + recipients: list[UserRecipient] + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + title = self.title + + banner: None | str + banner = self.banner + + description = self.description + + description_details = self.description_details + + recipients = [] + for recipients_item_data in self.recipients: + recipients_item = recipients_item_data.to_dict() + recipients.append(recipients_item) + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "title": title, + "banner": banner, + "description": description, + "description_details": description_details, + "recipients": recipients, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + from ..models.user_recipient import UserRecipient + + d = dict(src_dict) + title = d.pop("title") + + def _parse_banner(data: object) -> None | str: + if data is None: + return data + return cast(None | str, data) + + banner = _parse_banner(d.pop("banner")) + + description = d.pop("description") + + description_details = d.pop("description_details") + + recipients = [] + _recipients = d.pop("recipients") + for recipients_item_data in _recipients: + recipients_item = UserRecipient.from_dict(recipients_item_data) + + recipients.append(recipients_item) + + user_plan = cls( + title=title, + banner=banner, + description=description, + description_details=description_details, + recipients=recipients, + ) + + user_plan.additional_properties = d + return user_plan + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/utils/clients/gooyal_promotions_client/models/user_recipient.py b/utils/clients/gooyal_promotions_client/models/user_recipient.py new file mode 100644 index 0000000..e43f137 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/models/user_recipient.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +T = TypeVar("T", bound="UserRecipient") + + +@_attrs_define +class UserRecipient: + """ + Attributes: + label (str): + base_amount_field (str): + """ + + label: str + base_amount_field: str + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + def to_dict(self) -> dict[str, Any]: + label = self.label + + base_amount_field = self.base_amount_field + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update( + { + "label": label, + "base_amount_field": base_amount_field, + } + ) + + return field_dict + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + label = d.pop("label") + + base_amount_field = d.pop("base_amount_field") + + user_recipient = cls( + label=label, + base_amount_field=base_amount_field, + ) + + user_recipient.additional_properties = d + return user_recipient + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/utils/clients/gooyal_promotions_client/py.typed b/utils/clients/gooyal_promotions_client/py.typed new file mode 100644 index 0000000..1aad327 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561 \ No newline at end of file diff --git a/utils/clients/gooyal_promotions_client/types.py b/utils/clients/gooyal_promotions_client/types.py new file mode 100644 index 0000000..b64af09 --- /dev/null +++ b/utils/clients/gooyal_promotions_client/types.py @@ -0,0 +1,54 @@ +"""Contains some shared types for properties""" + +from collections.abc import Mapping, MutableMapping +from http import HTTPStatus +from typing import IO, BinaryIO, Generic, Literal, TypeVar + +from attrs import define + + +class Unset: + def __bool__(self) -> Literal[False]: + return False + + +UNSET: Unset = Unset() + +# The types that `httpx.Client(files=)` can accept, copied from that library. +FileContent = IO[bytes] | bytes | str +FileTypes = ( + # (filename, file (or bytes), content_type) + tuple[str | None, FileContent, str | None] + # (filename, file (or bytes), content_type, headers) + | tuple[str | None, FileContent, str | None, Mapping[str, str]] +) +RequestFiles = list[tuple[str, FileTypes]] + + +@define +class File: + """Contains information for file uploads""" + + payload: BinaryIO + file_name: str | None = None + mime_type: str | None = None + + def to_tuple(self) -> FileTypes: + """Return a tuple representation that httpx will accept for multipart/form-data""" + return self.file_name, self.payload, self.mime_type + + +T = TypeVar("T") + + +@define +class Response(Generic[T]): + """A response from an endpoint""" + + status_code: HTTPStatus + content: bytes + headers: MutableMapping[str, str] + parsed: T | None + + +__all__ = ["UNSET", "File", "FileTypes", "RequestFiles", "Response", "Unset"] diff --git a/utils/clients/promotions_client.py b/utils/clients/promotions_client.py new file mode 100644 index 0000000..ed06ac6 --- /dev/null +++ b/utils/clients/promotions_client.py @@ -0,0 +1,88 @@ +import logging + +import requests +import time +from django.conf import settings +from django.core.cache import cache +from django.db import models +from httpx import Request + +from utils.clients.gooyal_promotions_client import AuthenticatedClient +from utils.clients.gooyal_promotions_client.api.api import ( + api_v2_promotions_application_plan_promote_create +) + +logger = logging.getLogger(__name__) + + +def login_as_client_credentials(): + access_token_cache_key = 'accounts_access_token' + access_token = cache.get(access_token_cache_key) + # expires_in = self.auth_data.get('expires_in') + # created_at = self.auth_data.get('created_at') + # + # if access_token and expires_in > time.time() - created_at: + # return self.auth_data + if access_token: + logger.debug('token available in cache') + return access_token + + logger.debug('no token available in cache') + + data = { + "grant_type": 'client_credentials', + "scope": settings.OAUTH2_PROVIDER_SCOPES, + } + auth = (settings.OAUTH2_PROVIDER_CLIENT_ID, settings.OAUTH2_PROVIDER_CLIENT_SECRET) + + response = requests.post(f'{settings.OAUTH2_PROVIDER_BASE_PUBLIC_URL}/token/', + data=data, + auth=auth) + logger.debug(response.content) + + auth_data = response.json() + if 'access_token' in auth_data: + auth_data['created_at'] = time.time() + cache.set(access_token_cache_key, auth_data, timeout=auth_data.get('expires_in') - 5) + + return auth_data + + +def log_request(request: Request): + logger.debug(f'authorization: {request.headers["authorization"]}') + logger.debug( + f"Request event hook: {request.method} {request.url} {request.method} {request.headers} - Waiting for response") + + +def log_response(response): + request = response.request + logger.debug(f"Response event hook: {request.method} {request.url} - Status {response.status_code}") + # logger.debug(f"Response event hook: {response.read()}") + + +def get_promotions_client(): + access_token = login_as_client_credentials()['access_token'] + client = AuthenticatedClient(base_url=settings.PROMOTIONS_BASE_PUBLIC_URL, token=access_token, + httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}}, + ) + return client + + +def promote_user(user, plan, label, data): + class Tmp(): + def to_dict(self): + return { + "data": data, + "label": label + } + + data = Tmp() + client = get_promotions_client() + response = api_v2_promotions_application_plan_promote_create.sync( + user_uuid=user, + id=plan, + client=client, + body=data + ) + + return response