Skip to content

Commit

Permalink
[requirements] removing requests
Browse files Browse the repository at this point in the history
  • Loading branch information
david-lev committed Jul 26, 2024
1 parent 5c2050d commit e831c30
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 47 deletions.
1 change: 1 addition & 0 deletions docs/source/content/handlers/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ To enable this feature, you need to pass the ``app_secret`` when initializing th
from pywa import WhatsApp
wa = WhatsApp(
validate_updates=True, # Default is True
app_secret='xxxx',
...
)
Expand Down
64 changes: 51 additions & 13 deletions pywa/api.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,48 @@
"""The internal API for the WhatsApp client."""

import typing
import importlib
import logging
from typing import Any, TYPE_CHECKING

import requests
import httpx

import pywa
from . import utils
from .errors import WhatsAppError

if TYPE_CHECKING:
from .client import WhatsApp


_logger = logging.getLogger(__name__)


class WhatsAppCloudApi:
"""Internal methods for the WhatsApp client. Do not use this class directly."""

def __init__(
self,
token: str,
session: requests.Session,
session: httpx.Client,
base_url: str,
api_version: float,
):
self._session = self._setup_session(session, token)
self._base_url = f"{base_url}/v{api_version}"

# backward compatibility with requests.Session
self._is_requests_session, _ = utils.is_requests_and_err(session)
if self._is_requests_session:
_logger.warning(
"Using `requests.Session` is deprecated and will be removed in future versions. "
"Please use `httpx.Client` instead."
)

@staticmethod
def _setup_session(session, token: str) -> requests.Session:
def _setup_session(session, token: str) -> httpx.Client:
if session.headers.get("Authorization") is not None:
raise ValueError(
"You can't use the same requests.Session for multiple WhatsApp instances!"
"You can't use the same httpx.Client for multiple WhatsApp instances!"
)
session.headers.update(
{
Expand Down Expand Up @@ -262,8 +275,20 @@ def upload_media(
return self._make_request(
method="POST",
endpoint=f"/{phone_id}/media",
files=[("file", (filename, media, mime_type))],
data={"messaging_product": "whatsapp"},
**(
dict(
files=[("file", (filename, media, mime_type))],
data={"messaging_product": "whatsapp"},
)
if self._is_requests_session
else dict(
files={
"file": (filename, media, mime_type),
"messaging_product": (None, "whatsapp"),
"type": (None, mime_type),
}
)
),
)

def get_media_url(self, media_id: str) -> dict:
Expand Down Expand Up @@ -808,12 +833,25 @@ def update_flow_json(self, flow_id: str, flow_json: str) -> dict:
return self._make_request(
method="POST",
endpoint=f"/{flow_id}/assets",
files=[("file", ("flow.json", flow_json, "application/json"))],
data={
"name": "flow.json",
"asset_type": "FLOW_JSON",
"messaging_product": "whatsapp",
},
**(
dict(
files=[("file", ("flow.json", flow_json, "application/json"))],
data={
"name": "flow.json",
"asset_type": "FLOW_JSON",
"messaging_product": "whatsapp",
},
)
if self._is_requests_session
else dict(
files={
"file": ("flow.json", flow_json, "application/json"),
"name": (None, "flow.json"),
"asset_type": (None, "FLOW_JSON"),
"messaging_product": (None, "whatsapp"),
}
)
),
)

def publish_flow(
Expand Down
29 changes: 17 additions & 12 deletions pywa/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import collections
import dataclasses
import functools
import hashlib
import json
import logging
Expand All @@ -17,7 +16,7 @@
from types import NoneType
from typing import BinaryIO, Iterable, Literal, Any, Callable

import requests
import httpx

from . import utils
from .api import WhatsAppCloudApi
Expand Down Expand Up @@ -73,7 +72,7 @@ def __init__(
| int
| float
| Literal[utils.Version.GRAPH_API] = utils.Version.GRAPH_API,
session: requests.Session | None = None,
session: httpx.Client | None = None,
server: Flask | FastAPI | None = utils.MISSING,
webhook_endpoint: str = "/",
verify_token: str | None = None,
Expand Down Expand Up @@ -128,11 +127,11 @@ def __init__(
`use permanent token <https://developers.facebook.com/docs/whatsapp/business-management-api/get-started>`_).
base_url: The base URL of the WhatsApp API (Do not change unless you know what you're doing).
api_version: The API version of the WhatsApp Cloud API (default to the latest version).
session: The session to use for requests (default: new ``requests.Session()``, For cases where you want to
session: The session to use for requests (default: new ``httpx.Client()``, For cases where you want to
use a custom session, e.g. for proxy support. Do not use the same session across multiple WhatsApp clients!).
server: The Flask or FastAPI app instance to use for the webhook. required when you want to handle incoming
updates.
callback_url: The callback URL to register (optional, only if you want pywa to register the callback URL for
callback_url: The callback URL of the server to register (optional, only if you want pywa to register the callback URL for
you).
verify_token: The verify token of the registered ``callback_url`` (Required when ``server`` is provided.
The verify token can be any string. It is used to challenge the webhook endpoint to verify that the
Expand Down Expand Up @@ -211,14 +210,14 @@ def __init__(

def _setup_api(
self,
session: requests.Session | None,
session: httpx.Client | None,
token: str,
base_url: str,
api_version: float,
) -> None:
self.api = WhatsAppCloudApi(
token=token,
session=session or requests.Session(),
session=session or httpx.Client(),
base_url=base_url,
api_version=api_version,
)
Expand Down Expand Up @@ -1381,7 +1380,7 @@ def upload_media(
media: str | pathlib.Path | bytes | BinaryIO,
mime_type: str | None = None,
filename: str | None = None,
dl_session: requests.Session | None = None,
dl_session: httpx.Client | None = None,
phone_id: str | None = None,
) -> str:
"""
Expand All @@ -1399,7 +1398,7 @@ def upload_media(
media: The media to upload (can be a URL, bytes, or a file path).
mime_type: The MIME type of the media (required if media is bytes or a file path).
filename: The file name of the media (required if media is bytes).
dl_session: A requests session to use when downloading the media from a URL (optional, if not provided, a
dl_session: A httpx client to use when downloading the media from a URL (optional, if not provided, a
new session will be created).
phone_id: The phone ID to upload the media to (optional, if not provided, the client's phone ID will be used).
Expand All @@ -1422,10 +1421,16 @@ def upload_media(
mime_type or mimetypes.guess_type(path)[0],
)
elif (url := str(media)).startswith(("https://", "http://")):
res = (dl_session or requests).get(url)
is_requests, err_cls = utils.is_requests_and_err(dl_session)
if is_requests:
_logger.warning(
"Using `requests.Session` is deprecated and will be removed in future versions. "
"Please use `httpx.Client` instead."
)
res = (dl_session or httpx).get(url)
try:
res.raise_for_status()
except requests.HTTPError as e:
except err_cls as e:
raise ValueError(
f"An error occurred while downloading from {url}"
) from e
Expand Down Expand Up @@ -1502,7 +1507,7 @@ def download_media(
path: The path where to save the file (if not provided, the current working directory will be used).
filename: The name of the file (if not provided, it will be guessed from the URL + extension).
in_memory: Whether to return the file as bytes instead of saving it to disk (default: False).
**kwargs: Additional arguments to pass to :py:func:`requests.get`.
**kwargs: Additional arguments to pass to :py:func:`httpx.get`.
Returns:
The path of the saved file if ``in_memory`` is False, the file as bytes otherwise.
Expand Down
8 changes: 4 additions & 4 deletions pywa/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import functools
from typing import Iterable, Type

import requests
import httpx


class WhatsAppError(Exception):
Expand All @@ -23,7 +23,7 @@ class WhatsAppError(Exception):
details: The error details (optional).
fbtrace_id: The Facebook trace ID (optional).
href: The href to the documentation (optional).
raw_response: The :class:`requests.Response` obj that returned the error (optional, only if the error was raised
raw_response: The :class:`httpx.Response` obj that returned the error (optional, only if the error was raised
from an API call).
"""

Expand All @@ -36,7 +36,7 @@ def __init__(
details: str | None,
fbtrace_id: str | None,
href: str | None,
raw_response: requests.Response | None,
raw_response: httpx.Response | None,
error_subcode: int | None = None,
err_type: str | None = None,
) -> None:
Expand All @@ -56,7 +56,7 @@ def status_code(self) -> int | None:

@classmethod
def from_dict(
cls, error: dict, response: requests.Response | None = None
cls, error: dict, response: httpx.Response | None = None
) -> "WhatsAppError":
"""Create an error from a response."""
return cls._get_exception(error["code"])(
Expand Down
4 changes: 2 additions & 2 deletions pywa/types/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def download(
path: The path where to save the file (if not provided, the current working directory will be used).
filename: The name of the file (if not provided, it will be guessed from the URL + extension).
in_memory: Whether to return the file as bytes instead of saving it to disk (default: False).
**kwargs: Additional arguments to pass to requests.get.
**kwargs: Additional arguments to pass to ``httpx.get(...)``.
Returns:
The path of the saved file if ``in_memory`` is False, the file as bytes otherwise.
Expand Down Expand Up @@ -223,7 +223,7 @@ def download(
filepath: The path where to save the file (if not provided, the current working directory will be used).
filename: The name of the file (if not provided, it will be guessed from the URL + extension).
in_memory: Whether to return the file as bytes instead of saving it to disk (default: False).
**kwargs: Additional arguments to pass to requests.get.
**kwargs: Additional arguments to pass to ``httpx.get(...)``.
Returns:
The path of the saved file if ``in_memory`` is False, the file as bytes otherwise.
Expand Down
30 changes: 23 additions & 7 deletions pywa/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
import enum
import importlib
import warnings
import logging
from typing import Any, Callable, Protocol, TypeAlias


import httpx
import requests

_logger = logging.getLogger(__name__)


HUB_VT = "hub.verify_token"
"""The key for the verify token in the query parameters of the webhook get request."""
Expand Down Expand Up @@ -77,6 +79,16 @@ def is_installed(lib: str) -> bool:
return False


def is_requests_and_err(session) -> tuple[bool, type[Exception]]:
"""Check if the given object is a requests/httpx session and return the error type."""
try:
if isinstance(session, importlib.import_module("requests").Session):
return True, importlib.import_module("requests").HTTPError
raise ImportError
except ImportError:
return False, importlib.import_module("httpx").HTTPStatusError


class Version(enum.Enum):
"""
Enum for the latest and minimum versions of the `Graph API <https://developers.facebook.com/docs/graph-api>`_ and
Expand Down Expand Up @@ -295,9 +307,7 @@ def webhook_updates_validator(
return hmac.compare_digest(signature, x_hub_signature.removeprefix("sha256="))


def _download_cdn_file_sync(
session: requests.Session | httpx.Client, url: str
) -> bytes:
def _download_cdn_file_sync(session: httpx.Client, url: str) -> bytes:
response = session.get(url)
response.raise_for_status()
return response.content
Expand All @@ -311,7 +321,7 @@ async def _download_cdn_file_async(session: httpx.AsyncClient, url: str) -> byte

def flow_request_media_decryptor_sync(
encrypted_media: dict[str, str | dict[str, str]],
dl_session: requests.Session | httpx.Client | None = None,
dl_session: httpx.Client | None = None,
) -> tuple[str, str, bytes]:
"""
Decrypt the encrypted media file from the flow request.
Expand All @@ -334,7 +344,7 @@ def flow_request_media_decryptor_sync(
Args:
encrypted_media (dict): encrypted media data from the flow request (see example above).
dl_session (requests.Session | httpx.Client): download session. Optional.
dl_session (httpx.Client): download session. Optional.
Returns:
tuple[str, str, bytes]
Expand All @@ -345,6 +355,12 @@ def flow_request_media_decryptor_sync(
Raises:
ValueError: If any of the hash verifications fail.
"""
is_requests, _ = is_requests_and_err(dl_session)
if is_requests:
_logger.warning(
"Using `requests.Session` is deprecated and will be removed in future versions. "
"Please use `httpx.Client` instead."
)
cdn_file = _download_cdn_file_sync(
dl_session or httpx.Client(), encrypted_media["cdn_url"]
)
Expand Down
4 changes: 2 additions & 2 deletions pywa_async/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def __init__(
use a custom session, e.g. for proxy support. Do not use the same session across multiple WhatsApp clients!).
server: The Flask or FastAPI app instance to use for the webhook. required when you want to handle incoming
updates.
callback_url: The callback URL to register (optional, only if you want pywa to register the callback URL for
callback_url: The callback URL of the server to register (optional, only if you want pywa to register the callback URL for
you).
verify_token: The verify token of the registered ``callback_url`` (Required when ``server`` is provided.
The verify token can be any string. It is used to challenge the webhook endpoint to verify that the
Expand Down Expand Up @@ -1340,7 +1340,7 @@ async def upload_media(
media: The media to upload (can be a URL, bytes, or a file path).
mime_type: The MIME type of the media (required if media is bytes or a file path).
filename: The file name of the media (required if media is bytes).
dl_session: A requests session to use when downloading the media from a URL (optional, if not provided, a
dl_session: A httpx client to use when downloading the media from a URL (optional, if not provided, a
new session will be created).
phone_id: The phone ID to upload the media to (optional, if not provided, the client's phone ID will be used).
Expand Down
2 changes: 1 addition & 1 deletion pywa_async/types/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async def download(
path: The path where to save the file (if not provided, the current working directory will be used).
filename: The name of the file (if not provided, it will be guessed from the URL + extension).
in_memory: Whether to return the file as bytes instead of saving it to disk (default: False).
**kwargs: Additional arguments to pass to requests.get.
**kwargs: Additional arguments to pass to httpx.get.
Returns:
The path of the saved file if ``in_memory`` is False, the file as bytes otherwise.
Expand Down
2 changes: 1 addition & 1 deletion pywa_async/types/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ async def download_media(
filepath: The path where to save the file (if not provided, the current working directory will be used).
filename: The name of the file (if not provided, it will be guessed from the URL + extension).
in_memory: Whether to return the file as bytes instead of saving it to disk (default: False).
**kwargs: Additional arguments to pass to requests.get.
**kwargs: Additional arguments to pass to httpx.get.
Returns:
The path of the saved file if ``in_memory`` is False, the file as bytes otherwise.
Expand Down
8 changes: 4 additions & 4 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-r requirements.txt
pytest==8.2.0
pytest-asyncio==0.23.6
pytest==8.3.2
pytest-asyncio==0.23.8
pytest-mock==3.14.0
ruff==0.4.3
pre-commit==3.7.0
ruff==0.5.5
pre-commit==3.7.1
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
requests>=2.31.0
httpx>=0.27.0

0 comments on commit e831c30

Please sign in to comment.