Skip to content
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
15 changes: 13 additions & 2 deletions fintoc/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,15 @@ def _build_jws_header(self, method, raw_body=None):

return {}

def _build_request(self, method, url, params, headers, json=None):
def _build_request(self, method, url, params, headers, json=None, files=None):
if files is not None:
# Multipart uploads: let httpx set the multipart Content-Type and
# boundary automatically. Multipart bodies are not utf-8-safe, so
# JWS signing is skipped for these requests.
return httpx.Request(
method, url, headers=headers, params=params, files=files
)

request = httpx.Request(method, url, headers=headers, params=params, json=json)

request.headers.update(
Expand All @@ -76,6 +84,7 @@ def request(
method="get",
params=None,
json=None,
files=None,
idempotency_key=None,
):
"""
Expand All @@ -97,7 +106,9 @@ def request(
headers=headers,
)

_request = self._build_request(method, url, all_params, headers, json)
_request = self._build_request(
method, url, all_params, headers, json, files=files
)
response = self._client.send(_request)
response.raise_for_status()
try:
Expand Down
1 change: 1 addition & 0 deletions fintoc/managers/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .entities_manager import EntitiesManager
from .invoices_manager import InvoicesManager
from .movements_manager import MovementsManager
from .onboardings_manager import OnboardingsManager
from .payment_intents_manager import PaymentIntentsManager
from .payment_methods_manager import PaymentMethodsManager
from .products_manager import ProductsManager
Expand Down
21 changes: 20 additions & 1 deletion fintoc/managers/v2/entities_manager.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
"""Module to hold the entities manager."""

from fintoc.managers.v2.onboardings_manager import OnboardingsManager
from fintoc.mixins import ManagerMixin


class EntitiesManager(ManagerMixin):
"""Represents an entities manager."""

resource = "entity"
methods = ["list", "get"]
methods = ["list", "get", "create"]

def __init__(self, path, client):
super().__init__(path, client)
self.__onboardings_manager = None

@property
def onboardings(self):
"""Proxies the onboardings manager."""
if self.__onboardings_manager is None:
self.__onboardings_manager = OnboardingsManager(
"/v2/entities/{entity_id}/onboardings",
self._client,
)
return self.__onboardings_manager

@onboardings.setter
def onboardings(self, new_value): # pylint: disable=no-self-use
raise NameError("Attribute name corresponds to a manager")
54 changes: 54 additions & 0 deletions fintoc/managers/v2/onboardings_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Module to hold the onboardings manager."""

import mimetypes
import os

from fintoc.mixins import ManagerMixin


class OnboardingsManager(ManagerMixin):
"""Represents an onboardings manager."""

resource = "onboarding"
methods = [
"list",
"get",
"create",
"submit",
"upload_document",
"upload_shareholder_document",
]

def _submit(self, identifier, **kwargs):
"""Submit an onboarding for review."""
path = f"{self._build_path(**kwargs)}/{identifier}/submit"
return self._create(path_=path, **kwargs)

def _upload_document(self, identifier, slot_key, file, **kwargs):
"""Upload a document to a slot, identified by :slot_key:."""
path = f"{self._build_path(**kwargs)}/{identifier}/documents/{slot_key}"
return self._upload(path, {"file": self._build_file_payload(file)})

def _upload_shareholder_document(self, identifier, shareholder_id, file, **kwargs):
"""Upload a document for a shareholder of an onboarding."""
path = (
f"{self._build_path(**kwargs)}/{identifier}"
f"/shareholders/{shareholder_id}/document"
)
return self._upload(path, {"file": self._build_file_payload(file)})

@staticmethod
def _build_file_payload(file):
"""
Build the ``(filename, fileobj, content_type)`` tuple expected by httpx
from either a path (``str`` / ``os.PathLike``) or a binary file-like
object.
"""
if isinstance(file, (str, os.PathLike)):
filename = os.path.basename(os.fspath(file))
content_type = mimetypes.guess_type(filename)[0]
return (filename, open(file, "rb"), content_type) # noqa: SIM115

filename = os.path.basename(getattr(file, "name", "") or "") or None
content_type = mimetypes.guess_type(filename)[0] if filename else None
return (filename, file, content_type)
12 changes: 12 additions & 0 deletions fintoc/mixins/manager_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
resource_get,
resource_list,
resource_update,
resource_upload,
)
from fintoc.utils import can_raise_fintoc_error, deprecate, get_resource_class

Expand Down Expand Up @@ -117,6 +118,17 @@ def _create(self, idempotency_key=None, path_=None, **kwargs):
)
return self.post_create_handler(object_, **kwargs)

@can_raise_fintoc_error
def _upload(self, path, files):
"""
Upload :files: to :path: through a multipart PUT and objetize the
result. :files: is a dict in the format expected by httpx.
"""
klass = get_resource_class(self.__class__.resource)
return resource_upload(
self._client, path, klass, self._handlers, self.__class__.methods, files
)

@can_raise_fintoc_error
def _update(self, identifier, path_=None, **kwargs):
"""
Expand Down
13 changes: 13 additions & 0 deletions fintoc/resource_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ def resource_create(
)


def resource_upload(client, path, klass, handlers, methods, files):
"""Upload files to a resource endpoint through a multipart PUT request."""
data = client.request(path, method="put", files=files)
return objetize(
klass,
client,
data,
handlers=handlers,
methods=methods,
path=path,
)


def resource_update(
client, path, id_, klass, handlers, methods, params, custom_path=None
):
Expand Down
3 changes: 3 additions & 0 deletions fintoc/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
from .v2.customer import Customer
from .v2.entity import Entity
from .v2.invoice import Invoice as InvoiceV2
from .v2.onboarding import Onboarding
from .v2.onboarding_document import OnboardingDocument
from .v2.onboarding_shareholder import OnboardingShareholder
from .v2.payment_method import PaymentMethod
from .v2.subscription import Subscription as SubscriptionV2
from .v2.transfer import Transfer
Expand Down
3 changes: 3 additions & 0 deletions fintoc/resources/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
from .entity import Entity
from .invoice import Invoice
from .movement import Movement
from .onboarding import Onboarding
from .onboarding_document import OnboardingDocument
from .onboarding_shareholder import OnboardingShareholder
from .payment_method import PaymentMethod
from .product import Product
from .subscription import Subscription
Expand Down
12 changes: 12 additions & 0 deletions fintoc/resources/v2/onboarding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""Module to hold the Onboarding resource."""

from fintoc.mixins import ResourceMixin


class Onboarding(ResourceMixin):
"""Represents a Fintoc Onboarding."""

mappings = {
"shareholders": "onboarding_shareholder",
"documents": "onboarding_document",
}
7 changes: 7 additions & 0 deletions fintoc/resources/v2/onboarding_document.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Module to hold the OnboardingDocument resource."""

from fintoc.mixins import ResourceMixin


class OnboardingDocument(ResourceMixin):
"""Represents a Fintoc Onboarding Document."""
9 changes: 9 additions & 0 deletions fintoc/resources/v2/onboarding_shareholder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Module to hold the OnboardingShareholder resource."""

from fintoc.mixins import ResourceMixin


class OnboardingShareholder(ResourceMixin):
"""Represents a Fintoc Onboarding Shareholder."""

mappings = {"document": "onboarding_document"}
2 changes: 1 addition & 1 deletion fintoc/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def objetize(klass, client, data, handlers={}, methods=[], path=None):
"""Transform the :data: object into an object with class :klass:."""
if data is None:
return None
if klass in [str, int, dict, bool, objetize_datetime]:
if klass in [str, int, float, dict, bool, objetize_datetime]:
return klass(data)
return klass(client, handlers, methods, path, **data)

Expand Down
14 changes: 12 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,23 @@ def send(self, request: httpx.Request, **_kwargs):
**({} if request.url.params is None else request.url.params),
}
usable_url = request.url.path
raw_body = request.content.decode("utf-8")
# Materialize the (possibly streaming, e.g. multipart) request body,
# mirroring what the real httpx transport does before sending.
request.read()
content_type = request.headers.get("content-type", "")
if content_type.startswith("multipart/form-data"):
# Multipart bodies are not utf-8/JSON-parseable; surface the raw
# body length so tests can assert the upload was sent.
body = {"multipart": True, "content_length": len(request.content)}
else:
raw_body = request.content.decode("utf-8")
body = json.loads(raw_body) if raw_body else None
return MockResponse(
request.method.lower(),
self.base_url,
usable_url.lstrip("/"),
complete_params,
json.loads(raw_body) if raw_body else None,
body,
request.headers,
)

Expand Down
Loading
Loading