8000 Pass the raw body to the signature generator instead of an object by daleal · Pull Request #75 · fintoc-com/fintoc-python · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Pass the raw body to the signature generator instead of an object #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions fintoc/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Module to house the Client object of the Fintoc Python SDK.
"""

import urllib
import uuid
from json.decoder import JSONDecodeError

Expand Down Expand Up @@ -45,17 +46,29 @@ def _get_static_headers(self):

return headers

def _get_headers(self, method, json=None, idempotency_key=None):
def _get_base_headers(self, method, idempotency_key=None):
headers = dict(self.headers)
if self.__jws and json and method.lower() in ["post", "put", "patch"]:
jws_header = self.__jws.generate_header(json)
headers["Fintoc-JWS-Signature"] = jws_header

if method.lower() == "post":
headers["Idempotency-Key"] = idempotency_key or str(uuid.uuid4())

return headers

def _build_jws_header(self, method, raw_body=None):
if self.__jws and raw_body and method.lower() in ["post", "put", "patch"]:
jws_header = self.__jws.generate_header(raw_body)
return {"Fintoc-JWS-Signature": jws_header}

return {}

def _build_request(self, method, url, params, headers, json=None):
request = httpx.Request(method, url, headers=headers, params=params, json=json)

request.headers.update(
self._build_jws_header(method, request.content.decode("utf-8"))
)
return request

def request(
self,
path,
Expand All @@ -68,16 +81,24 @@ def request(
"""
Uses the internal httpx client to make a simple or paginated request.
"""
url = f"{self.base_url}/{path.lstrip('/')}"
url = (
path
if urllib.parse.urlparse(path).scheme
else f"{self.base_url}/{path.lstrip('/')}"
)
headers = self._get_base_headers(method, idempotency_key=idempotency_key)
all_params = {**self.params, **params} if params else self.params
headers = self._get_headers(method, json=json, idempotency_key=idempotency_key)

if paginated:
return paginate(self._client, url, headers=headers, params=all_params)

response = self._client.request(
method, url, headers=headers, params=all_params, json=json
)
return paginate(
self._client,
url,
params=all_params,
headers=headers,
)

_request = self._build_request(method, url, all_params, headers, json)
response = self._client.send(_request)
response.raise_for_status()
try:
return response.json()
Expand Down
25 changes: 19 additions & 6 deletions fintoc/paginator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
import re
from functools import reduce

import httpx

from fintoc.constants import LINK_HEADER_PATTERN


def paginate(client, path, headers, params):
def paginate(
client: httpx.Client,
url: str,
params: dict[str, str] = {},
headers: dict[str, str] = {},
):
"""
Fetch a paginated resource and return a generator with all of
its instances.
"""
response = request(client, path, headers=headers, params=params)
response = request(client, url, params=params, headers=headers)
elements = response["elements"]
for element in elements:
yield element
Expand All @@ -22,12 +29,18 @@ def paginate(client, path, headers, params):
yield element


def request(client, path, headers={}, params={}):
def request(
client: httpx.Client,
url: str,
params: dict[str, str] = {},
headers: dict[str, str] = {},
):
"""
Fetch a page of a resource and return its elements and the next
page of the resource.
"""
response = client.request("get", path, headers=headers, params=params)
_request = httpx.Request("get", url, params=params, headers=headers)
response = client.send(_request)
response.raise_for_status()
headers = parse_link_headers(response.headers.get("link"))
next_ = headers and headers.get("next")
Expand All @@ -38,7 +51,7 @@ def request(client, path, headers={}, params={}):
}


def parse_link_headers(link_header):
def parse_link_headers(link_header: str):
"""
Receive the 'link' header and return a dictionary with
every key: value instance present on the header.
Expand All @@ -48,7 +61,7 @@ def parse_link_headers(link_header):
return reduce(parse_link, link_header.split(","), {})


def parse_link(dictionary, link):
def parse_link(dictionary: dict[str, str], link: str):
"""
Receive a dictionary with already parsed key: values from the
'link' header along with another link and return the original
Expand Down
21 changes: 16 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
by PyTest.
"""

import json
from json.decoder import JSONDecodeError

import httpx
Expand Down Expand Up @@ -96,13 +97,23 @@ def json(self):
}

class MockClient(httpx.Client):
def request(self, method, url, params=None, json=None, headers=None, **kwargs):
query = url.split("?")[-1].split("&") if "?" in url else []
def send(self, request: httpx.Request, **_kwargs):
query_string = request.url.query.decode("utf-8")
query = query_string.split("&") if query_string else []
inner_params = {y[0]: y[1] for y in (x.split("=") for x in query)}
complete_params = {**inner_params, **({} if params is None else params)}
usable_url = url.split("//")[-1].split("/", 1)[-1].split("?")[0]
complete_params = {
**inner_params,
**({} if request.url.params is None else request.url.params),
}
usable_url = request.url.path
raw_body = request.content.decode("utf-8")
return MockResponse(
method, self.base_url, usable_url, complete_params, json, headers
request.method.lower(),
self.base_url,
usable_url.lstrip("/"),
complete_params,
json.loads(raw_body) if raw_body else None,
request.headers,
)

monkeypatch.setattr(httpx, "Client", MockClient)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_client.py
5D40
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def test_post_request(self):
data = self.client.request("/v2/transfers", method="post")
assert isinstance(data, dict)

idempotency_key = data["headers"]["Idempotency-Key"]
idempotency_key = data["headers"]["idempotency-key"]
assert idempotency_key is not None and idempotency_key != ""

def test_post_request_with_custom_idempotency_key(self):
Expand All @@ -130,5 +130,5 @@ def test_post_request_with_custom_idempotency_key(self):
)
assert isinstance(data, dict)

idempotency_key = data["headers"]["Idempotency-Key"]
idempotency_key = data["headers"]["idempotency-key"]
assert idempotency_key == "1234"
4 changes: 2 additions & 2 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ def test_v2_transfer_create(self):
assert transfer.json.description == description
assert transfer.json.metadata.test_key == metadata["test_key"]

idempotency_key_header = transfer.serialize()["headers"]["Idempotency-Key"]
idempotency_key_header = getattr(transfer.headers, "idempotency-key")
assert idempotency_key_header is not None and idempotency_key_header != ""

idempotency_key = "123456"
Expand All @@ -626,7 +626,7 @@ def test_v2_transfer_create(self):
metadata=metadata,
idempotency_key=idempotency_key,
)
idempotency_key_header = transfer.serialize()["headers"]["Idempotency-Key"]
idempotency_key_header = getattr(transfer.headers, "idempotency-key")
assert idempotency_key_header == "123456"

def test_v2_simulate_receive_transfer(self):
Expand Down
4 changes: 1 addition & 3 deletions tests/test_paginator.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,7 @@ def test_request_response(self):

def test_request_params_get_passed_to_next_url(self):
client = httpx.Client(base_url="https://test.com")
data = request(
client, "/movements", headers={}, params={"link_token": "sample_link_token"}
)
data = request(client, "/movements", params={"link_token": "sample_link_token"})
assert "next" in data
assert "link_token=sample_link_token" in data["next"]

Expand Down
0