diff --git a/poetry.lock b/poetry.lock index 8abf7ffc..741446ad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1564,4 +1564,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "ae1b53ae512540bd171fd27573ba403cbb0305600894fcde428fcc629ecf04c8" +content-hash = "ec5f040ea5213a128515324d56efc110618240b642f3cea46727573acab61f87" diff --git a/pyproject.toml b/pyproject.toml index 1b7dfd9f..170ee7c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ __version__ = "0.0.0" [tool.poetry.dependencies] python = "^3.8" -requests = ">=2,<3" arrow = ">=0.4.4,<1" pyparsing = ">=2.0,<3" clique = "==1.6.1" diff --git a/source/ftrack_api/accessor/server.py b/source/ftrack_api/accessor/server.py index a8f5a188..8f553e05 100644 --- a/source/ftrack_api/accessor/server.py +++ b/source/ftrack_api/accessor/server.py @@ -4,25 +4,43 @@ import os import hashlib import base64 +from typing import Optional, TYPE_CHECKING -import requests +import httpx from .base import Accessor from ..data import String import ftrack_api.exception from ftrack_api.uploader import Uploader import ftrack_api.symbol +from .._http import ssl_context + +if TYPE_CHECKING: + from ftrack_api.session import Session class ServerFile(String): """Representation of a server file.""" - def __init__(self, resource_identifier, session, mode="rb"): + def __init__( + self, + resource_identifier, + session: "Session", + mode="rb", + http: Optional[httpx.Client] = None, + ): """Initialise file.""" self.mode = mode self.resource_identifier = resource_identifier self._session = session self._has_read = False + self._http = ( + http + if http is not None + else httpx.Client( + verify=ssl_context, + ) + ) super(ServerFile, self).__init__() @@ -46,25 +64,25 @@ def _read(self): position = self.tell() self.seek(0) - response = requests.get( + with self._http.stream( + "GET", "{0}/component/get".format(self._session.server_url), params={ "id": self.resource_identifier, "username": self._session.api_user, "apiKey": self._session.api_key, }, - stream=True, - ) - - try: - response.raise_for_status() - except requests.exceptions.HTTPError as error: - raise ftrack_api.exception.AccessorOperationFailedError( - "Failed to read data: {0}.".format(error) - ) + follow_redirects=True, + ) as response: + try: + response.raise_for_status() + except httpx.HTTPError as error: + raise ftrack_api.exception.AccessorOperationFailedError( + "Failed to read data: {0}.".format(error) + ) - for block in response.iter_content(ftrack_api.symbol.CHUNK_SIZE): - self.wrapped_file.write(block) + for block in response.iter_bytes(ftrack_api.symbol.CHUNK_SIZE): + self.wrapped_file.write(block) self.flush() self.seek(position) @@ -134,19 +152,24 @@ def _compute_checksum(self): class _ServerAccessor(Accessor): """Provide server location access.""" - def __init__(self, session, **kw): + def __init__(self, session: "Session", **kw): """Initialise location accessor.""" super(_ServerAccessor, self).__init__(**kw) + self.http = httpx.Client( + verify=ssl_context, + ) self._session = session def open(self, resource_identifier, mode="rb"): """Return :py:class:`~ftrack_api.Data` for *resource_identifier*.""" - return ServerFile(resource_identifier, session=self._session, mode=mode) + return ServerFile( + resource_identifier, session=self._session, mode=mode, http=self.http + ) def remove(self, resourceIdentifier): """Remove *resourceIdentifier*.""" - response = requests.get( + response = self.http.get( "{0}/component/remove".format(self._session.server_url), params={ "id": resourceIdentifier, @@ -154,7 +177,7 @@ def remove(self, resourceIdentifier): "apiKey": self._session.api_key, }, ) - if response.status_code != 200: + if not response.is_success: raise ftrack_api.exception.AccessorOperationFailedError( "Failed to remove file." ) diff --git a/source/ftrack_api/data.py b/source/ftrack_api/data.py index 9978beb9..3c1e6ad9 100644 --- a/source/ftrack_api/data.py +++ b/source/ftrack_api/data.py @@ -5,7 +5,7 @@ import os from abc import ABCMeta, abstractmethod import tempfile -import typing +from typing import BinaryIO class Data(metaclass=ABCMeta): @@ -51,7 +51,7 @@ def close(self): class FileWrapper(Data): """Data wrapper for Python file objects.""" - def __init__(self, wrapped_file: typing.IO): + def __init__(self, wrapped_file: BinaryIO): """Initialise access to *wrapped_file*.""" self.wrapped_file = wrapped_file self._read_since_last_write = False diff --git a/source/ftrack_api/event/hub.py b/source/ftrack_api/event/hub.py index 4e870677..b34fa4c6 100644 --- a/source/ftrack_api/event/hub.py +++ b/source/ftrack_api/event/hub.py @@ -19,14 +19,14 @@ import socket import ssl -import requests -import requests.exceptions +import httpx import websocket import ftrack_api.exception import ftrack_api.event.base import ftrack_api.event.subscriber import ftrack_api.event.expression +from ftrack_api._http import ssl_context from ftrack_api.logging import LazyLogMessage as L @@ -859,29 +859,25 @@ def _get_socket_io_session(self): } if self._headers: req_headers.update(self._headers) - response = requests.get( + response = httpx.get( socket_io_url, + verify=ssl_context, headers=req_headers, cookies=self._cookies, timeout=60, # 60 seconds timeout to recieve errors faster. ) - except requests.exceptions.Timeout as error: + except httpx.TimeoutException as error: raise ftrack_api.exception.EventHubConnectionError( "Timed out connecting to server: {0}.".format(error) ) - except requests.exceptions.SSLError as error: - raise ftrack_api.exception.EventHubConnectionError( - "Failed to negotiate SSL with server: {0}.".format(error) - ) - except requests.exceptions.ConnectionError as error: + except httpx.ConnectError as error: raise ftrack_api.exception.EventHubConnectionError( "Failed to connect to server: {0}.".format(error) ) else: - status = response.status_code - if status != 200: + if not response.is_success: raise ftrack_api.exception.EventHubConnectionError( - "Received unexpected status code {0}.".format(status) + "Received unexpected status code {0}.".format(response.status_code) ) # Parse result and return session information. diff --git a/source/ftrack_api/session.py b/source/ftrack_api/session.py index 998d9789..1950e08a 100644 --- a/source/ftrack_api/session.py +++ b/source/ftrack_api/session.py @@ -22,9 +22,7 @@ import atexit import warnings -import requests -import requests.auth -import requests.utils +import httpx import arrow import clique import platformdirs @@ -50,26 +48,26 @@ import ftrack_api.accessor.server import ftrack_api._centralized_storage_scenario import ftrack_api.logging +from ftrack_api._http import ssl_context from ftrack_api.logging import LazyLogMessage as L from weakref import WeakMethod -class SessionAuthentication(requests.auth.AuthBase): +class SessionAuthentication(httpx.Auth): """Attach ftrack session authentication information to requests.""" def __init__(self, api_key, api_user): """Initialise with *api_key* and *api_user*.""" self.api_key = api_key self.api_user = api_user - super(SessionAuthentication, self).__init__() - def __call__(self, request): + def auth_flow(self, request: httpx.Request): """Modify *request* to have appropriate headers.""" request.headers.update( {"ftrack-api-key": self.api_key, "ftrack-user": self.api_user} ) - return request + yield request class Session(object): @@ -246,30 +244,20 @@ def __init__( self.merge_lock = threading.RLock() self._managed_request = None - self._request = requests.Session() - - if cookies: - if not isinstance(cookies, collections.abc.Mapping): - raise TypeError("The cookies argument is required to be a mapping.") - self._request.cookies.update(cookies) - - if headers: - if not isinstance(headers, collections.abc.Mapping): - raise TypeError("The headers argument is required to be a mapping.") - - headers = dict(headers) - - else: - headers = {} if not isinstance(strict_api, bool): raise TypeError("The strict_api argument is required to be a boolean.") - headers.update({"ftrack-strict-api": "true" if strict_api is True else "false"}) + headers = httpx.Headers(headers) + headers["ftrack-strict-api"] = "true" if strict_api is True else "false" - self._request.headers.update(headers) - self._request.auth = SessionAuthentication(self._api_key, self._api_user) - self.request_timeout = timeout + self._request = httpx.Client( + auth=SessionAuthentication(self._api_key, self._api_user), + verify=ssl_context, + cookies=cookies, + headers=headers, + timeout=timeout, + ) # Auto populating state is now thread-local self._auto_populate = collections.defaultdict(lambda: auto_populate) @@ -286,7 +274,7 @@ def __init__( self._api_user, self._api_key, headers=headers, - cookies=requests.utils.dict_from_cookiejar(self._request.cookies), + cookies=self._request.cookies, ) self._auto_connect_event_hub_thread = None @@ -350,7 +338,7 @@ def __exit__(self, exception_type, exception_value, traceback): self.close() @property - def _request(self): + def _request(self) -> httpx.Client: """Return request session. Raise :exc:`ftrack_api.exception.ConnectionClosedError` if session has @@ -1631,8 +1619,7 @@ def call(self, data): response = self._request.post( url, headers=headers, - data=data, - timeout=self.request_timeout, + content=data, ) self.logger.debug(L("Call took: {0}", response.elapsed.total_seconds())) self.logger.debug(L("Response: {0!r}", response.text)) @@ -1642,7 +1629,7 @@ def call(self, data): # handle response exceptions and / or other http exceptions # (strict api used => status code returned => raise_for_status() => HTTPError) - except requests.exceptions.HTTPError as exc: + except httpx.HTTPError as exc: if "exception" in result: error_message = "Server reported error: {0}({1})".format( result["exception"], result["content"] diff --git a/test/unit/test_session.py b/test/unit/test_session.py index 8735aab9..2864bc27 100644 --- a/test/unit/test_session.py +++ b/test/unit/test_session.py @@ -13,8 +13,7 @@ import pytest import mock import arrow -import requests -import requests.utils +import httpx import ftrack_api import ftrack_api.cache @@ -1113,14 +1112,14 @@ def test_load_schemas_bypassing_cache(mocker, session, temporary_valid_schema_ca def test_get_tasks_widget_url(session): """Tasks widget URL returns valid HTTP status.""" url = session.get_widget_url("tasks") - response = requests.get(url) + response = httpx.get(url) response.raise_for_status() def test_get_info_widget_url(session, task): """Info widget URL for *task* returns valid HTTP status.""" url = session.get_widget_url("info", entity=task, theme="light") - response = requests.get(url) + response = httpx.get(url) response.raise_for_status() @@ -1466,25 +1465,18 @@ def test_strict_api_header(): """Create ftrack session containing ftrack-strict-api = True header.""" new_session = ftrack_api.Session(strict_api=True) - assert ( - "ftrack-strict-api" in new_session._request.headers.keys(), - new_session._request.headers["ftrack-strict-api"] == "true", - ) + assert new_session._request.headers.get("ftrack-strict-api") == "true" def test_custom_cookies_session(): """Create ftrack session containing custom cookies.""" new_session = ftrack_api.Session(cookies={"abc": "def"}) - cookies_dict = requests.utils.dict_from_cookiejar(new_session._request.cookies) - assert ("abc" in cookies_dict.keys(), cookies_dict["abc"] == "def") + assert new_session._request.cookies.get("abc") == "def" def test_custom_headers_session(): """Create ftrack session containing custom headers.""" new_session = ftrack_api.Session(headers={"abc": "def"}) - assert ( - "abc" in new_session._request.headers.keys(), - new_session._request.headers["abc"] == "def", - ) + assert new_session._request.headers.get("abc") == "def"