Skip to content

Commit 7c3f587

Browse files
authored
Merge pull request #35 from Frameio/circleci-project-setup
DEVREL-234, DEVREL-235, DEVREL-250: CircleCI setup, integration testing, improved dev tooling, and CODEOWNER setup
2 parents 825d497 + 43f77d1 commit 7c3f587

File tree

12 files changed

+496
-52
lines changed

12 files changed

+496
-52
lines changed

.bumpversion.cfg

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[bumpversion]
2+
current_version = 0.7.5
3+
commit = True
4+
tag = True
5+
6+
[bumpversion:file:setup.py]

.circleci/config.yml

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
version: 2.1
2+
3+
orbs:
4+
python: circleci/[email protected]
5+
win: circleci/[email protected]
6+
7+
workflows:
8+
version: 2
9+
build_test_deploy:
10+
jobs:
11+
- build
12+
13+
- linux_py_27_17:
14+
requires:
15+
- build
16+
17+
- linux_py_27_latest:
18+
requires:
19+
- build
20+
21+
- linux_py_37:
22+
requires:
23+
- build
24+
25+
- linux_py_latest:
26+
requires:
27+
- build
28+
29+
- deploy:
30+
requires:
31+
- linux_py_27_17
32+
- linux_py_27_latest
33+
- linux_py_37
34+
- linux_py_latest
35+
36+
filters:
37+
branches:
38+
only:
39+
- master
40+
tags:
41+
only: /[0-9]+(\.[0-9]+)*/
42+
43+
jobs:
44+
build:
45+
docker:
46+
- image: circleci/python:latest
47+
steps:
48+
- checkout:
49+
name: Checkout Git
50+
51+
- run:
52+
name: Build Package
53+
command: |
54+
echo -e "Running sdist"
55+
python setup.py sdist
56+
echo -e "Creating wheel"
57+
python setup.py bdist_wheel --universal
58+
59+
- persist_to_workspace:
60+
root: /home/circleci/project/
61+
paths:
62+
- .
63+
64+
linux_py_27_17:
65+
description: Linux 2.7.17
66+
docker:
67+
- image: circleci/python:2.7.17
68+
69+
steps:
70+
- attach_workspace:
71+
at: /tmp/artifact
72+
name: Attach build artifact
73+
74+
- run:
75+
name: Install package
76+
command: |
77+
pip install '/tmp/artifact'
78+
79+
- run:
80+
name: Run integration test
81+
command: |
82+
python /tmp/artifact/tests/integration.py
83+
84+
linux_py_27_latest:
85+
description: Linux 2.7.18
86+
docker:
87+
- image: circleci/python:2.7.18
88+
89+
steps:
90+
- attach_workspace:
91+
at: /tmp/artifact
92+
name: Attach build artifact
93+
94+
- run:
95+
name: Install package
96+
command: |
97+
pip install '/tmp/artifact'
98+
99+
- run:
100+
name: Run integration test
101+
command: |
102+
python /tmp/artifact/tests/integration.py
103+
104+
linux_py_37:
105+
description: Linux 3.7.7
106+
docker:
107+
- image: circleci/python:3.7.7
108+
109+
steps:
110+
- attach_workspace:
111+
at: /tmp/artifact
112+
name: Attach build artifact
113+
114+
- run:
115+
name: Install package
116+
command: |
117+
pip install '/tmp/artifact'
118+
119+
- run:
120+
name: Run integration test
121+
command: |
122+
python /tmp/artifact/tests/integration.py
123+
124+
linux_py_latest:
125+
description: Linux latest
126+
docker:
127+
- image: circleci/python:latest
128+
129+
steps:
130+
- attach_workspace:
131+
at: /tmp/artifact
132+
name: Attach build artifact
133+
134+
- run:
135+
name: Install package
136+
command: |
137+
pip install '/tmp/artifact'
138+
139+
- run:
140+
name: Run integration test
141+
command: |
142+
python /tmp/artifact/tests/integration.py
143+
144+
145+
deploy:
146+
docker:
147+
- image: circleci/python:latest
148+
149+
steps:
150+
- attach_workspace:
151+
at: /tmp/artifact
152+
name: Attach build artifact
153+
154+
- run:
155+
name: Install dependencies
156+
command: |
157+
pip install setuptools wheel twine
158+
159+
- run:
160+
name: init .pypirc
161+
command: |
162+
cd /tmp/artifact
163+
echo -e "[pypi]" >> ~/.pypirc
164+
echo -e "username = $TWINE_USERNAME" >> ~/.pypirc
165+
echo -e "password = $TWINE_PASSWORD" >> ~/.pypirc
166+
167+
- run:
168+
name: Upload to pypi
169+
command: |
170+
cd /tmp/artifact
171+
. venv/bin/activate
172+
twine upload dist/
173+
174+

.github/CODEOWNERS

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# These owners will be the default owners for everything in
2+
# the repo. Unless a later match takes precedence,
3+
# @global-owner1 and @global-owner2 will be requested for
4+
# review when someone opens a pull request.
5+
* @jhodges10
6+
7+
# Order is important; the last matching pattern takes the most
8+
# precedence. When someone opens a pull request that only
9+
# modifies Python files.
10+

.github/workflows/pythonpublish.yml

Lines changed: 0 additions & 31 deletions
This file was deleted.

Makefile

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/sh
2+
3+
install-dev:
4+
pip3 install -e .[dev]
5+
curl -fLSs https://raw.githubusercontent.com/CircleCI-Public/circleci-cli/master/install.sh | bash
6+
7+
bump-minor:
8+
bump2version minor
9+
10+
bump-major:
11+
bump2version major
12+
13+
bump-patch:
14+
bump2version patch

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ _Note: The Frame.io Python client may not work correctly in Python 3.8+_
3232

3333
## Usage
3434

35-
_Note: A valid token is required to make requests to Frame.io. Please contact platform@frame.io to get setup._
35+
_Note: A valid token is required to make requests to Frame.io. Go to our [Developer Portal](https://developer.frame.io/) to get a token!_
3636

3737
In addition to the snippets below, examples are included in [/examples](/examples).
3838

frameioclient/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
from .client import FrameioClient
2+
from .utils import stream

frameioclient/client.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import sys
12
import requests
2-
from .upload import FrameioUploader
33
from requests.adapters import HTTPAdapter
44
from requests.packages.urllib3.util.retry import Retry
55
from .download import FrameioDownloader
66

7+
if sys.version_info.major >= 3:
8+
from .py3_uploader import FrameioUploader
9+
else:
10+
from .py2_uploader import FrameioUploader
11+
712
class PaginatedResponse(object):
813
def __init__(self, results=[], page=0, page_size=0, total=0, total_pages=0):
914
super(PaginatedResponse, self).__init__()
@@ -26,12 +31,23 @@ def __init__(self, token, host='https://api.frame.io'):
2631
status_forcelist=[429],
2732
method_whitelist=["POST", "OPTIONS", "GET"]
2833
)
34+
self.client_version = self._get_version()
35+
36+
def _get_version(self):
37+
try:
38+
from importlib import metadata
39+
except ImportError:
40+
# Running on pre-3.8 Python; use importlib-metadata package
41+
import importlib_metadata as metadata
42+
43+
return metadata.version('frameioclient')
2944

3045
def _api_call(self, method, endpoint, payload={}):
3146
url = '{}/v2{}'.format(self.host, endpoint)
3247

3348
headers = {
34-
'Authorization': 'Bearer {}'.format(self.token)
49+
'Authorization': 'Bearer {}'.format(self.token),
50+
'x-frameio-client': 'python/{}'.format(self.client_version)
3551
}
3652

3753
adapter = HTTPAdapter(max_retries=self.retry_strategy)
@@ -48,13 +64,14 @@ def _api_call(self, method, endpoint, payload={}):
4864

4965
if r.ok:
5066
if r.headers.get('page-number'):
51-
return PaginatedResponse(
52-
results=r.json(),
53-
page=r.headers['page-number'],
54-
page_size=r.headers['per-page'],
55-
total_pages=r.headers['total-pages'],
56-
total=r.headers['total']
57-
)
67+
if int(r.headers.get('total-pages')) > 1:
68+
return PaginatedResponse(
69+
results=r.json(),
70+
page=r.headers['page-number'],
71+
page_size=r.headers['per-page'],
72+
total_pages=r.headers['total-pages'],
73+
total=r.headers['total']
74+
)
5875

5976
return r.json()
6077

@@ -220,7 +237,7 @@ def update_asset(self, asset_id, **kwargs):
220237
Example::
221238
client.update_asset("adeffee123342", name="updated_filename.mp4")
222239
"""
223-
endpoint = '/assets/{}/children'.format(asset_id)
240+
endpoint = '/assets/{}'.format(asset_id)
224241
return self._api_call('put', endpoint, kwargs)
225242

226243
def upload(self, asset, file):
@@ -260,7 +277,7 @@ def get_comment(self, comment_id, **kwargs):
260277
:Args:
261278
comment_id (string): The comment id.
262279
"""
263-
endpoint = '/comments/{id}'.format(comment_id)
280+
endpoint = '/comments/{}'.format(comment_id)
264281
return self._api_call('get', endpoint, **kwargs)
265282

266283
def get_comments(self, asset_id, **kwargs):
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@ def upload(self):
2828
size = int(math.ceil(total_size / len(upload_urls)))
2929

3030
for i, chunk in enumerate(self._read_chunk(self.file, size)):
31-
proc = Process(target=self._upload_chunk, args=(upload_urls[i], chunk,))
32-
procs.append(proc)
33-
proc.start()
31+
try:
32+
proc = Process(target=self._upload_chunk, args=(upload_urls[i], chunk))
33+
procs.append(proc)
34+
proc.start()
35+
except IndexError:
36+
# In python 2.7 the _read_chunk function sometimes keeps going \
37+
# past where it should, this prevents that.
38+
pass
3439

3540
for proc in procs:
3641
proc.join()

frameioclient/py3_uploader.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import requests
2+
import math
3+
import concurrent.futures
4+
import threading
5+
6+
thread_local = threading.local()
7+
8+
class FrameioUploader(object):
9+
def __init__(self, asset, file):
10+
self.asset = asset
11+
self.file = file
12+
13+
def _read_chunk(self, file, size):
14+
while True:
15+
data = file.read(size)
16+
if not data:
17+
break
18+
yield data
19+
20+
def _get_session(self):
21+
if not hasattr(thread_local, "session"):
22+
thread_local.session = requests.Session()
23+
return thread_local.session
24+
25+
def _upload_chunk(self, task):
26+
url = task[0]
27+
chunk = task[1]
28+
session = self._get_session()
29+
30+
session.put(url, data=chunk, headers={
31+
'content-type': self.asset['filetype'],
32+
'x-amz-acl': 'private'
33+
})
34+
35+
def upload(self):
36+
total_size = self.asset['filesize']
37+
upload_urls = self.asset['upload_urls']
38+
size = int(math.ceil(total_size / len(upload_urls)))
39+
40+
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
41+
for i, chunk in enumerate(self._read_chunk(self.file, size)):
42+
task = (upload_urls[i], chunk)
43+
executor.submit(self._upload_chunk, task)

0 commit comments

Comments
 (0)