Skip to content

Commit a3772fa

Browse files
committed
Update security vulnerabilities (does not resolve issues with python 2.7) requires version bounds
Update Readmes for testing various versions and basic publishing guide
1 parent 7377487 commit a3772fa

10 files changed

Lines changed: 123 additions & 46 deletions

File tree

README.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,7 @@ You can upload a file to the created review with the review id, we provided one
161161
item_data = s.upload_file(review['id'], 'examples/test.webm')
162162
```
163163

164-
If all steps were successful, you should see the following in the web-app.
165-
166-
![alt text](https://github.com/syncsketch/python-api/blob/documentation/examples/resources/exampleResult.jpg?raw=true)
164+
If all steps were successful, you should see the new item under the review in the web-app.
167165

168166
### Additional Examples
169167

@@ -229,3 +227,29 @@ projects = s.get_projects()
229227
for project in projects['objects']:
230228
print(project)
231229
```
230+
231+
### Publishing a New Release
232+
233+
1. Update the version in both `setup.py` and `syncsketch/__init__.py` (keep them in sync).
234+
235+
2. Build the distribution:
236+
```bash
237+
python -m build
238+
```
239+
240+
3. Verify the build artifacts in `dist/`:
241+
```bash
242+
ls dist/syncsketch-<version>*
243+
```
244+
245+
4. Upload to PyPI:
246+
```bash
247+
python -m twine upload dist/syncsketch-<version>*
248+
```
249+
250+
To test with TestPyPI first:
251+
```bash
252+
python -m twine upload --repository testpypi dist/syncsketch-<version>*
253+
```
254+
255+
Requires the `build` and `twine` packages (`pip install build twine`).

build/lib/syncsketch/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@
66

77
from __future__ import absolute_import
88

9+
import sys
10+
import warnings
11+
12+
if sys.version_info < (3, 8):
13+
warnings.warn(
14+
"SyncSketch: Python %d.%d is deprecated. "
15+
"New features will only target Python 3.8+. "
16+
"Please upgrade." % (sys.version_info[0], sys.version_info[1]),
17+
DeprecationWarning,
18+
stacklevel=2,
19+
)
20+
921
from .syncsketch import SyncSketchAPI
1022

1123
__version__ = "1.0.12.0"

build/lib/syncsketch/syncsketch.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def _worker(self):
5050
try:
5151
task_id, fn, args, kwargs = self.tasks.get(block=False)
5252
self.results[task_id] = fn(*args, **kwargs)
53-
except:
53+
except Exception:
5454
pass
5555

5656
def __enter__(self):
@@ -130,6 +130,13 @@ def __init__(
130130
def get_api_base_url(self, api_version=None):
131131
return self.join_url_path(self.HOST, "/api/{}/".format(api_version or self.api_version))
132132

133+
_SENSITIVE_KEYS = frozenset({"api_key", "token", "username", "email", "Authorization"})
134+
135+
@staticmethod
136+
def _redact_dict(d):
137+
"""Return a copy of dict *d* with sensitive values replaced by '***'."""
138+
return {k: ("***" if k in SyncSketchAPI._SENSITIVE_KEYS else v) for k, v in d.items()}
139+
133140
@staticmethod
134141
def join_url_path(base, *path_segments):
135142
"""Takes one more more strings and returns a properly terminated url path. Handles strings regardless
@@ -206,8 +213,8 @@ def _get_json_response(
206213
"{method} URL: {url}, params: {params}, headers: {headers}, status_code: {status_code}".format(
207214
method=method,
208215
url=url,
209-
params=params,
210-
headers=headers,
216+
params=self._redact_dict(params),
217+
headers=self._redact_dict(headers),
211218
status_code=r.status_code,
212219
)
213220
)
@@ -221,8 +228,7 @@ def _get_json_response(
221228
except Exception as e:
222229
if self.debug:
223230
print(e)
224-
225-
print("Error: %s" % r.text)
231+
print("Error: %s" % r.text)
226232

227233
return {"objects": []}
228234

@@ -958,22 +964,22 @@ def add_media(
958964
if itemParentId:
959965
get_params.update({"itemParentId": itemParentId})
960966

961-
uploadURL = "%s/items/uploadToReview/%s/?%s" % (
967+
uploadURL = "%s/items/uploadToReview/%s/" % (
962968
self.HOST,
963969
review_id,
964-
urlencode(get_params),
965970
)
966971

967972
files = {"reviewFile": open(filepath, "rb")}
968973
r = requests.post(
969974
uploadURL,
975+
params=get_params,
970976
files=files,
971977
data=dict(artist=artist_name, name=file_name),
972978
headers=self.headers,
973979
)
974980

975981
if self.debug:
976-
print("URL: %s, params: %s" % (uploadURL, get_params))
982+
print("URL: %s, params: %s" % (uploadURL, self._redact_dict(get_params)))
977983

978984
try:
979985
return json.loads(r.text)
@@ -1002,15 +1008,15 @@ def add_media_by_url(self, review_id, media_url, artist_name="", noConvertFlag=F
10021008
if noConvertFlag:
10031009
get_params.update({"noConvertFlag": 1})
10041010

1005-
upload_url = "%s/items/uploadToReview/%s/?%s" % (
1011+
upload_url = "%s/items/uploadToReview/%s/" % (
10061012
self.HOST,
10071013
review_id,
1008-
urlencode(get_params),
10091014
)
10101015

10111016
r = requests.post(
10121017
upload_url,
1013-
{"media_url": media_url, "artist": artist_name},
1018+
params=get_params,
1019+
data={"media_url": media_url, "artist": artist_name},
10141020
headers=self.headers,
10151021
)
10161022

@@ -1411,8 +1417,7 @@ def _get_s3_signed_url(
14111417
"""
14121418
Internal method. Use to retrieve s3 signed url for file upload in `add_media_via_s3`.
14131419
"""
1414-
request_data = self.api_params.copy()
1415-
additional_request_data = {
1420+
post_data = {
14161421
"review_id": review_id,
14171422
"item_name": item_name,
14181423
"item_data": {
@@ -1422,13 +1427,12 @@ def _get_s3_signed_url(
14221427
"noConvertFlag": no_convert,
14231428
},
14241429
}
1425-
request_data.update(additional_request_data)
14261430

14271431
request_url = "{}/uploads/get-s3-signed-url/".format(self.HOST)
14281432

14291433
return self._get_json_response(
14301434
url=request_url,
1431-
postData=request_data,
1435+
postData=post_data,
14321436
raw_response=raw_response,
14331437
)
14341438

@@ -1721,10 +1725,11 @@ def get_grease_pencil_overlays(self, review_id, item_id, homedir=None):
17211725
if result.get("status") == "done":
17221726
data = result.get("data")
17231727

1724-
# storing locally
1725-
local_filename = "/tmp/%s.zip" % data["fileName"]
1728+
# storing locally - sanitize fileName to prevent path traversal
1729+
safe_name = os.path.basename(data["fileName"])
1730+
local_filename = "/tmp/%s.zip" % safe_name
17261731
if homedir:
1727-
local_filename = os.path.join(homedir, "{}.zip".format(data["fileName"]))
1732+
local_filename = os.path.join(homedir, "{}.zip".format(safe_name))
17281733
r = requests.get(data["s3Path"], stream=True)
17291734
with open(local_filename, "wb") as f:
17301735
for chunk in r.iter_content(chunk_size=1024):
@@ -1788,7 +1793,7 @@ def get_user_by_email(self, email, fields=None, raw_response=True):
17881793
try:
17891794
data = response.json()
17901795
return data.get("objects")[0]
1791-
except:
1796+
except Exception:
17921797
return None
17931798

17941799
def get_users_by_project_id(self, project_id, raw_response=False):
217 Bytes
Binary file not shown.

dist/syncsketch-1.0.12.0.tar.gz

322 Bytes
Binary file not shown.

setup.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,15 @@
3636
long_description_content_type="text/markdown",
3737
url="https://github.com/syncsketch/python-api",
3838
packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
39-
install_requires=["requests>=2.20.0"],
39+
install_requires=[
40+
'requests>=2.20.0,<2.28; python_version < "3.0"',
41+
'requests>=2.20.0; python_version >= "3.7" and python_version < "3.9"',
42+
'requests>=2.32.0; python_version >= "3.9"',
43+
'urllib3>=2.6.3; python_version >= "3.9"',
44+
],
4045
extras_require={
4146
"test": [
42-
"pytest>=7.0,<9.0",
47+
"pytest>=9.0.3,<10.0",
4348
"pytest-cov>=4.0",
4449
"responses>=0.20.0",
4550
],

syncsketch.egg-info/PKG-INFO

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@ Classifier: Programming Language :: Python :: 3.14
2323
Requires-Python: >=2.7, <3.15
2424
Description-Content-Type: text/markdown
2525
License-File: LICENSE
26-
Requires-Dist: requests>=2.20.0
26+
Requires-Dist: requests<2.28,>=2.20.0; python_version < "3.0"
27+
Requires-Dist: requests>=2.20.0; python_version >= "3.7" and python_version < "3.9"
28+
Requires-Dist: requests>=2.32.0; python_version >= "3.9"
29+
Requires-Dist: urllib3>=2.6.3; python_version >= "3.9"
2730
Provides-Extra: test
28-
Requires-Dist: pytest<9.0,>=7.0; extra == "test"
31+
Requires-Dist: pytest<10.0,>=9.0.3; extra == "test"
2932
Requires-Dist: pytest-cov>=4.0; extra == "test"
3033
Requires-Dist: responses>=0.20.0; extra == "test"
3134
Dynamic: author

syncsketch.egg-info/requires.txt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
2+
[:python_version < "3.0"]
3+
requests<2.28,>=2.20.0
4+
5+
[:python_version >= "3.7" and python_version < "3.9"]
16
requests>=2.20.0
27

8+
[:python_version >= "3.9"]
9+
requests>=2.32.0
10+
urllib3>=2.6.3
11+
312
[test]
4-
pytest<9.0,>=7.0
13+
pytest<10.0,>=9.0.3
514
pytest-cov>=4.0
615
responses>=0.20.0

syncsketch/syncsketch.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def _worker(self):
5050
try:
5151
task_id, fn, args, kwargs = self.tasks.get(block=False)
5252
self.results[task_id] = fn(*args, **kwargs)
53-
except:
53+
except Exception:
5454
pass
5555

5656
def __enter__(self):
@@ -130,6 +130,13 @@ def __init__(
130130
def get_api_base_url(self, api_version=None):
131131
return self.join_url_path(self.HOST, "/api/{}/".format(api_version or self.api_version))
132132

133+
_SENSITIVE_KEYS = frozenset({"api_key", "token", "username", "email", "Authorization"})
134+
135+
@staticmethod
136+
def _redact_dict(d):
137+
"""Return a copy of dict *d* with sensitive values replaced by '***'."""
138+
return {k: ("***" if k in SyncSketchAPI._SENSITIVE_KEYS else v) for k, v in d.items()}
139+
133140
@staticmethod
134141
def join_url_path(base, *path_segments):
135142
"""Takes one more more strings and returns a properly terminated url path. Handles strings regardless
@@ -206,8 +213,8 @@ def _get_json_response(
206213
"{method} URL: {url}, params: {params}, headers: {headers}, status_code: {status_code}".format(
207214
method=method,
208215
url=url,
209-
params=params,
210-
headers=headers,
216+
params=self._redact_dict(params),
217+
headers=self._redact_dict(headers),
211218
status_code=r.status_code,
212219
)
213220
)
@@ -221,8 +228,7 @@ def _get_json_response(
221228
except Exception as e:
222229
if self.debug:
223230
print(e)
224-
225-
print("Error: %s" % r.text)
231+
print("Error: %s" % r.text)
226232

227233
return {"objects": []}
228234

@@ -958,22 +964,22 @@ def add_media(
958964
if itemParentId:
959965
get_params.update({"itemParentId": itemParentId})
960966

961-
uploadURL = "%s/items/uploadToReview/%s/?%s" % (
967+
uploadURL = "%s/items/uploadToReview/%s/" % (
962968
self.HOST,
963969
review_id,
964-
urlencode(get_params),
965970
)
966971

967972
files = {"reviewFile": open(filepath, "rb")}
968973
r = requests.post(
969974
uploadURL,
975+
params=get_params,
970976
files=files,
971977
data=dict(artist=artist_name, name=file_name),
972978
headers=self.headers,
973979
)
974980

975981
if self.debug:
976-
print("URL: %s, params: %s" % (uploadURL, get_params))
982+
print("URL: %s, params: %s" % (uploadURL, self._redact_dict(get_params)))
977983

978984
try:
979985
return json.loads(r.text)
@@ -1002,15 +1008,15 @@ def add_media_by_url(self, review_id, media_url, artist_name="", noConvertFlag=F
10021008
if noConvertFlag:
10031009
get_params.update({"noConvertFlag": 1})
10041010

1005-
upload_url = "%s/items/uploadToReview/%s/?%s" % (
1011+
upload_url = "%s/items/uploadToReview/%s/" % (
10061012
self.HOST,
10071013
review_id,
1008-
urlencode(get_params),
10091014
)
10101015

10111016
r = requests.post(
10121017
upload_url,
1013-
{"media_url": media_url, "artist": artist_name},
1018+
params=get_params,
1019+
data={"media_url": media_url, "artist": artist_name},
10141020
headers=self.headers,
10151021
)
10161022

@@ -1411,8 +1417,7 @@ def _get_s3_signed_url(
14111417
"""
14121418
Internal method. Use to retrieve s3 signed url for file upload in `add_media_via_s3`.
14131419
"""
1414-
request_data = self.api_params.copy()
1415-
additional_request_data = {
1420+
post_data = {
14161421
"review_id": review_id,
14171422
"item_name": item_name,
14181423
"item_data": {
@@ -1422,13 +1427,12 @@ def _get_s3_signed_url(
14221427
"noConvertFlag": no_convert,
14231428
},
14241429
}
1425-
request_data.update(additional_request_data)
14261430

14271431
request_url = "{}/uploads/get-s3-signed-url/".format(self.HOST)
14281432

14291433
return self._get_json_response(
14301434
url=request_url,
1431-
postData=request_data,
1435+
postData=post_data,
14321436
raw_response=raw_response,
14331437
)
14341438

@@ -1721,10 +1725,11 @@ def get_grease_pencil_overlays(self, review_id, item_id, homedir=None):
17211725
if result.get("status") == "done":
17221726
data = result.get("data")
17231727

1724-
# storing locally
1725-
local_filename = "/tmp/%s.zip" % data["fileName"]
1728+
# storing locally - sanitize fileName to prevent path traversal
1729+
safe_name = os.path.basename(data["fileName"])
1730+
local_filename = "/tmp/%s.zip" % safe_name
17261731
if homedir:
1727-
local_filename = os.path.join(homedir, "{}.zip".format(data["fileName"]))
1732+
local_filename = os.path.join(homedir, "{}.zip".format(safe_name))
17281733
r = requests.get(data["s3Path"], stream=True)
17291734
with open(local_filename, "wb") as f:
17301735
for chunk in r.iter_content(chunk_size=1024):
@@ -1788,7 +1793,7 @@ def get_user_by_email(self, email, fields=None, raw_response=True):
17881793
try:
17891794
data = response.json()
17901795
return data.get("objects")[0]
1791-
except:
1796+
except Exception:
17921797
return None
17931798

17941799
def get_users_by_project_id(self, project_id, raw_response=False):

0 commit comments

Comments
 (0)