"""This is the class that do the requests against the API.""" from __future__ import annotations import asyncio from typing import Any, Dict import aiohttp from .const import ( GitHubClientKwarg, GitHubRequestKwarg, HttpContentType, HttpMethod, HttpStatusCode, ) from .exceptions import ( GitHubAuthenticationException, GitHubConnectionException, GitHubException, GitHubGraphQLException, GitHubNotFoundException, GitHubNotModifiedException, GitHubPayloadException, GitHubPermissionException, GitHubRatelimitException, ) from .legacy.client import AIOGitHubAPIClient as LegacyAIOGitHubAPIClient from .models.base import GitHubBase from .models.request_data import GitHubBaseRequestDataModel from .models.response import GitHubResponseModel STATUS_EXCEPTIONS: Dict[HttpStatusCode, GitHubException] = { HttpStatusCode.FORBIDDEN: GitHubAuthenticationException, HttpStatusCode.UNAUTHORIZED: GitHubAuthenticationException, HttpStatusCode.NOT_MODIFIED: GitHubNotModifiedException, HttpStatusCode.NOT_FOUND: GitHubNotFoundException, HttpStatusCode.BAD_REQUEST: GitHubPayloadException, HttpStatusCode.UNPROCESSABLE_ENTITY: GitHubException, } MESSAGE_EXCEPTIONS: Dict[str, GitHubException] = { "Bad credentials": GitHubAuthenticationException, "You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.": GitHubRatelimitException, "Must have push access to repository": GitHubPermissionException, } class AIOGitHubAPIClient(LegacyAIOGitHubAPIClient): """Dummy class to not break existing code.""" class GitHubClient(GitHubBase): """ Client to handle API calls. Don't use this directly, use `aiogithubapi.github.GitHubApi` to get the client. """ def __init__( self, session: aiohttp.ClientSession, token: str | None = None, *, api_version: str | None = None, **kwargs: Dict[GitHubClientKwarg, Any], ) -> None: """ Initialise the GitHub API client. **Arguments:** `session`: The aiohttp client session to use for making requests. `token`: The GitHub access token to use for authenticating requests. Can be a string or None. `api_version`: The version of the GitHub API to use. Can be a string or None. """ self._base_request_data = GitHubBaseRequestDataModel( token=token, api_version=api_version, kwargs=kwargs, ) self._session = session self._loop = asyncio.get_running_loop() async def async_call_api( self, endpoint: str, *, data: Dict[str, Any] | str | None = None, headers: Dict[str, Any] | None = None, method: HttpMethod = HttpMethod.GET, params: Dict[str, Any] | None = None, timeout: int | None = None, **kwargs: Dict[GitHubRequestKwarg, Any], ) -> GitHubResponseModel: """ Makes an HTTP request to the specified endpoint using the specified parameters. This method is asynchronous, meaning that it will not block the execution of the program while the request is being made and processed. **Arguments**: - `endpoint` (Required): The API endpoint to call. **Optional arguments**: - `endpoint`: The API endpoint to call. - `data`: The data to include in the request body. Can be a dictionary, a string, or None. - `headers`: The headers to include in the request. Can be a dictionary or None. - `method`: The HTTP method to use for the request. Defaults to GET. - `params`: The query parameters to include in the request. Can be a dictionary or None. - `timeout`: The maximum amount of time to wait for the request to complete, in seconds. Can be an integer or None. Returns: A GitHubResponseModel object representing the API response. """ request_arguments: Dict[str, Any] = { "url": self._base_request_data.request_url(endpoint), "method": kwargs.get(GitHubRequestKwarg.METHOD, method).lower(), "params": params or kwargs.get(GitHubRequestKwarg.PARAMS, kwargs.get(GitHubRequestKwarg.QUERY, {})), "timeout": timeout or self._base_request_data.timeout, "headers": { **(headers or {}), **self._base_request_data.headers, **(headers or {}), **kwargs.get("headers", {}), }, } if etag := kwargs.get(GitHubRequestKwarg.ETAG): request_arguments["headers"][aiohttp.hdrs.IF_NONE_MATCH] = etag if isinstance(data, dict): request_arguments["json"] = data else: request_arguments["data"] = data try: result = await self._session.request(**request_arguments) except (aiohttp.ClientError, asyncio.CancelledError) as exception: raise GitHubConnectionException( "Request exception for " f"'{self._base_request_data.request_url(endpoint)}' with - {exception}" ) from exception except asyncio.TimeoutError: raise GitHubConnectionException( f"Timeout of {self._base_request_data.timeout} reached while " f"waiting for {self._base_request_data.request_url(endpoint)}" ) from None except BaseException as exception: raise GitHubException( "Unexpected exception for " f"'{self._base_request_data.request_url(endpoint)}' with - {exception}" ) from exception response = GitHubResponseModel(result) if response.status == HttpStatusCode.NO_CONTENT: return response try: if HttpContentType.BASE_JSON in (response.headers.content_type or ""): response.data = await result.json(encoding="utf-8") elif (response.headers.content_type or "") in ( HttpContentType.BASE_ZIP, HttpContentType.BASE_GZIP, ): response.data = await result.read() else: response.data = await result.text(encoding="utf-8") except BaseException as exception: raise GitHubException( f"Could not handle response data from '{self._base_request_data.request_url(endpoint)}' with - {exception}" ) message = response.data.get("message") if isinstance(response.data, dict) else None if message is not None and "rate limit" in message: raise GitHubRatelimitException(message) if exception := STATUS_EXCEPTIONS.get(response.status): raise exception(message or response.data) if isinstance(response.data, dict): if message is not None: if exception := MESSAGE_EXCEPTIONS.get(message): raise exception(message) # For a 201 Created response, we assume the operation was successful and ignore any "message" field. if response.status != HttpStatusCode.CREATED: raise GitHubException(message) if endpoint == "/graphql" and response.data.get("errors", []): raise GitHubGraphQLException( ", ".join(entry.get("message") for entry in response.data["errors"]) ) return response