Skip to content

Commit

Permalink
Add support to multipart/form-data requests
Browse files Browse the repository at this point in the history
Change-type: minor
  • Loading branch information
otaviojacobi committed Nov 13, 2023
1 parent 0a2b341 commit 6a39d1f
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 15 deletions.
22 changes: 20 additions & 2 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)[<code>List[SSHKeyType]</code>](#sshkeytype)
- [remove(id)](#key.remove) ⇒ <code>None</code>
- [.organization](#organization)
- [create(name, handle)](#organization.create)[<code>OrganizationType</code>](#organizationtype)
- [create(name, handle, logo_image)](#organization.create)[<code>OrganizationType</code>](#organizationtype)
- [get(handle_or_id, options)](#organization.get)[<code>OrganizationType</code>](#organizationtype)
- [get_all(options)](#organization.get_all)[<code>List[OrganizationType]</code>](#organizationtype)
- [remove(handle_or_id)](#organization.remove) ⇒ <code>None</code>
Expand Down Expand Up @@ -2705,20 +2705,23 @@ Remove a ssh key.
This class implements organization model for balena python SDK.

<a name="organization.create"></a>
### Function: create(name, handle) ⇒ [<code>OrganizationType</code>](#organizationtype)
### Function: create(name, handle, logo_image) ⇒ [<code>OrganizationType</code>](#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:
```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)
```

<a name="organization.get"></a>
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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
}
```


16 changes: 12 additions & 4 deletions balena/models/organization.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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]:
Expand Down
26 changes: 19 additions & 7 deletions balena/pine.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
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
import os
from pine_client import PinejsClientCore
from pine_client.client import Params

Expand Down Expand Up @@ -40,15 +41,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] = (os.path.basename(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:
Expand Down
9 changes: 9 additions & 0 deletions balena/types/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"]
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 10 additions & 2 deletions tests/functional/models/test_organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,19 @@ def test_create(self):
self.assertEqual(TestOrganization.org2["name"], self.test_org_name)
self.assertEqual(TestOrganization.org2["handle"], self.test_org_custom_handle)

# should be able to create a new organization with the same name
TestOrganization.org3 = self.balena.models.organization.create(self.test_org_name)
# should be able to create a new organization with the same name & a file
filename = 'test-data/balena-python-sdk-test-logo.png'
with open(filename, 'rb') as f:
TestOrganization.org3 = self.balena.models.organization.create(self.test_org_name, None, f)

self.assertEqual(TestOrganization.org3["name"], self.test_org_name)
self.assertNotEqual(TestOrganization.org3["handle"], TestOrganization.org1["handle"])

org_with_logo = self.balena.models.organization.get(TestOrganization.org3["id"], {"$select": ["id", "logo_image"]})
self.assertIsInstance(org_with_logo["logo_image"]["href"], str)
self.assertGreater(len(org_with_logo["logo_image"]["href"]), 0)
self.assertEqual(org_with_logo["logo_image"]["filename"], 'balena-python-sdk-test-logo.png')

def test_get_all(self):
# given three extra non-user organization, should retrieve all organizations.
orgs = self.balena.models.organization.get_all()
Expand Down

0 comments on commit 6a39d1f

Please sign in to comment.