From b315e2e5e276b18fba8c36a915dd860a924b30a1 Mon Sep 17 00:00:00 2001 From: Michela Iannaccone Date: Wed, 8 May 2024 22:08:21 -0400 Subject: [PATCH 1/3] build out http helper class --- canvas_sdk/utils/__init__.py | 3 ++ canvas_sdk/utils/http.py | 55 ++++++++++++++++++++++++++++++++++++ poetry.lock | 15 ++++++++-- pyproject.toml | 3 +- 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 canvas_sdk/utils/http.py diff --git a/canvas_sdk/utils/__init__.py b/canvas_sdk/utils/__init__.py index e69de29b..cf673a5d 100644 --- a/canvas_sdk/utils/__init__.py +++ b/canvas_sdk/utils/__init__.py @@ -0,0 +1,3 @@ +from canvas_sdk.utils.http import Http + +__all__ = ("Http",) diff --git a/canvas_sdk/utils/http.py b/canvas_sdk/utils/http.py new file mode 100644 index 00000000..f52aa9b0 --- /dev/null +++ b/canvas_sdk/utils/http.py @@ -0,0 +1,55 @@ +import time +from functools import wraps +from typing import Any, Callable, TypeVar + +import requests +import statsd + +F = TypeVar("F", bound=Callable) + + +class Http: + """A helper class for completing HTTP calls with metrics tracking.""" + + def __init__(self) -> None: + self.session = requests.Session() + self.statsd_client = statsd.StatsClient() + + @staticmethod + def measure_time(fn: F | None = None) -> Callable[[F], F] | F: + """A decorator to store timing of HTTP calls.""" + + def _decorator(fn: F) -> F: + @wraps(fn) + def wrapper(self: "Http", *args: Any, **kwargs: Any) -> Any: + print(fn.__name__) + start_time = time.time() + result = fn(self, *args, **kwargs) + end_time = time.time() + timing = int((end_time - start_time) * 1000) + self.statsd_client.timing(f"http_{fn.__name__}", timing) + return result + + return wrapper + + return _decorator(fn) if fn else _decorator + + @measure_time + def get(self, url: str, headers: dict = {}) -> requests.Response: + """Sends a GET request.""" + return self.session.get(url, headers=headers) + + @measure_time + def post(self, url: str, json: dict | None = None, headers: dict = {}) -> requests.Response: + """Sends a POST request.""" + return self.session.post(url, json=json, headers=headers) + + @measure_time + def put(self, url: str, json: dict | None = None, headers: dict = {}) -> requests.Response: + """Sends a PUT request.""" + return self.session.put(url, json=json, headers=headers) + + @measure_time + def patch(self, url: str, json: dict | None = None, headers: dict = {}) -> requests.Response: + """Sends a PATCH request.""" + return self.session.patch(url, json=json, headers=headers) diff --git a/poetry.lock b/poetry.lock index 4508478b..5593bb88 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -1781,6 +1781,17 @@ pure-eval = "*" [package.extras] tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] +[[package]] +name = "statsd" +version = "4.0.1" +description = "A simple statsd client." +optional = false +python-versions = "*" +files = [ + {file = "statsd-4.0.1-py2.py3-none-any.whl", hash = "sha256:c2676519927f7afade3723aca9ca8ea986ef5b059556a980a867721ca69df093"}, + {file = "statsd-4.0.1.tar.gz", hash = "sha256:99763da81bfea8daf6b3d22d11aaccb01a8d0f52ea521daab37e758a4ca7d128"}, +] + [[package]] name = "text-unidecode" version = "1.3" @@ -1960,4 +1971,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "510372828fd1203954304bbbae17282beb9eb1b5fc6aa8ede1bd52e0c30655b9" +content-hash = "9f99da92ecc0df5d05c98a9a00247f05bd871eb892c1c886595c198eb3a001d5" diff --git a/pyproject.toml b/pyproject.toml index 2dc61fa8..3300c881 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,9 +49,10 @@ pydantic = "^2.6.1" python = ">=3.11,<3.13" python-dotenv = "^1.0.1" requests = "*" +restrictedpython = "^7.1" +statsd = "^4.0.1" typer = {extras = ["all"], version = "*"} websocket-client = "^1.7.0" -restrictedpython = "^7.1" [tool.poetry.group.dev.dependencies] grpcio-tools = "^1.60.1" From 073f9cce064eaa7f7849671e057a83c1e5b04342 Mon Sep 17 00:00:00 2001 From: Michela Iannaccone Date: Fri, 10 May 2024 13:23:52 -0400 Subject: [PATCH 2/3] add data argument to post, put, patch --- canvas_sdk/utils/http.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/canvas_sdk/utils/http.py b/canvas_sdk/utils/http.py index f52aa9b0..07e3d214 100644 --- a/canvas_sdk/utils/http.py +++ b/canvas_sdk/utils/http.py @@ -40,16 +40,34 @@ def get(self, url: str, headers: dict = {}) -> requests.Response: return self.session.get(url, headers=headers) @measure_time - def post(self, url: str, json: dict | None = None, headers: dict = {}) -> requests.Response: + def post( + self, + url: str, + json: dict | None = None, + data: dict | str | list | bytes | None = None, + headers: dict = {}, + ) -> requests.Response: """Sends a POST request.""" - return self.session.post(url, json=json, headers=headers) + return self.session.post(url, json=json, data=data, headers=headers) @measure_time - def put(self, url: str, json: dict | None = None, headers: dict = {}) -> requests.Response: + def put( + self, + url: str, + json: dict | None = None, + data: dict | str | list | bytes | None = None, + headers: dict = {}, + ) -> requests.Response: """Sends a PUT request.""" - return self.session.put(url, json=json, headers=headers) + return self.session.put(url, json=json, data=data, headers=headers) @measure_time - def patch(self, url: str, json: dict | None = None, headers: dict = {}) -> requests.Response: + def patch( + self, + url: str, + json: dict | None = None, + data: dict | str | list | bytes | None = None, + headers: dict = {}, + ) -> requests.Response: """Sends a PATCH request.""" - return self.session.patch(url, json=json, headers=headers) + return self.session.patch(url, json=json, data=data, headers=headers) From 7130f14b29c4d836fe3902d5a4df4c0f14078735 Mon Sep 17 00:00:00 2001 From: Michela Iannaccone Date: Fri, 10 May 2024 13:33:00 -0400 Subject: [PATCH 3/3] create tests --- canvas_sdk/utils/tests.py | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 canvas_sdk/utils/tests.py diff --git a/canvas_sdk/utils/tests.py b/canvas_sdk/utils/tests.py new file mode 100644 index 00000000..be9e1678 --- /dev/null +++ b/canvas_sdk/utils/tests.py @@ -0,0 +1,46 @@ +from unittest.mock import MagicMock, patch + +from canvas_sdk.utils import Http + + +@patch("requests.Session.get") +def test_http_get(mock_get: MagicMock) -> None: + http = Http() + http.get("https://www.canvasmedical.com/", headers={"Authorization": "Bearer as;ldkfjdkj"}) + mock_get.assert_called_once() + + +@patch("requests.Session.post") +def test_http_post(mock_post: MagicMock) -> None: + http = Http() + http.post( + "https://www.canvasmedical.com/", + json={"hey": "hi"}, + data="grant-type=client_credentials", + headers={"Content-type": "application/json"}, + ) + mock_post.assert_called_once() + + +@patch("requests.Session.put") +def test_http_put(mock_put: MagicMock) -> None: + http = Http() + http.put( + "https://www.canvasmedical.com/", + json={"hey": "hi"}, + data="grant-type=client_credentials", + headers={"Content-type": "application/json"}, + ) + mock_put.assert_called_once() + + +@patch("requests.Session.patch") +def test_http_patch(mock_patch: MagicMock) -> None: + http = Http() + http.patch( + "https://www.canvasmedical.com/", + json={"hey": "hi"}, + data="grant-type=client_credentials", + headers={"Content-type": "application/json"}, + ) + mock_patch.assert_called_once()