From b2f1412133087c36d71b0d66c197671f0bbbe695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ot=C3=A1vio=20Jacobi?= Date: Fri, 20 Oct 2023 14:44:09 -0300 Subject: [PATCH] Add support to multipart/form-data requests Change-type: minor --- DOCUMENTATION.md | 22 ++++++++++++++++++++-- balena/models/organization.py | 16 ++++++++++++---- balena/pine.py | 25 ++++++++++++++++++------- balena/types/models.py | 9 +++++++++ 4 files changed, 59 insertions(+), 13 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 3b124a36..caeb440a 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -252,7 +252,7 @@ hesitate to open an issue in GitHub](https://github.com/balena-io/balena-sdk-pyt - [get_all(options)](#key.get_all) ⇒ [List[SSHKeyType]](#sshkeytype) - [remove(id)](#key.remove) ⇒ None - [.organization](#organization) - - [create(name, handle)](#organization.create) ⇒ [OrganizationType](#organizationtype) + - [create(name, handle, logo_image)](#organization.create) ⇒ [OrganizationType](#organizationtype) - [get(handle_or_id, options)](#organization.get) ⇒ [OrganizationType](#organizationtype) - [get_all(options)](#organization.get_all) ⇒ [List[OrganizationType]](#organizationtype) - [remove(handle_or_id)](#organization.remove) ⇒ None @@ -2705,13 +2705,14 @@ Remove a ssh key. This class implements organization model for balena python SDK. -### Function: create(name, handle) ⇒ [OrganizationType](#organizationtype) +### Function: create(name, handle, logo_image) ⇒ [OrganizationType](#organizationtype) Creates a new organization. #### Args: name (str): the name of the organization that will be created. handle (Optional[str]): The handle of the organization that will be created. + logo_image (Optional[io.BufferedReader]): The organization logo to be used. #### Returns: dict: organization info. @@ -2719,6 +2720,8 @@ Creates a new organization. #### Examples: ```python >>> balena.models.organization.create('My Org', 'test_org') +>>> with open('mypath/myfile.png', 'rb') as f: +>>> org = sdk.models.organization.create("my-name", None, f) ``` @@ -4246,6 +4249,7 @@ The name must be a string; the optional doc argument can have any type. "created_at": str, "name": str, "handle": str, + "logo_image": WebResource, "has_past_due_invoice_since__date": str, "application": Union[List[TypeApplication], None], "organization_membership": Union[List[OrganizationMembershipType], None], @@ -4669,3 +4673,17 @@ The name must be a string; the optional doc argument can have any type. ``` +### WebResource + + +```python +{ + "filename": str, + "href": str, + "content_type": str, + "content_disposition": str, + "size": int +} +``` + + diff --git a/balena/models/organization.py b/balena/models/organization.py index 66c54376..50c362b2 100644 --- a/balena/models/organization.py +++ b/balena/models/organization.py @@ -1,5 +1,5 @@ -from typing import List, Optional, Union - +from typing import List, Optional, Union, Dict +import io from .. import exceptions from ..balena_auth import request from ..dependent_resource import DependentResource @@ -26,25 +26,33 @@ def __init__(self, pine: PineClient, settings: Settings): self.invite = OrganizationInvite(pine, self, settings) self.membership = OrganizationMembership(pine, self) - def create(self, name: str, handle: Optional[str] = None) -> OrganizationType: + def create( + self, name: str, handle: Optional[str] = None, logo_image: Optional[io.BufferedReader] = None + ) -> OrganizationType: """ Creates a new organization. Args: name (str): the name of the organization that will be created. handle (Optional[str]): The handle of the organization that will be created. + logo_image (Optional[io.BufferedReader]): The organization logo to be used. Returns: dict: organization info. Examples: >>> balena.models.organization.create('My Org', 'test_org') + >>> with open('mypath/myfile.png', 'rb') as f: + >>> org = sdk.models.organization.create("my-name", None, f) """ - data = {"name": name} + data: Dict[str, Union[str, io.BufferedReader]] = {"name": name} if handle is not None: data["handle"] = handle + if logo_image is not None: + data["logo_image"] = logo_image + return self.__pine.post({"resource": "organization", "body": data}) def get_all(self, options: AnyObject = {}) -> List[OrganizationType]: diff --git a/balena/pine.py b/balena/pine.py index de97062c..a8399862 100644 --- a/balena/pine.py +++ b/balena/pine.py @@ -1,9 +1,9 @@ -import json from typing import Any, Optional, cast from urllib.parse import urljoin from ratelimit import limits, sleep_and_retry from time import sleep - +import mimetypes +import io import requests from pine_client import PinejsClientCore from pine_client.client import Params @@ -40,15 +40,26 @@ def _request(self, method: str, url: str, body: Optional[Any] = None) -> Any: def __base_request(self, method: str, url: str, body: Optional[Any] = None) -> Any: token = get_token(self.__settings) - headers = {"Content-Type": "application/json", "X-Balena-Client": f"balena-python-sdk/{self.__sdk_version}"} + headers = {"X-Balena-Client": f"balena-python-sdk/{self.__sdk_version}"} if token is not None: headers["Authorization"] = f"Bearer {token}" - data = None + is_multipart_form_data = False + files = {} + values = {} if body is not None: - data = json.dumps(body) - - req = requests.request(method, url=url, data=data, headers=headers) + for k, v in body.items(): + if isinstance(v, io.BufferedReader): + mimetype, _ = mimetypes.guess_type(v.name) + files[k] = (v.name, v, mimetype) + is_multipart_form_data = True + else: + values[k] = v + + if is_multipart_form_data: + req = requests.request(method, url=url, files=files, data=values, headers=headers) + else: + req = requests.request(method, url=url, json=body, headers=headers) if req.ok: try: diff --git a/balena/types/models.py b/balena/types/models.py index f14e0518..323b249a 100644 --- a/balena/types/models.py +++ b/balena/types/models.py @@ -13,6 +13,14 @@ class PineDeferred(TypedDict): OptionalNavigationResource = Union[List[__T], PineDeferred, None] +class WebResource(TypedDict): + filename: str + href: str + content_type: str + content_disposition: str + size: int + + class UserType(TypedDict): id: int actor: ConceptTypeNavigationResource["ActorType"] @@ -354,6 +362,7 @@ class OrganizationType(TypedDict): created_at: str name: str handle: str + logo_image: WebResource has_past_due_invoice_since__date: str application: ReverseNavigationResource[TypeApplication] organization_membership: ReverseNavigationResource["OrganizationMembershipType"]