Source code for megu.models.http

# -*- encoding: utf-8 -*-
# Copyright (c) 2021 Stephen Bunn <stephen@bunn.io>
# GPLv3 License <https://choosealicense.com/licenses/gpl-3.0/>

"""Contains definitions of HTTP resource types used throughout the project."""

from __future__ import annotations

from enum import Enum
from io import BytesIO
from typing import Callable, Optional

from cached_property import cached_property
from pydantic import Field
from requests import PreparedRequest
from requests.sessions import Request

from ..hasher import HashType, hash_io
from ..log import instance as log
from .content import Resource
from .types import Url


[docs]class HttpMethod(Enum): """Enumeration of the available HTTP methods that resources can use.""" GET = "GET" HEAD = "HEAD" POST = "POST" PUT = "PUT" DELETE = "DELETE" CONNECT = "CONNECT" OPTIONS = "OPTIONS" TRACE = "TRACE" PATCH = "PATCH"
[docs]class HttpResource(Resource): """Describes a downloadable HTTP resource that is part of some local content. Parameters: method (~megu.models.http.HttpMethod): The HTTP method that should be used to fetch this resource. url (str): The URL that should be used to fetch this resource. This URL string gets translated into a :class:`~megu.models.types.Url` instance. headers (dict): The dictionary of headers to use to fetch this resource (if any). data (Optional[bytes], optional): The data body to send in the resource request (if any). auth: (Optional[Callable[[~requests.Request], ~requests.Request]], optional): A callable that mutates a request to ensure it is authenticated for fetching the resource. """
[docs] class Config: """Model configuration for the Resource model.""" arbitrary_types_allowed = True keep_untouched = (cached_property,)
method: HttpMethod = Field( title="Method", description="HTTP Method to fetch the resource URL.", ) url: Url = Field( title="URL", description="The resource URL to fetch.", ) headers: dict = Field( default_factory=dict, title="Headers", description="The headers to fetch the resource URL.", ) data: Optional[bytes] = Field( default=None, title="Data", description="The request data to fetch the resource URL.", ) auth: Optional[Callable[[Request], Request]] = Field( default=None, title="Authentication Handler", description="Callable to handle authenticating a request.", ) def _get_signature(self) -> bytes: """Get the unique signature of the request data. Returns: bytes: A byte array containing a unique signature for the current resource. """ signature = bytes( "|".join((str(self.method.value), str(self.url), str(self.headers))), "utf-8", ) if self.data is not None: signature += b"|" + self.data return signature @cached_property def fingerprint(self) -> str: """Get a computed unique identifier for the resource. Returns: str: The unique identifier for the resource. """ fingerprint = hash_io( BytesIO(self._get_signature()), {HashType.MD5}, )[HashType.MD5] log.debug(f"Computed fingerprint {fingerprint!r} for resource {self!r}") return fingerprint
[docs] @classmethod def from_request(cls, request: PreparedRequest) -> HttpResource: """Produce an resource from an existing prepared request. Args: request (~requests.PreparedRequest): The request to construct an resource from. Returns: ~megu.models.http.HttpResource: The newly produced resource. """ return HttpResource( method=HttpMethod(request.method or HttpMethod.GET.value), url=request.url, headers=request.headers, data=request.body, )
[docs] def to_request(self) -> PreparedRequest: """Get a matching prepared request for the current resource. Returns: ~requests.PreparedRequest: The matching prepared request for the current resource. """ return Request( method=self.method.value, url=str(self.url), headers=self.headers, data=self.data, ).prepare()