Skip to content

Commit 450dc8b

Browse files
Merge pull request #9 from benchling/ddeutsch-blob-upload
docs: Add example for blob uploading BNCH-27939
2 parents c984b80 + 8b41eb6 commit 450dc8b

File tree

7 files changed

+228
-0
lines changed

7 files changed

+228
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ This repository contains examples of how to use Benchling's API.
55
- `sync_into_benchling/` shows how to import and register entities
66
- `sync_out_of_benchling/` shows how to export all registered entities modified after a certain timestamp
77
- `upload_results/` shows how to upload data from a plate reader as structured results
8+
- `blob_upload/` shows how to upload a blob attachment

blob_upload/Pipfile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[[source]]
2+
url = "https://pypi.org/simple"
3+
verify_ssl = true
4+
name = "pypi"
5+
6+
[packages]
7+
certifi = "==2021.5.30"
8+
chardet = "==4.0.0"
9+
idna = "==2.10"
10+
requests = "==2.25.1"
11+
urllib3 = "==1.26.6"
12+
click = "*"
13+
14+
[dev-packages]
15+
16+
[requires]
17+
python_version = "3.9"

blob_upload/Pipfile.lock

Lines changed: 69 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

blob_upload/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# **Upload Blobs To Benchling**
2+
3+
Example code for uploading blobs to Benchling, part of the [Uploading Instrument Results](https://docs.benchling.com/docs/example-creating-results) guide available on Benchling Developer Platform Documentation Page.
4+
5+
# How to run the script
6+
7+
- First, ask Benchling support to enable API access on your account, and create API credentials. Instructions: https://help.benchling.com/articles/2353570-access-the-benchling-api-enterprise
8+
- Install Python 3 and [Pipenv](https://docs.pipenv.org/en/latest/)
9+
- Install dependencies using `pipenv install`
10+
- Run `pipenv shell` to work in a virtualenv that includes the dependencies.
11+
- Run the script. For example:
12+
13+
```
14+
python blob_upload.py \
15+
--domain example.benchling.com \
16+
--api-key $YOUR_API_KEY \
17+
--filepath path/to/chromatogram_file
18+
```

blob_upload/blob_upload.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import json
2+
import os
3+
4+
import click
5+
import requests
6+
import time
7+
8+
from file_helpers import calculate_md5, encode_base64
9+
10+
CHUNK_SIZE_BYTES = int(10e6)
11+
12+
class BadRequestException(Exception):
13+
def __init__(self, message, rv):
14+
super(BadRequestException, self).__init__(message)
15+
self.rv = rv
16+
17+
18+
def api_post(domain, api_key, path, body):
19+
url = "https://{}/api/v2/{}".format(domain, path)
20+
rv = requests.post(url, json=body, auth=(api_key, ""))
21+
if rv.status_code >= 400:
22+
raise BadRequestException(
23+
"Server returned status {}. Response:\n{}".format(
24+
rv.status_code, json.dumps(rv.json())
25+
),
26+
rv,
27+
)
28+
return rv.json()
29+
30+
31+
@click.command()
32+
@click.option(
33+
"--domain",
34+
help="Domain name of your Benchling instance, e.g. example.benchling.com",
35+
required=True,
36+
)
37+
@click.option("--api-key", help="Your API key", required=True)
38+
@click.option("--filepath", help="Filepath of blob to upload", required=True)
39+
@click.option("--destination-filename", help="Name of file (omit to keep same name as source)", required=False)
40+
def main(
41+
domain,
42+
api_key,
43+
filepath,
44+
destination_filename,
45+
):
46+
name = destination_filename
47+
if name is None:
48+
name = os.path.basename(filepath)
49+
file_size = os.path.getsize(filepath)
50+
with open(filepath, "rb") as file:
51+
if file_size <= CHUNK_SIZE_BYTES:
52+
upload_single_part_blob(api_key, domain, file, name)
53+
else:
54+
upload_multi_part_blob(api_key, domain, file, name)
55+
56+
57+
def upload_single_part_blob(api_key, domain, file, name):
58+
file_contents = file.read()
59+
encoded64 = encode_base64(file_contents)
60+
md5 = calculate_md5(file_contents)
61+
res = api_post(domain, api_key, "blobs", {
62+
"data64": encoded64,
63+
"md5": md5,
64+
"mimeType": "application/octet-stream",
65+
"name": name,
66+
"type": "RAW_FILE",
67+
})
68+
assert(res["uploadStatus"] == "COMPLETE")
69+
print("Finished uploading {} with blob ID {}".format(
70+
res["name"], res["id"]
71+
))
72+
73+
74+
def upload_multi_part_blob(api_key, domain, file, name):
75+
chunk_producer = lambda chunk_size: file.read(chunk_size)
76+
start_blob = api_post(domain, api_key, "blobs:start-multipart-upload", {
77+
"mimeType": "application/octet-stream",
78+
"name": name,
79+
"type": "RAW_FILE",
80+
})
81+
part_number = 0
82+
blob_parts = []
83+
try:
84+
while True:
85+
cursor = chunk_producer(CHUNK_SIZE_BYTES)
86+
if not cursor:
87+
break
88+
part_number += 1
89+
encoded64 = encode_base64(cursor)
90+
md5 = calculate_md5(cursor)
91+
created_part = api_post(domain, api_key, "blobs/{}/parts".format(start_blob["id"]), {
92+
"data64": encoded64,
93+
"md5": md5,
94+
"partNumber": part_number,
95+
})
96+
blob_parts.append(created_part)
97+
api_post(domain, api_key, "blobs/{}:complete-upload".format(start_blob["id"]), {
98+
"parts": blob_parts
99+
})
100+
print("Completed uploading {} parts for blob {}".format(part_number, start_blob["id"]))
101+
except Exception as e:
102+
print("Error while uploading part {} for blob {}".format(part_number, start_blob["id"]))
103+
api_post(domain, api_key, "blobs/{}:abort-upload".format(start_blob["id"]), {})
104+
raise e
105+
106+
if __name__ == "__main__":
107+
main()

blob_upload/file_helpers.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import base64
2+
import hashlib
3+
4+
5+
def encode_base64(input: bytes, charset: str = "utf-8") -> str:
6+
file_bytes = base64.encodebytes(input)
7+
return str(file_bytes, charset)
8+
9+
10+
def calculate_md5(input: bytes) -> str:
11+
return hashlib.md5(input).hexdigest()

blob_upload/requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
certifi==2021.5.30
2+
chardet==4.0.0
3+
idna==2.10
4+
requests==2.25.1
5+
urllib3==1.26.6

0 commit comments

Comments
 (0)