diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..8826785 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,55 @@ +name: tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install -e ".[test]" + - name: Run tests + run: pytest tests/ --cov=syncsketch --cov-report=term-missing + + test-legacy: + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + python-version: ["3.7"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install -e ".[test]" + - name: Run tests + run: pytest tests/ --cov=syncsketch --cov-report=term-missing + + test-py27: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "2.7" + continue-on-error: true + - name: Install Python 2.7 via apt + run: | + sudo apt-get update + sudo apt-get install -y python2 + curl -sSL https://bootstrap.pypa.io/pip/2.7/get-pip.py -o get-pip.py + python2 get-pip.py + - name: Install requests + run: python2 -m pip install "requests>=2.20.0,<2.28.0" + - name: Run smoke tests + run: python2 tests/test_py27_smoke.py diff --git a/.gitignore b/.gitignore index 2fae3d1..ef37262 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ .idea/workspace.xml local_test.py __pycache__/ -*.pyc \ No newline at end of file +*.pyc +.tox/ +htmlcov/ +.coverage \ No newline at end of file diff --git a/README.md b/README.md index b08e2ca..fe334fe 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,17 @@ SyncSketch is a synchronized visual review tool for the Film/TV/Games industry. #### Compatibility This library was tested with and confirmed on python versions: -- 2.7.14+ -- 3.6 -- 3.7 +- 2.7.14+ (see note below) +- 3.7 (see note below) - 3.8 - 3.9 - 3.10 - 3.11 - 3.12 +- 3.13 +- 3.14 + +> **Python 2.7 & 3.7 Deprecation Notice:** Python 2.7 and 3.7 have reached end-of-life and are no longer actively supported by the Python community. While existing functionality in this library will continue to work on these versions, new features and improvements will only be tested against Python 3.8 and above. We recommend upgrading to a supported Python version. #### Installation @@ -158,9 +161,7 @@ You can upload a file to the created review with the review id, we provided one item_data = s.upload_file(review['id'], 'examples/test.webm') ``` -If all steps were successful, you should see the following in the web-app. - -![alt text](https://github.com/syncsketch/python-api/blob/documentation/examples/resources/exampleResult.jpg?raw=true) +If all steps were successful, you should see the new item under the review in the web-app. ### Additional Examples @@ -226,3 +227,29 @@ projects = s.get_projects() for project in projects['objects']: print(project) ``` + +### Publishing a New Release + +1. Update the version in both `setup.py` and `syncsketch/__init__.py` (keep them in sync). + +2. Build the distribution: +```bash +python -m build +``` + +3. Verify the build artifacts in `dist/`: +```bash +ls dist/syncsketch-* +``` + +4. Upload to PyPI: +```bash +python -m twine upload dist/syncsketch-* +``` + +To test with TestPyPI first: +```bash +python -m twine upload --repository testpypi dist/syncsketch-* +``` + +Requires the `build` and `twine` packages (`pip install build twine`). diff --git a/_build/.buildinfo b/_build/.buildinfo index 3ef5a91..2ea5174 100644 --- a/_build/.buildinfo +++ b/_build/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 92f8d6ac56ccab42ac73d860c02e5024 +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. +config: ad357e14ce56b1b9f762fda6051cd040 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/_build/.doctrees/environment.pickle b/_build/.doctrees/environment.pickle index 06c5eab..d682858 100644 Binary files a/_build/.doctrees/environment.pickle and b/_build/.doctrees/environment.pickle differ diff --git a/_build/.doctrees/index.doctree b/_build/.doctrees/index.doctree index 49bf696..6363b64 100644 Binary files a/_build/.doctrees/index.doctree and b/_build/.doctrees/index.doctree differ diff --git a/_build/_static/alabaster.css b/_build/_static/alabaster.css index e3174bf..7e75bf8 100644 --- a/_build/_static/alabaster.css +++ b/_build/_static/alabaster.css @@ -1,5 +1,3 @@ -@import url("basic.css"); - /* -- page layout ----------------------------------------------------------- */ body { @@ -160,8 +158,8 @@ div.sphinxsidebar input { font-size: 1em; } -div.sphinxsidebar #searchbox input[type="text"] { - width: 160px; +div.sphinxsidebar #searchbox { + margin: 1em 0; } div.sphinxsidebar .search > div { @@ -263,10 +261,6 @@ div.admonition p.last { margin-bottom: 0; } -div.highlight { - background-color: #fff; -} - dt:target, .highlight { background: #FAF3E8; } @@ -454,7 +448,7 @@ ul, ol { } pre { - background: #EEE; + background: unset; padding: 7px 30px; margin: 15px 0px; line-height: 1.3em; @@ -485,15 +479,15 @@ a.reference { border-bottom: 1px dotted #004B6B; } +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + /* Don't put an underline on images */ a.image-reference, a.image-reference:hover { border-bottom: none; } -a.reference:hover { - border-bottom: 1px solid #6D4100; -} - a.footnote-reference { text-decoration: none; font-size: 0.7em; @@ -509,68 +503,7 @@ a:hover tt, a:hover code { background: #EEE; } - -@media screen and (max-width: 870px) { - - div.sphinxsidebar { - display: none; - } - - div.document { - width: 100%; - - } - - div.documentwrapper { - margin-left: 0; - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - } - - div.bodywrapper { - margin-top: 0; - margin-right: 0; - margin-bottom: 0; - margin-left: 0; - } - - ul { - margin-left: 0; - } - - li > ul { - /* Matches the 30px from the "ul, ol" selector above */ - margin-left: 30px; - } - - .document { - width: auto; - } - - .footer { - width: auto; - } - - .bodywrapper { - margin: 0; - } - - .footer { - width: auto; - } - - .github { - display: none; - } - - - -} - - - -@media screen and (max-width: 875px) { +@media screen and (max-width: 940px) { body { margin: 0; @@ -580,12 +513,16 @@ a:hover tt, a:hover code { div.documentwrapper { float: none; background: #fff; + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; } div.sphinxsidebar { display: block; float: none; - width: 102.5%; + width: unset; margin: 50px -30px -20px -30px; padding: 10px 20px; background: #333; @@ -620,8 +557,14 @@ a:hover tt, a:hover code { div.body { min-height: 0; + min-width: auto; /* fixes width on small screens, breaks .hll */ padding: 0; } + + .hll { + /* "fixes" the breakage */ + width: max-content; + } .rtd_doc_footer { display: none; @@ -635,13 +578,18 @@ a:hover tt, a:hover code { width: auto; } - .footer { - width: auto; - } - .github { display: none; } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } } @@ -705,4 +653,11 @@ nav#breadcrumbs li+li:before { div.related { display: none; } +} + +img.github { + position: absolute; + top: 0; + border: 0; + right: 0; } \ No newline at end of file diff --git a/_build/_static/base-stemmer.js b/_build/_static/base-stemmer.js new file mode 100644 index 0000000..e6fa0c4 --- /dev/null +++ b/_build/_static/base-stemmer.js @@ -0,0 +1,476 @@ +// @ts-check + +/**@constructor*/ +BaseStemmer = function() { + /** @protected */ + this.current = ''; + this.cursor = 0; + this.limit = 0; + this.limit_backward = 0; + this.bra = 0; + this.ket = 0; + + /** + * @param {string} value + */ + this.setCurrent = function(value) { + this.current = value; + this.cursor = 0; + this.limit = this.current.length; + this.limit_backward = 0; + this.bra = this.cursor; + this.ket = this.limit; + }; + + /** + * @return {string} + */ + this.getCurrent = function() { + return this.current; + }; + + /** + * @param {BaseStemmer} other + */ + this.copy_from = function(other) { + /** @protected */ + this.current = other.current; + this.cursor = other.cursor; + this.limit = other.limit; + this.limit_backward = other.limit_backward; + this.bra = other.bra; + this.ket = other.ket; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.in_grouping = function(s, min, max) { + /** @protected */ + if (this.cursor >= this.limit) return false; + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) return false; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false; + this.cursor++; + return true; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_in_grouping = function(s, min, max) { + /** @protected */ + while (this.cursor < this.limit) { + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) + return true; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) + return true; + this.cursor++; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.in_grouping_b = function(s, min, max) { + /** @protected */ + if (this.cursor <= this.limit_backward) return false; + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) return false; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return false; + this.cursor--; + return true; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_in_grouping_b = function(s, min, max) { + /** @protected */ + while (this.cursor > this.limit_backward) { + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) return true; + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) return true; + this.cursor--; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.out_grouping = function(s, min, max) { + /** @protected */ + if (this.cursor >= this.limit) return false; + var ch = this.current.charCodeAt(this.cursor); + if (ch > max || ch < min) { + this.cursor++; + return true; + } + ch -= min; + if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) == 0) { + this.cursor++; + return true; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_out_grouping = function(s, min, max) { + /** @protected */ + while (this.cursor < this.limit) { + var ch = this.current.charCodeAt(this.cursor); + if (ch <= max && ch >= min) { + ch -= min; + if ((s[ch >>> 3] & (0X1 << (ch & 0x7))) != 0) { + return true; + } + } + this.cursor++; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.out_grouping_b = function(s, min, max) { + /** @protected */ + if (this.cursor <= this.limit_backward) return false; + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch > max || ch < min) { + this.cursor--; + return true; + } + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) == 0) { + this.cursor--; + return true; + } + return false; + }; + + /** + * @param {number[]} s + * @param {number} min + * @param {number} max + * @return {boolean} + */ + this.go_out_grouping_b = function(s, min, max) { + /** @protected */ + while (this.cursor > this.limit_backward) { + var ch = this.current.charCodeAt(this.cursor - 1); + if (ch <= max && ch >= min) { + ch -= min; + if ((s[ch >>> 3] & (0x1 << (ch & 0x7))) != 0) { + return true; + } + } + this.cursor--; + } + return false; + }; + + /** + * @param {string} s + * @return {boolean} + */ + this.eq_s = function(s) + { + /** @protected */ + if (this.limit - this.cursor < s.length) return false; + if (this.current.slice(this.cursor, this.cursor + s.length) != s) + { + return false; + } + this.cursor += s.length; + return true; + }; + + /** + * @param {string} s + * @return {boolean} + */ + this.eq_s_b = function(s) + { + /** @protected */ + if (this.cursor - this.limit_backward < s.length) return false; + if (this.current.slice(this.cursor - s.length, this.cursor) != s) + { + return false; + } + this.cursor -= s.length; + return true; + }; + + /** + * @param {Among[]} v + * @return {number} + */ + this.find_among = function(v) + { + /** @protected */ + var i = 0; + var j = v.length; + + var c = this.cursor; + var l = this.limit; + + var common_i = 0; + var common_j = 0; + + var first_key_inspected = false; + + while (true) + { + var k = i + ((j - i) >>> 1); + var diff = 0; + var common = common_i < common_j ? common_i : common_j; // smaller + // w[0]: string, w[1]: substring_i, w[2]: result, w[3]: function (optional) + var w = v[k]; + var i2; + for (i2 = common; i2 < w[0].length; i2++) + { + if (c + common == l) + { + diff = -1; + break; + } + diff = this.current.charCodeAt(c + common) - w[0].charCodeAt(i2); + if (diff != 0) break; + common++; + } + if (diff < 0) + { + j = k; + common_j = common; + } + else + { + i = k; + common_i = common; + } + if (j - i <= 1) + { + if (i > 0) break; // v->s has been inspected + if (j == i) break; // only one item in v + + // - but now we need to go round once more to get + // v->s inspected. This looks messy, but is actually + // the optimal approach. + + if (first_key_inspected) break; + first_key_inspected = true; + } + } + do { + var w = v[i]; + if (common_i >= w[0].length) + { + this.cursor = c + w[0].length; + if (w.length < 4) return w[2]; + var res = w[3](this); + this.cursor = c + w[0].length; + if (res) return w[2]; + } + i = w[1]; + } while (i >= 0); + return 0; + }; + + // find_among_b is for backwards processing. Same comments apply + /** + * @param {Among[]} v + * @return {number} + */ + this.find_among_b = function(v) + { + /** @protected */ + var i = 0; + var j = v.length + + var c = this.cursor; + var lb = this.limit_backward; + + var common_i = 0; + var common_j = 0; + + var first_key_inspected = false; + + while (true) + { + var k = i + ((j - i) >> 1); + var diff = 0; + var common = common_i < common_j ? common_i : common_j; + var w = v[k]; + var i2; + for (i2 = w[0].length - 1 - common; i2 >= 0; i2--) + { + if (c - common == lb) + { + diff = -1; + break; + } + diff = this.current.charCodeAt(c - 1 - common) - w[0].charCodeAt(i2); + if (diff != 0) break; + common++; + } + if (diff < 0) + { + j = k; + common_j = common; + } + else + { + i = k; + common_i = common; + } + if (j - i <= 1) + { + if (i > 0) break; + if (j == i) break; + if (first_key_inspected) break; + first_key_inspected = true; + } + } + do { + var w = v[i]; + if (common_i >= w[0].length) + { + this.cursor = c - w[0].length; + if (w.length < 4) return w[2]; + var res = w[3](this); + this.cursor = c - w[0].length; + if (res) return w[2]; + } + i = w[1]; + } while (i >= 0); + return 0; + }; + + /* to replace chars between c_bra and c_ket in this.current by the + * chars in s. + */ + /** + * @param {number} c_bra + * @param {number} c_ket + * @param {string} s + * @return {number} + */ + this.replace_s = function(c_bra, c_ket, s) + { + /** @protected */ + var adjustment = s.length - (c_ket - c_bra); + this.current = this.current.slice(0, c_bra) + s + this.current.slice(c_ket); + this.limit += adjustment; + if (this.cursor >= c_ket) this.cursor += adjustment; + else if (this.cursor > c_bra) this.cursor = c_bra; + return adjustment; + }; + + /** + * @return {boolean} + */ + this.slice_check = function() + { + /** @protected */ + if (this.bra < 0 || + this.bra > this.ket || + this.ket > this.limit || + this.limit > this.current.length) + { + return false; + } + return true; + }; + + /** + * @param {number} c_bra + * @return {boolean} + */ + this.slice_from = function(s) + { + /** @protected */ + var result = false; + if (this.slice_check()) + { + this.replace_s(this.bra, this.ket, s); + result = true; + } + return result; + }; + + /** + * @return {boolean} + */ + this.slice_del = function() + { + /** @protected */ + return this.slice_from(""); + }; + + /** + * @param {number} c_bra + * @param {number} c_ket + * @param {string} s + */ + this.insert = function(c_bra, c_ket, s) + { + /** @protected */ + var adjustment = this.replace_s(c_bra, c_ket, s); + if (c_bra <= this.bra) this.bra += adjustment; + if (c_bra <= this.ket) this.ket += adjustment; + }; + + /** + * @return {string} + */ + this.slice_to = function() + { + /** @protected */ + var result = ''; + if (this.slice_check()) + { + result = this.current.slice(this.bra, this.ket); + } + return result; + }; + + /** + * @return {string} + */ + this.assign_to = function() + { + /** @protected */ + return this.current.slice(0, this.limit); + }; +}; diff --git a/_build/_static/basic.css b/_build/_static/basic.css index e5179b7..0028826 100644 --- a/_build/_static/basic.css +++ b/_build/_static/basic.css @@ -1,12 +1,5 @@ /* - * basic.css - * ~~~~~~~~~ - * * Sphinx stylesheet -- basic theme. - * - * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * */ /* -- main layout ----------------------------------------------------------- */ @@ -115,15 +108,11 @@ img { /* -- search page ----------------------------------------------------------- */ ul.search { - margin: 10px 0 0 20px; - padding: 0; + margin-top: 10px; } ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; + padding: 5px 0; } ul.search li a { @@ -752,14 +741,6 @@ abbr, acronym { cursor: help; } -.translated { - background-color: rgba(207, 255, 207, 0.2) -} - -.untranslated { - background-color: rgba(255, 207, 207, 0.2) -} - /* -- code displays --------------------------------------------------------- */ pre { diff --git a/_build/_static/doctools.js b/_build/_static/doctools.js index 4d67807..807cdb1 100644 --- a/_build/_static/doctools.js +++ b/_build/_static/doctools.js @@ -1,12 +1,5 @@ /* - * doctools.js - * ~~~~~~~~~~~ - * * Base JavaScript utilities for all Sphinx HTML documentation. - * - * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * */ "use strict"; @@ -66,7 +59,7 @@ const Documentation = { Object.assign(Documentation.TRANSLATIONS, catalog.messages); Documentation.PLURAL_EXPR = new Function( "n", - `return (${catalog.plural_expr})` + `return (${catalog.plural_expr})`, ); Documentation.LOCALE = catalog.locale; }, @@ -96,7 +89,7 @@ const Documentation = { const togglerElements = document.querySelectorAll("img.toggler"); togglerElements.forEach((el) => - el.addEventListener("click", (event) => toggler(event.currentTarget)) + el.addEventListener("click", (event) => toggler(event.currentTarget)), ); togglerElements.forEach((el) => (el.style.display = "")); if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); @@ -105,14 +98,15 @@ const Documentation = { initOnKeyListeners: () => { // only install a listener if it is really needed if ( - !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && - !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS + && !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS ) return; document.addEventListener("keydown", (event) => { // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) + return; // bail with special keys if (event.altKey || event.ctrlKey || event.metaKey) return; diff --git a/_build/_static/english-stemmer.js b/_build/_static/english-stemmer.js new file mode 100644 index 0000000..056760e --- /dev/null +++ b/_build/_static/english-stemmer.js @@ -0,0 +1,1066 @@ +// Generated from english.sbl by Snowball 3.0.1 - https://snowballstem.org/ + +/**@constructor*/ +var EnglishStemmer = function() { + var base = new BaseStemmer(); + + /** @const */ var a_0 = [ + ["arsen", -1, -1], + ["commun", -1, -1], + ["emerg", -1, -1], + ["gener", -1, -1], + ["later", -1, -1], + ["organ", -1, -1], + ["past", -1, -1], + ["univers", -1, -1] + ]; + + /** @const */ var a_1 = [ + ["'", -1, 1], + ["'s'", 0, 1], + ["'s", -1, 1] + ]; + + /** @const */ var a_2 = [ + ["ied", -1, 2], + ["s", -1, 3], + ["ies", 1, 2], + ["sses", 1, 1], + ["ss", 1, -1], + ["us", 1, -1] + ]; + + /** @const */ var a_3 = [ + ["succ", -1, 1], + ["proc", -1, 1], + ["exc", -1, 1] + ]; + + /** @const */ var a_4 = [ + ["even", -1, 2], + ["cann", -1, 2], + ["inn", -1, 2], + ["earr", -1, 2], + ["herr", -1, 2], + ["out", -1, 2], + ["y", -1, 1] + ]; + + /** @const */ var a_5 = [ + ["", -1, -1], + ["ed", 0, 2], + ["eed", 1, 1], + ["ing", 0, 3], + ["edly", 0, 2], + ["eedly", 4, 1], + ["ingly", 0, 2] + ]; + + /** @const */ var a_6 = [ + ["", -1, 3], + ["bb", 0, 2], + ["dd", 0, 2], + ["ff", 0, 2], + ["gg", 0, 2], + ["bl", 0, 1], + ["mm", 0, 2], + ["nn", 0, 2], + ["pp", 0, 2], + ["rr", 0, 2], + ["at", 0, 1], + ["tt", 0, 2], + ["iz", 0, 1] + ]; + + /** @const */ var a_7 = [ + ["anci", -1, 3], + ["enci", -1, 2], + ["ogi", -1, 14], + ["li", -1, 16], + ["bli", 3, 12], + ["abli", 4, 4], + ["alli", 3, 8], + ["fulli", 3, 9], + ["lessli", 3, 15], + ["ousli", 3, 10], + ["entli", 3, 5], + ["aliti", -1, 8], + ["biliti", -1, 12], + ["iviti", -1, 11], + ["tional", -1, 1], + ["ational", 14, 7], + ["alism", -1, 8], + ["ation", -1, 7], + ["ization", 17, 6], + ["izer", -1, 6], + ["ator", -1, 7], + ["iveness", -1, 11], + ["fulness", -1, 9], + ["ousness", -1, 10], + ["ogist", -1, 13] + ]; + + /** @const */ var a_8 = [ + ["icate", -1, 4], + ["ative", -1, 6], + ["alize", -1, 3], + ["iciti", -1, 4], + ["ical", -1, 4], + ["tional", -1, 1], + ["ational", 5, 2], + ["ful", -1, 5], + ["ness", -1, 5] + ]; + + /** @const */ var a_9 = [ + ["ic", -1, 1], + ["ance", -1, 1], + ["ence", -1, 1], + ["able", -1, 1], + ["ible", -1, 1], + ["ate", -1, 1], + ["ive", -1, 1], + ["ize", -1, 1], + ["iti", -1, 1], + ["al", -1, 1], + ["ism", -1, 1], + ["ion", -1, 2], + ["er", -1, 1], + ["ous", -1, 1], + ["ant", -1, 1], + ["ent", -1, 1], + ["ment", 15, 1], + ["ement", 16, 1] + ]; + + /** @const */ var a_10 = [ + ["e", -1, 1], + ["l", -1, 2] + ]; + + /** @const */ var a_11 = [ + ["andes", -1, -1], + ["atlas", -1, -1], + ["bias", -1, -1], + ["cosmos", -1, -1], + ["early", -1, 5], + ["gently", -1, 3], + ["howe", -1, -1], + ["idly", -1, 2], + ["news", -1, -1], + ["only", -1, 6], + ["singly", -1, 7], + ["skies", -1, 1], + ["sky", -1, -1], + ["ugly", -1, 4] + ]; + + /** @const */ var /** Array */ g_aeo = [17, 64]; + + /** @const */ var /** Array */ g_v = [17, 65, 16, 1]; + + /** @const */ var /** Array */ g_v_WXY = [1, 17, 65, 208, 1]; + + /** @const */ var /** Array */ g_valid_LI = [55, 141, 2]; + + var /** boolean */ B_Y_found = false; + var /** number */ I_p2 = 0; + var /** number */ I_p1 = 0; + + + /** @return {boolean} */ + function r_prelude() { + B_Y_found = false; + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + base.bra = base.cursor; + if (!(base.eq_s("'"))) + { + break lab0; + } + base.ket = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + base.cursor = v_1; + /** @const */ var /** number */ v_2 = base.cursor; + lab1: { + base.bra = base.cursor; + if (!(base.eq_s("y"))) + { + break lab1; + } + base.ket = base.cursor; + if (!base.slice_from("Y")) + { + return false; + } + B_Y_found = true; + } + base.cursor = v_2; + /** @const */ var /** number */ v_3 = base.cursor; + lab2: { + while(true) + { + /** @const */ var /** number */ v_4 = base.cursor; + lab3: { + golab4: while(true) + { + /** @const */ var /** number */ v_5 = base.cursor; + lab5: { + if (!(base.in_grouping(g_v, 97, 121))) + { + break lab5; + } + base.bra = base.cursor; + if (!(base.eq_s("y"))) + { + break lab5; + } + base.ket = base.cursor; + base.cursor = v_5; + break golab4; + } + base.cursor = v_5; + if (base.cursor >= base.limit) + { + break lab3; + } + base.cursor++; + } + if (!base.slice_from("Y")) + { + return false; + } + B_Y_found = true; + continue; + } + base.cursor = v_4; + break; + } + } + base.cursor = v_3; + return true; + }; + + /** @return {boolean} */ + function r_mark_regions() { + I_p1 = base.limit; + I_p2 = base.limit; + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + lab1: { + /** @const */ var /** number */ v_2 = base.cursor; + lab2: { + if (base.find_among(a_0) == 0) + { + break lab2; + } + break lab1; + } + base.cursor = v_2; + if (!base.go_out_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + if (!base.go_in_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + } + I_p1 = base.cursor; + if (!base.go_out_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + if (!base.go_in_grouping(g_v, 97, 121)) + { + break lab0; + } + base.cursor++; + I_p2 = base.cursor; + } + base.cursor = v_1; + return true; + }; + + /** @return {boolean} */ + function r_shortv() { + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.out_grouping_b(g_v_WXY, 89, 121))) + { + break lab1; + } + if (!(base.in_grouping_b(g_v, 97, 121))) + { + break lab1; + } + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + lab2: { + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab2; + } + if (!(base.in_grouping_b(g_v, 97, 121))) + { + break lab2; + } + if (base.cursor > base.limit_backward) + { + break lab2; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("past"))) + { + return false; + } + } + return true; + }; + + /** @return {boolean} */ + function r_R1() { + return I_p1 <= base.cursor; + }; + + /** @return {boolean} */ + function r_R2() { + return I_p2 <= base.cursor; + }; + + /** @return {boolean} */ + function r_Step_1a() { + var /** number */ among_var; + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab0: { + base.ket = base.cursor; + if (base.find_among_b(a_1) == 0) + { + base.cursor = base.limit - v_1; + break lab0; + } + base.bra = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + base.ket = base.cursor; + among_var = base.find_among_b(a_2); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + switch (among_var) { + case 1: + if (!base.slice_from("ss")) + { + return false; + } + break; + case 2: + lab1: { + /** @const */ var /** number */ v_2 = base.limit - base.cursor; + lab2: { + { + /** @const */ var /** number */ c1 = base.cursor - 2; + if (c1 < base.limit_backward) + { + break lab2; + } + base.cursor = c1; + } + if (!base.slice_from("i")) + { + return false; + } + break lab1; + } + base.cursor = base.limit - v_2; + if (!base.slice_from("ie")) + { + return false; + } + } + break; + case 3: + if (base.cursor <= base.limit_backward) + { + return false; + } + base.cursor--; + if (!base.go_out_grouping_b(g_v, 97, 121)) + { + return false; + } + base.cursor--; + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_1b() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_5); + base.bra = base.cursor; + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + switch (among_var) { + case 1: + /** @const */ var /** number */ v_2 = base.limit - base.cursor; + lab2: { + lab3: { + /** @const */ var /** number */ v_3 = base.limit - base.cursor; + lab4: { + if (base.find_among_b(a_3) == 0) + { + break lab4; + } + if (base.cursor > base.limit_backward) + { + break lab4; + } + break lab3; + } + base.cursor = base.limit - v_3; + if (!r_R1()) + { + break lab2; + } + if (!base.slice_from("ee")) + { + return false; + } + } + } + base.cursor = base.limit - v_2; + break; + case 2: + break lab1; + case 3: + among_var = base.find_among_b(a_4); + if (among_var == 0) + { + break lab1; + } + switch (among_var) { + case 1: + /** @const */ var /** number */ v_4 = base.limit - base.cursor; + if (!(base.out_grouping_b(g_v, 97, 121))) + { + break lab1; + } + if (base.cursor > base.limit_backward) + { + break lab1; + } + base.cursor = base.limit - v_4; + base.bra = base.cursor; + if (!base.slice_from("ie")) + { + return false; + } + break; + case 2: + if (base.cursor > base.limit_backward) + { + break lab1; + } + break; + } + break; + } + break lab0; + } + base.cursor = base.limit - v_1; + /** @const */ var /** number */ v_5 = base.limit - base.cursor; + if (!base.go_out_grouping_b(g_v, 97, 121)) + { + return false; + } + base.cursor--; + base.cursor = base.limit - v_5; + if (!base.slice_del()) + { + return false; + } + base.ket = base.cursor; + base.bra = base.cursor; + /** @const */ var /** number */ v_6 = base.limit - base.cursor; + among_var = base.find_among_b(a_6); + switch (among_var) { + case 1: + if (!base.slice_from("e")) + { + return false; + } + return false; + case 2: + { + /** @const */ var /** number */ v_7 = base.limit - base.cursor; + lab5: { + if (!(base.in_grouping_b(g_aeo, 97, 111))) + { + break lab5; + } + if (base.cursor > base.limit_backward) + { + break lab5; + } + return false; + } + base.cursor = base.limit - v_7; + } + break; + case 3: + if (base.cursor != I_p1) + { + return false; + } + /** @const */ var /** number */ v_8 = base.limit - base.cursor; + if (!r_shortv()) + { + return false; + } + base.cursor = base.limit - v_8; + if (!base.slice_from("e")) + { + return false; + } + return false; + } + base.cursor = base.limit - v_6; + base.ket = base.cursor; + if (base.cursor <= base.limit_backward) + { + return false; + } + base.cursor--; + base.bra = base.cursor; + if (!base.slice_del()) + { + return false; + } + } + return true; + }; + + /** @return {boolean} */ + function r_Step_1c() { + base.ket = base.cursor; + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.eq_s_b("y"))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("Y"))) + { + return false; + } + } + base.bra = base.cursor; + if (!(base.out_grouping_b(g_v, 97, 121))) + { + return false; + } + lab2: { + if (base.cursor > base.limit_backward) + { + break lab2; + } + return false; + } + if (!base.slice_from("i")) + { + return false; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_2() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_7); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R1()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("tion")) + { + return false; + } + break; + case 2: + if (!base.slice_from("ence")) + { + return false; + } + break; + case 3: + if (!base.slice_from("ance")) + { + return false; + } + break; + case 4: + if (!base.slice_from("able")) + { + return false; + } + break; + case 5: + if (!base.slice_from("ent")) + { + return false; + } + break; + case 6: + if (!base.slice_from("ize")) + { + return false; + } + break; + case 7: + if (!base.slice_from("ate")) + { + return false; + } + break; + case 8: + if (!base.slice_from("al")) + { + return false; + } + break; + case 9: + if (!base.slice_from("ful")) + { + return false; + } + break; + case 10: + if (!base.slice_from("ous")) + { + return false; + } + break; + case 11: + if (!base.slice_from("ive")) + { + return false; + } + break; + case 12: + if (!base.slice_from("ble")) + { + return false; + } + break; + case 13: + if (!base.slice_from("og")) + { + return false; + } + break; + case 14: + if (!(base.eq_s_b("l"))) + { + return false; + } + if (!base.slice_from("og")) + { + return false; + } + break; + case 15: + if (!base.slice_from("less")) + { + return false; + } + break; + case 16: + if (!(base.in_grouping_b(g_valid_LI, 99, 116))) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_3() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_8); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R1()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("tion")) + { + return false; + } + break; + case 2: + if (!base.slice_from("ate")) + { + return false; + } + break; + case 3: + if (!base.slice_from("al")) + { + return false; + } + break; + case 4: + if (!base.slice_from("ic")) + { + return false; + } + break; + case 5: + if (!base.slice_del()) + { + return false; + } + break; + case 6: + if (!r_R2()) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_4() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_9); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + if (!r_R2()) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_del()) + { + return false; + } + break; + case 2: + lab0: { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab1: { + if (!(base.eq_s_b("s"))) + { + break lab1; + } + break lab0; + } + base.cursor = base.limit - v_1; + if (!(base.eq_s_b("t"))) + { + return false; + } + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_Step_5() { + var /** number */ among_var; + base.ket = base.cursor; + among_var = base.find_among_b(a_10); + if (among_var == 0) + { + return false; + } + base.bra = base.cursor; + switch (among_var) { + case 1: + lab0: { + lab1: { + if (!r_R2()) + { + break lab1; + } + break lab0; + } + if (!r_R1()) + { + return false; + } + { + /** @const */ var /** number */ v_1 = base.limit - base.cursor; + lab2: { + if (!r_shortv()) + { + break lab2; + } + return false; + } + base.cursor = base.limit - v_1; + } + } + if (!base.slice_del()) + { + return false; + } + break; + case 2: + if (!r_R2()) + { + return false; + } + if (!(base.eq_s_b("l"))) + { + return false; + } + if (!base.slice_del()) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_exception1() { + var /** number */ among_var; + base.bra = base.cursor; + among_var = base.find_among(a_11); + if (among_var == 0) + { + return false; + } + base.ket = base.cursor; + if (base.cursor < base.limit) + { + return false; + } + switch (among_var) { + case 1: + if (!base.slice_from("sky")) + { + return false; + } + break; + case 2: + if (!base.slice_from("idl")) + { + return false; + } + break; + case 3: + if (!base.slice_from("gentl")) + { + return false; + } + break; + case 4: + if (!base.slice_from("ugli")) + { + return false; + } + break; + case 5: + if (!base.slice_from("earli")) + { + return false; + } + break; + case 6: + if (!base.slice_from("onli")) + { + return false; + } + break; + case 7: + if (!base.slice_from("singl")) + { + return false; + } + break; + } + return true; + }; + + /** @return {boolean} */ + function r_postlude() { + if (!B_Y_found) + { + return false; + } + while(true) + { + /** @const */ var /** number */ v_1 = base.cursor; + lab0: { + golab1: while(true) + { + /** @const */ var /** number */ v_2 = base.cursor; + lab2: { + base.bra = base.cursor; + if (!(base.eq_s("Y"))) + { + break lab2; + } + base.ket = base.cursor; + base.cursor = v_2; + break golab1; + } + base.cursor = v_2; + if (base.cursor >= base.limit) + { + break lab0; + } + base.cursor++; + } + if (!base.slice_from("y")) + { + return false; + } + continue; + } + base.cursor = v_1; + break; + } + return true; + }; + + this.stem = /** @return {boolean} */ function() { + lab0: { + /** @const */ var /** number */ v_1 = base.cursor; + lab1: { + if (!r_exception1()) + { + break lab1; + } + break lab0; + } + base.cursor = v_1; + lab2: { + { + /** @const */ var /** number */ v_2 = base.cursor; + lab3: { + { + /** @const */ var /** number */ c1 = base.cursor + 3; + if (c1 > base.limit) + { + break lab3; + } + base.cursor = c1; + } + break lab2; + } + base.cursor = v_2; + } + break lab0; + } + base.cursor = v_1; + r_prelude(); + r_mark_regions(); + base.limit_backward = base.cursor; base.cursor = base.limit; + /** @const */ var /** number */ v_3 = base.limit - base.cursor; + r_Step_1a(); + base.cursor = base.limit - v_3; + /** @const */ var /** number */ v_4 = base.limit - base.cursor; + r_Step_1b(); + base.cursor = base.limit - v_4; + /** @const */ var /** number */ v_5 = base.limit - base.cursor; + r_Step_1c(); + base.cursor = base.limit - v_5; + /** @const */ var /** number */ v_6 = base.limit - base.cursor; + r_Step_2(); + base.cursor = base.limit - v_6; + /** @const */ var /** number */ v_7 = base.limit - base.cursor; + r_Step_3(); + base.cursor = base.limit - v_7; + /** @const */ var /** number */ v_8 = base.limit - base.cursor; + r_Step_4(); + base.cursor = base.limit - v_8; + /** @const */ var /** number */ v_9 = base.limit - base.cursor; + r_Step_5(); + base.cursor = base.limit - v_9; + base.cursor = base.limit_backward; + /** @const */ var /** number */ v_10 = base.cursor; + r_postlude(); + base.cursor = v_10; + } + return true; + }; + + /**@return{string}*/ + this['stemWord'] = function(/**string*/word) { + base.setCurrent(word); + this.stem(); + return base.getCurrent(); + }; +}; diff --git a/_build/_static/github-banner.svg b/_build/_static/github-banner.svg new file mode 100644 index 0000000..c47d9dc --- /dev/null +++ b/_build/_static/github-banner.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/_build/_static/language_data.js b/_build/_static/language_data.js index 367b8ed..5776786 100644 --- a/_build/_static/language_data.js +++ b/_build/_static/language_data.js @@ -1,199 +1,13 @@ /* - * language_data.js - * ~~~~~~~~~~~~~~~~ - * * This script contains the language-specific data used by searchtools.js, - * namely the list of stopwords, stemmer, scorer and splitter. - * - * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * + * namely the set of stopwords, stemmer, scorer and splitter. */ -var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; +const stopwords = new Set(["a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "aren't", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can't", "cannot", "could", "couldn't", "did", "didn't", "do", "does", "doesn't", "doing", "don't", "down", "during", "each", "few", "for", "from", "further", "had", "hadn't", "has", "hasn't", "have", "haven't", "having", "he", "he'd", "he'll", "he's", "her", "here", "here's", "hers", "herself", "him", "himself", "his", "how", "how's", "i", "i'd", "i'll", "i'm", "i've", "if", "in", "into", "is", "isn't", "it", "it's", "its", "itself", "let's", "me", "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought", "our", "ours", "ourselves", "out", "over", "own", "same", "shan't", "she", "she'd", "she'll", "she's", "should", "shouldn't", "so", "some", "such", "than", "that", "that's", "the", "their", "theirs", "them", "themselves", "then", "there", "there's", "these", "they", "they'd", "they'll", "they're", "they've", "this", "those", "through", "to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "we'd", "we'll", "we're", "we've", "were", "weren't", "what", "what's", "when", "when's", "where", "where's", "which", "while", "who", "who's", "whom", "why", "why's", "with", "won't", "would", "wouldn't", "you", "you'd", "you'll", "you're", "you've", "your", "yours", "yourself", "yourselves"]); +window.stopwords = stopwords; // Export to global scope -/* Non-minified version is copied as a separate JS file, if available */ - -/** - * Porter Stemmer - */ -var Stemmer = function() { - - var step2list = { - ational: 'ate', - tional: 'tion', - enci: 'ence', - anci: 'ance', - izer: 'ize', - bli: 'ble', - alli: 'al', - entli: 'ent', - eli: 'e', - ousli: 'ous', - ization: 'ize', - ation: 'ate', - ator: 'ate', - alism: 'al', - iveness: 'ive', - fulness: 'ful', - ousness: 'ous', - aliti: 'al', - iviti: 'ive', - biliti: 'ble', - logi: 'log' - }; - - var step3list = { - icate: 'ic', - ative: '', - alize: 'al', - iciti: 'ic', - ical: 'ic', - ful: '', - ness: '' - }; - - var c = "[^aeiou]"; // consonant - var v = "[aeiouy]"; // vowel - var C = c + "[^aeiouy]*"; // consonant sequence - var V = v + "[aeiou]*"; // vowel sequence - - var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 - var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 - var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 - var s_v = "^(" + C + ")?" + v; // vowel in stem - - this.stemWord = function (w) { - var stem; - var suffix; - var firstch; - var origword = w; - - if (w.length < 3) - return w; - - var re; - var re2; - var re3; - var re4; - - firstch = w.substr(0,1); - if (firstch == "y") - w = firstch.toUpperCase() + w.substr(1); - - // Step 1a - re = /^(.+?)(ss|i)es$/; - re2 = /^(.+?)([^s])s$/; - - if (re.test(w)) - w = w.replace(re,"$1$2"); - else if (re2.test(w)) - w = w.replace(re2,"$1$2"); - - // Step 1b - re = /^(.+?)eed$/; - re2 = /^(.+?)(ed|ing)$/; - if (re.test(w)) { - var fp = re.exec(w); - re = new RegExp(mgr0); - if (re.test(fp[1])) { - re = /.$/; - w = w.replace(re,""); - } - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1]; - re2 = new RegExp(s_v); - if (re2.test(stem)) { - w = stem; - re2 = /(at|bl|iz)$/; - re3 = new RegExp("([^aeiouylsz])\\1$"); - re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re2.test(w)) - w = w + "e"; - else if (re3.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - else if (re4.test(w)) - w = w + "e"; - } - } - - // Step 1c - re = /^(.+?)y$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(s_v); - if (re.test(stem)) - w = stem + "i"; - } - - // Step 2 - re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step2list[suffix]; - } - - // Step 3 - re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step3list[suffix]; - } - - // Step 4 - re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; - re2 = /^(.+?)(s|t)(ion)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - if (re.test(stem)) - w = stem; - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1] + fp[2]; - re2 = new RegExp(mgr1); - if (re2.test(stem)) - w = stem; - } - - // Step 5 - re = /^(.+?)e$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - re2 = new RegExp(meq1); - re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) - w = stem; - } - re = /ll$/; - re2 = new RegExp(mgr1); - if (re.test(w) && re2.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - - // and turn initial Y back to y - if (firstch == "y") - w = firstch.toLowerCase() + w.substr(1); - return w; - } -} - +/* Non-minified versions are copied as separate JavaScript files, if available */ +BaseStemmer=function(){this.current="",this.cursor=0,this.limit=0,this.limit_backward=0,this.bra=0,this.ket=0,this.setCurrent=function(t){this.current=t,this.cursor=0,this.limit=this.current.length,this.limit_backward=0,this.bra=this.cursor,this.ket=this.limit},this.getCurrent=function(){return this.current},this.copy_from=function(t){this.current=t.current,this.cursor=t.cursor,this.limit=t.limit,this.limit_backward=t.limit_backward,this.bra=t.bra,this.ket=t.ket},this.in_grouping=function(t,r,i){return!(this.cursor>=this.limit||i<(i=this.current.charCodeAt(this.cursor))||i>>3]&1<<(7&i))||(this.cursor++,0))},this.go_in_grouping=function(t,r,i){for(;this.cursor>>3]&1<<(7&s)))return!0;this.cursor++}return!1},this.in_grouping_b=function(t,r,i){return!(this.cursor<=this.limit_backward||i<(i=this.current.charCodeAt(this.cursor-1))||i>>3]&1<<(7&i))||(this.cursor--,0))},this.go_in_grouping_b=function(t,r,i){for(;this.cursor>this.limit_backward;){var s=this.current.charCodeAt(this.cursor-1);if(i>>3]&1<<(7&s)))return!0;this.cursor--}return!1},this.out_grouping=function(t,r,i){return!(this.cursor>=this.limit)&&(i<(i=this.current.charCodeAt(this.cursor))||i>>3]&1<<(7&i)))&&(this.cursor++,!0)},this.go_out_grouping=function(t,r,i){for(;this.cursor>>3]&1<<(7&s)))return!0;this.cursor++}return!1},this.out_grouping_b=function(t,r,i){return!(this.cursor<=this.limit_backward)&&(i<(i=this.current.charCodeAt(this.cursor-1))||i>>3]&1<<(7&i)))&&(this.cursor--,!0)},this.go_out_grouping_b=function(t,r,i){for(;this.cursor>this.limit_backward;){var s=this.current.charCodeAt(this.cursor-1);if(s<=i&&r<=s&&0!=(t[(s-=r)>>>3]&1<<(7&s)))return!0;this.cursor--}return!1},this.eq_s=function(t){return!(this.limit-this.cursor>>1),o=0,a=e=(l=t[r])[0].length){if(this.cursor=s+l[0].length,l.length<4)return l[2];var g=l[3](this);if(this.cursor=s+l[0].length,g)return l[2]}}while(0<=(r=l[1]));return 0},this.find_among_b=function(t){for(var r=0,i=t.length,s=this.cursor,h=this.limit_backward,e=0,n=0,c=!1;;){for(var u,o=r+(i-r>>1),a=0,l=e=(u=t[r])[0].length){if(this.cursor=s-u[0].length,u.length<4)return u[2];var g=u[3](this);if(this.cursor=s-u[0].length,g)return u[2]}}while(0<=(r=u[1]));return 0},this.replace_s=function(t,r,i){var s=i.length-(r-t);return this.current=this.current.slice(0,t)+i+this.current.slice(r),this.limit+=s,this.cursor>=r?this.cursor+=s:this.cursor>t&&(this.cursor=t),s},this.slice_check=function(){return!(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>this.current.length)},this.slice_from=function(t){var r=!1;return this.slice_check()&&(this.replace_s(this.bra,this.ket,t),r=!0),r},this.slice_del=function(){return this.slice_from("")},this.insert=function(t,r,i){r=this.replace_s(t,r,i);t<=this.bra&&(this.bra+=r),t<=this.ket&&(this.ket+=r)},this.slice_to=function(){var t="";return t=this.slice_check()?this.current.slice(this.bra,this.ket):t},this.assign_to=function(){return this.current.slice(0,this.limit)}}; +var EnglishStemmer=function(){var a=new BaseStemmer,c=[["arsen",-1,-1],["commun",-1,-1],["emerg",-1,-1],["gener",-1,-1],["later",-1,-1],["organ",-1,-1],["past",-1,-1],["univers",-1,-1]],o=[["'",-1,1],["'s'",0,1],["'s",-1,1]],u=[["ied",-1,2],["s",-1,3],["ies",1,2],["sses",1,1],["ss",1,-1],["us",1,-1]],t=[["succ",-1,1],["proc",-1,1],["exc",-1,1]],l=[["even",-1,2],["cann",-1,2],["inn",-1,2],["earr",-1,2],["herr",-1,2],["out",-1,2],["y",-1,1]],n=[["",-1,-1],["ed",0,2],["eed",1,1],["ing",0,3],["edly",0,2],["eedly",4,1],["ingly",0,2]],f=[["",-1,3],["bb",0,2],["dd",0,2],["ff",0,2],["gg",0,2],["bl",0,1],["mm",0,2],["nn",0,2],["pp",0,2],["rr",0,2],["at",0,1],["tt",0,2],["iz",0,1]],_=[["anci",-1,3],["enci",-1,2],["ogi",-1,14],["li",-1,16],["bli",3,12],["abli",4,4],["alli",3,8],["fulli",3,9],["lessli",3,15],["ousli",3,10],["entli",3,5],["aliti",-1,8],["biliti",-1,12],["iviti",-1,11],["tional",-1,1],["ational",14,7],["alism",-1,8],["ation",-1,7],["ization",17,6],["izer",-1,6],["ator",-1,7],["iveness",-1,11],["fulness",-1,9],["ousness",-1,10],["ogist",-1,13]],m=[["icate",-1,4],["ative",-1,6],["alize",-1,3],["iciti",-1,4],["ical",-1,4],["tional",-1,1],["ational",5,2],["ful",-1,5],["ness",-1,5]],b=[["ic",-1,1],["ance",-1,1],["ence",-1,1],["able",-1,1],["ible",-1,1],["ate",-1,1],["ive",-1,1],["ize",-1,1],["iti",-1,1],["al",-1,1],["ism",-1,1],["ion",-1,2],["er",-1,1],["ous",-1,1],["ant",-1,1],["ent",-1,1],["ment",15,1],["ement",16,1]],k=[["e",-1,1],["l",-1,2]],g=[["andes",-1,-1],["atlas",-1,-1],["bias",-1,-1],["cosmos",-1,-1],["early",-1,5],["gently",-1,3],["howe",-1,-1],["idly",-1,2],["news",-1,-1],["only",-1,6],["singly",-1,7],["skies",-1,1],["sky",-1,-1],["ugly",-1,4]],d=[17,64],v=[17,65,16,1],i=[1,17,65,208,1],w=[55,141,2],p=!1,y=0,h=0;function q(){var r=a.limit-a.cursor;return!!(a.out_grouping_b(i,89,121)&&a.in_grouping_b(v,97,121)&&a.out_grouping_b(v,97,121)||(a.cursor=a.limit-r,a.out_grouping_b(v,97,121)&&a.in_grouping_b(v,97,121)&&!(a.cursor>a.limit_backward))||(a.cursor=a.limit-r,a.eq_s_b("past")))}function z(){return h<=a.cursor}function Y(){return y<=a.cursor}this.stem=function(){var r=a.cursor;if(!(()=>{var r;if(a.bra=a.cursor,0!=(r=a.find_among(g))&&(a.ket=a.cursor,!(a.cursora.limit)a.cursor=i;else{a.cursor=e,a.cursor=r,(()=>{p=!1;var r=a.cursor;if(a.bra=a.cursor,!a.eq_s("'")||(a.ket=a.cursor,a.slice_del())){a.cursor=r;r=a.cursor;if(a.bra=a.cursor,a.eq_s("y")){if(a.ket=a.cursor,!a.slice_from("Y"))return;p=!0}a.cursor=r;for(r=a.cursor;;){var i=a.cursor;r:{for(;;){var e=a.cursor;if(a.in_grouping(v,97,121)&&(a.bra=a.cursor,a.eq_s("y"))){a.ket=a.cursor,a.cursor=e;break}if(a.cursor=e,a.cursor>=a.limit)break r;a.cursor++}if(!a.slice_from("Y"))return;p=!0;continue}a.cursor=i;break}a.cursor=r}})(),h=a.limit,y=a.limit;i=a.cursor;r:{var s=a.cursor;if(0==a.find_among(c)){if(a.cursor=s,!a.go_out_grouping(v,97,121))break r;if(a.cursor++,!a.go_in_grouping(v,97,121))break r;a.cursor++}h=a.cursor,a.go_out_grouping(v,97,121)&&(a.cursor++,a.go_in_grouping(v,97,121))&&(a.cursor++,y=a.cursor)}a.cursor=i,a.limit_backward=a.cursor,a.cursor=a.limit;var e=a.limit-a.cursor,r=((()=>{var r=a.limit-a.cursor;if(a.ket=a.cursor,0==a.find_among_b(o))a.cursor=a.limit-r;else if(a.bra=a.cursor,!a.slice_del())return;if(a.ket=a.cursor,0!=(r=a.find_among_b(u)))switch(a.bra=a.cursor,r){case 1:if(a.slice_from("ss"))break;return;case 2:r:{var i=a.limit-a.cursor,e=a.cursor-2;if(!(e{a.ket=a.cursor,o=a.find_among_b(n),a.bra=a.cursor;r:{var r=a.limit-a.cursor;i:{switch(o){case 1:var i=a.limit-a.cursor;e:{var e=a.limit-a.cursor;if(0==a.find_among_b(t)||a.cursor>a.limit_backward){if(a.cursor=a.limit-e,!z())break e;if(!a.slice_from("ee"))return}}a.cursor=a.limit-i;break;case 2:break i;case 3:if(0==(o=a.find_among_b(l)))break i;switch(o){case 1:var s=a.limit-a.cursor;if(!a.out_grouping_b(v,97,121))break i;if(a.cursor>a.limit_backward)break i;if(a.cursor=a.limit-s,a.bra=a.cursor,a.slice_from("ie"))break;return;case 2:if(a.cursor>a.limit_backward)break i}}break r}a.cursor=a.limit-r;var c=a.limit-a.cursor;if(!a.go_out_grouping_b(v,97,121))return;if(a.cursor--,a.cursor=a.limit-c,!a.slice_del())return;a.ket=a.cursor,a.bra=a.cursor;var o,c=a.limit-a.cursor;switch(o=a.find_among_b(f)){case 1:return a.slice_from("e");case 2:var u=a.limit-a.cursor;if(a.in_grouping_b(d,97,111)&&!(a.cursor>a.limit_backward))return;a.cursor=a.limit-u;break;case 3:return a.cursor!=h||(u=a.limit-a.cursor,q()&&(a.cursor=a.limit-u,a.slice_from("e")))}if(a.cursor=a.limit-c,a.ket=a.cursor,a.cursor<=a.limit_backward)return;if(a.cursor--,a.bra=a.cursor,!a.slice_del())return}})(),a.cursor=a.limit-r,a.limit-a.cursor),r=(a.ket=a.cursor,e=a.limit-a.cursor,(a.eq_s_b("y")||(a.cursor=a.limit-e,a.eq_s_b("Y")))&&(a.bra=a.cursor,a.out_grouping_b(v,97,121))&&a.cursor>a.limit_backward&&a.slice_from("i"),a.cursor=a.limit-i,a.limit-a.cursor),e=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(_))&&(a.bra=a.cursor,z()))switch(r){case 1:if(a.slice_from("tion"))break;return;case 2:if(a.slice_from("ence"))break;return;case 3:if(a.slice_from("ance"))break;return;case 4:if(a.slice_from("able"))break;return;case 5:if(a.slice_from("ent"))break;return;case 6:if(a.slice_from("ize"))break;return;case 7:if(a.slice_from("ate"))break;return;case 8:if(a.slice_from("al"))break;return;case 9:if(a.slice_from("ful"))break;return;case 10:if(a.slice_from("ous"))break;return;case 11:if(a.slice_from("ive"))break;return;case 12:if(a.slice_from("ble"))break;return;case 13:if(a.slice_from("og"))break;return;case 14:if(!a.eq_s_b("l"))return;if(a.slice_from("og"))break;return;case 15:if(a.slice_from("less"))break;return;case 16:if(!a.in_grouping_b(w,99,116))return;if(a.slice_del())break}})(),a.cursor=a.limit-r,a.limit-a.cursor),i=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(m))&&(a.bra=a.cursor,z()))switch(r){case 1:if(a.slice_from("tion"))break;return;case 2:if(a.slice_from("ate"))break;return;case 3:if(a.slice_from("al"))break;return;case 4:if(a.slice_from("ic"))break;return;case 5:if(a.slice_del())break;return;case 6:if(!Y())return;if(a.slice_del())break}})(),a.cursor=a.limit-e,a.limit-a.cursor),r=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(b))&&(a.bra=a.cursor,Y()))switch(r){case 1:if(a.slice_del())break;return;case 2:var i=a.limit-a.cursor;if(!a.eq_s_b("s")&&(a.cursor=a.limit-i,!a.eq_s_b("t")))return;if(a.slice_del())break}})(),a.cursor=a.limit-i,a.limit-a.cursor),e=((()=>{var r;if(a.ket=a.cursor,0!=(r=a.find_among_b(k)))switch(a.bra=a.cursor,r){case 1:if(!Y()){if(!z())return;var i=a.limit-a.cursor;if(q())return;a.cursor=a.limit-i}if(a.slice_del())break;return;case 2:if(!Y())return;if(!a.eq_s_b("l"))return;if(a.slice_del())break}})(),a.cursor=a.limit-r,a.cursor=a.limit_backward,a.cursor);(()=>{if(p)for(;;){var r=a.cursor;r:{for(;;){var i=a.cursor;if(a.bra=a.cursor,a.eq_s("Y")){a.ket=a.cursor,a.cursor=i;break}if(a.cursor=i,a.cursor>=a.limit)break r;a.cursor++}if(a.slice_from("y"))continue;return}a.cursor=r;break}})(),a.cursor=e}}return!0},this.stemWord=function(r){return a.setCurrent(r),this.stem(),a.getCurrent()}}; +window.Stemmer = EnglishStemmer; diff --git a/_build/_static/pygments.css b/_build/_static/pygments.css index 04a4174..9392ddc 100644 --- a/_build/_static/pygments.css +++ b/_build/_static/pygments.css @@ -5,80 +5,80 @@ td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5 span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } .highlight .hll { background-color: #ffffcc } .highlight { background: #f8f8f8; } -.highlight .c { color: #8f5902; font-style: italic } /* Comment */ -.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ -.highlight .g { color: #000000 } /* Generic */ +.highlight .c { color: #8F5902; font-style: italic } /* Comment */ +.highlight .err { color: #A40000; border: 1px solid #EF2929 } /* Error */ +.highlight .g { color: #000 } /* Generic */ .highlight .k { color: #004461; font-weight: bold } /* Keyword */ -.highlight .l { color: #000000 } /* Literal */ -.highlight .n { color: #000000 } /* Name */ +.highlight .l { color: #000 } /* Literal */ +.highlight .n { color: #000 } /* Name */ .highlight .o { color: #582800 } /* Operator */ -.highlight .x { color: #000000 } /* Other */ -.highlight .p { color: #000000; font-weight: bold } /* Punctuation */ -.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #8f5902 } /* Comment.Preproc */ -.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #a40000 } /* Generic.Deleted */ -.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ -.highlight .ges { color: #000000 } /* Generic.EmphStrong */ -.highlight .gr { color: #ef2929 } /* Generic.Error */ +.highlight .x { color: #000 } /* Other */ +.highlight .p { color: #000; font-weight: bold } /* Punctuation */ +.highlight .ch { color: #8F5902; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #8F5902; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #8F5902 } /* Comment.Preproc */ +.highlight .cpf { color: #8F5902; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #8F5902; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #8F5902; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A40000 } /* Generic.Deleted */ +.highlight .ge { color: #000; font-style: italic } /* Generic.Emph */ +.highlight .ges { color: #000 } /* Generic.EmphStrong */ +.highlight .gr { color: #EF2929 } /* Generic.Error */ .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ .highlight .gi { color: #00A000 } /* Generic.Inserted */ -.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .go { color: #888 } /* Generic.Output */ .highlight .gp { color: #745334 } /* Generic.Prompt */ -.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ +.highlight .gs { color: #000; font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ +.highlight .gt { color: #A40000; font-weight: bold } /* Generic.Traceback */ .highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */ .highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */ .highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */ .highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */ .highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */ .highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */ -.highlight .ld { color: #000000 } /* Literal.Date */ -.highlight .m { color: #990000 } /* Literal.Number */ -.highlight .s { color: #4e9a06 } /* Literal.String */ -.highlight .na { color: #c4a000 } /* Name.Attribute */ +.highlight .ld { color: #000 } /* Literal.Date */ +.highlight .m { color: #900 } /* Literal.Number */ +.highlight .s { color: #4E9A06 } /* Literal.String */ +.highlight .na { color: #C4A000 } /* Name.Attribute */ .highlight .nb { color: #004461 } /* Name.Builtin */ -.highlight .nc { color: #000000 } /* Name.Class */ -.highlight .no { color: #000000 } /* Name.Constant */ -.highlight .nd { color: #888888 } /* Name.Decorator */ -.highlight .ni { color: #ce5c00 } /* Name.Entity */ -.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #000000 } /* Name.Function */ -.highlight .nl { color: #f57900 } /* Name.Label */ -.highlight .nn { color: #000000 } /* Name.Namespace */ -.highlight .nx { color: #000000 } /* Name.Other */ -.highlight .py { color: #000000 } /* Name.Property */ +.highlight .nc { color: #000 } /* Name.Class */ +.highlight .no { color: #000 } /* Name.Constant */ +.highlight .nd { color: #888 } /* Name.Decorator */ +.highlight .ni { color: #CE5C00 } /* Name.Entity */ +.highlight .ne { color: #C00; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #000 } /* Name.Function */ +.highlight .nl { color: #F57900 } /* Name.Label */ +.highlight .nn { color: #000 } /* Name.Namespace */ +.highlight .nx { color: #000 } /* Name.Other */ +.highlight .py { color: #000 } /* Name.Property */ .highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #000000 } /* Name.Variable */ +.highlight .nv { color: #000 } /* Name.Variable */ .highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */ -.highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */ -.highlight .w { color: #f8f8f8 } /* Text.Whitespace */ -.highlight .mb { color: #990000 } /* Literal.Number.Bin */ -.highlight .mf { color: #990000 } /* Literal.Number.Float */ -.highlight .mh { color: #990000 } /* Literal.Number.Hex */ -.highlight .mi { color: #990000 } /* Literal.Number.Integer */ -.highlight .mo { color: #990000 } /* Literal.Number.Oct */ -.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ -.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ -.highlight .sc { color: #4e9a06 } /* Literal.String.Char */ -.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ -.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ -.highlight .se { color: #4e9a06 } /* Literal.String.Escape */ -.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ -.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ -.highlight .sx { color: #4e9a06 } /* Literal.String.Other */ -.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ -.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ -.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ -.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #000000 } /* Name.Function.Magic */ -.highlight .vc { color: #000000 } /* Name.Variable.Class */ -.highlight .vg { color: #000000 } /* Name.Variable.Global */ -.highlight .vi { color: #000000 } /* Name.Variable.Instance */ -.highlight .vm { color: #000000 } /* Name.Variable.Magic */ -.highlight .il { color: #990000 } /* Literal.Number.Integer.Long */ \ No newline at end of file +.highlight .pm { color: #000; font-weight: bold } /* Punctuation.Marker */ +.highlight .w { color: #F8F8F8 } /* Text.Whitespace */ +.highlight .mb { color: #900 } /* Literal.Number.Bin */ +.highlight .mf { color: #900 } /* Literal.Number.Float */ +.highlight .mh { color: #900 } /* Literal.Number.Hex */ +.highlight .mi { color: #900 } /* Literal.Number.Integer */ +.highlight .mo { color: #900 } /* Literal.Number.Oct */ +.highlight .sa { color: #4E9A06 } /* Literal.String.Affix */ +.highlight .sb { color: #4E9A06 } /* Literal.String.Backtick */ +.highlight .sc { color: #4E9A06 } /* Literal.String.Char */ +.highlight .dl { color: #4E9A06 } /* Literal.String.Delimiter */ +.highlight .sd { color: #8F5902; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4E9A06 } /* Literal.String.Double */ +.highlight .se { color: #4E9A06 } /* Literal.String.Escape */ +.highlight .sh { color: #4E9A06 } /* Literal.String.Heredoc */ +.highlight .si { color: #4E9A06 } /* Literal.String.Interpol */ +.highlight .sx { color: #4E9A06 } /* Literal.String.Other */ +.highlight .sr { color: #4E9A06 } /* Literal.String.Regex */ +.highlight .s1 { color: #4E9A06 } /* Literal.String.Single */ +.highlight .ss { color: #4E9A06 } /* Literal.String.Symbol */ +.highlight .bp { color: #3465A4 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #000 } /* Name.Function.Magic */ +.highlight .vc { color: #000 } /* Name.Variable.Class */ +.highlight .vg { color: #000 } /* Name.Variable.Global */ +.highlight .vi { color: #000 } /* Name.Variable.Instance */ +.highlight .vm { color: #000 } /* Name.Variable.Magic */ +.highlight .il { color: #900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_build/_static/searchtools.js b/_build/_static/searchtools.js index b08d58c..e29b1c7 100644 --- a/_build/_static/searchtools.js +++ b/_build/_static/searchtools.js @@ -1,12 +1,5 @@ /* - * searchtools.js - * ~~~~~~~~~~~~~~~~ - * * Sphinx JavaScript utilities for the full-text search. - * - * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * */ "use strict"; @@ -20,7 +13,7 @@ if (typeof Scorer === "undefined") { // and returns the new score. /* score: result => { - const [docname, title, anchor, descr, score, filename] = result + const [docname, title, anchor, descr, score, filename, kind] = result return score }, */ @@ -47,6 +40,15 @@ if (typeof Scorer === "undefined") { }; } +// Global search result kind enum, used by themes to style search results. +// prettier-ignore +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + const _removeChildren = (element) => { while (element && element.lastChild) element.removeChild(element.lastChild); }; @@ -57,6 +59,15 @@ const _removeChildren = (element) => { const _escapeRegExp = (string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string +const _escapeHTML = (text) => { + return text + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +}; + const _displayItem = (item, searchTerms, highlightTerms) => { const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; @@ -64,9 +75,13 @@ const _displayItem = (item, searchTerms, highlightTerms) => { const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; const contentRoot = document.documentElement.dataset.content_root; - const [docName, title, anchor, descr, score, _filename] = item; + const [docName, title, anchor, descr, score, _filename, kind] = item; let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); let requestUrl; let linkUrl; if (docBuilder === "dirhtml") { @@ -85,25 +100,30 @@ const _displayItem = (item, searchTerms, highlightTerms) => { let linkEl = listItem.appendChild(document.createElement("a")); linkEl.href = linkUrl + anchor; linkEl.dataset.score = score; - linkEl.innerHTML = title; + linkEl.innerHTML = _escapeHTML(title); if (descr) { listItem.appendChild(document.createElement("span")).innerHTML = - " (" + descr + ")"; + ` (${_escapeHTML(descr)})`; // highlight search terms in the description - if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js - highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); - } - else if (showSearchSummary) + if (SPHINX_HIGHLIGHT_ENABLED) + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + highlightTerms.forEach((term) => + _highlightText(listItem, term, "highlighted"), + ); + } else if (showSearchSummary) fetch(requestUrl) .then((responseData) => responseData.text()) .then((data) => { if (data) listItem.appendChild( - Search.makeSearchSummary(data, searchTerms, anchor) + Search.makeSearchSummary(data, searchTerms, anchor), ); // highlight search terms in the summary - if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js - highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + if (SPHINX_HIGHLIGHT_ENABLED) + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + highlightTerms.forEach((term) => + _highlightText(listItem, term, "highlighted"), + ); }); Search.output.appendChild(listItem); }; @@ -112,12 +132,14 @@ const _finishSearch = (resultCount) => { Search.title.innerText = _("Search Results"); if (!resultCount) Search.status.innerText = Documentation.gettext( - "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories.", ); else - Search.status.innerText = _( - "Search finished, found ${resultCount} page(s) matching the search query." - ).replace('${resultCount}', resultCount); + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace("${resultCount}", resultCount); }; const _displayNextItem = ( results, @@ -131,14 +153,14 @@ const _displayNextItem = ( _displayItem(results.pop(), searchTerms, highlightTerms); setTimeout( () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), - 5 + 5, ); } // search finished, update title and status message else _finishSearch(resultCount); }; // Helper function used by query() to order search results. -// Each input is an array of [docname, title, anchor, descr, score, filename]. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. // Order the results by score (in opposite order of appearance, since the // `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. const _orderResultsByScoreThenName = (a, b) => { @@ -163,9 +185,10 @@ const _orderResultsByScoreThenName = (a, b) => { * This is the same as ``\W+`` in Python, preserving the surrogate pair area. */ if (typeof splitQuery === "undefined") { - var splitQuery = (query) => query + var splitQuery = (query) => + query .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) - .filter(term => term) // remove remaining empty strings + .filter((term) => term); // remove remaining empty strings } /** @@ -177,16 +200,23 @@ const Search = { _pulse_status: -1, htmlToText: (htmlString, anchor) => { - const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + const htmlElement = new DOMParser().parseFromString( + htmlString, + "text/html", + ); for (const removalQuery of [".headerlink", "script", "style"]) { - htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + htmlElement.querySelectorAll(removalQuery).forEach((el) => { + el.remove(); + }); } if (anchor) { - const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + const anchorContent = htmlElement.querySelector( + `[role="main"] ${anchor}`, + ); if (anchorContent) return anchorContent.textContent; console.warn( - `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.`, ); } @@ -195,7 +225,7 @@ const Search = { if (docContent) return docContent.textContent; console.warn( - "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template.", ); return ""; }, @@ -248,6 +278,7 @@ const Search = { searchSummary.classList.add("search-summary"); searchSummary.innerText = ""; const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); searchList.classList.add("search"); const out = document.getElementById("search-results"); @@ -279,12 +310,8 @@ const Search = { const queryTermLower = queryTerm.toLowerCase(); // maybe skip this "word" - // stopwords array is from language_data.js - if ( - stopwords.indexOf(queryTermLower) !== -1 || - queryTerm.match(/^\d+$/) - ) - return; + // stopwords set is from language_data.js + if (stopwords.has(queryTermLower) || queryTerm.match(/^\d+$/)) return; // stem the word let word = stemmer.stemWord(queryTermLower); @@ -296,8 +323,12 @@ const Search = { } }); - if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js - localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + if (SPHINX_HIGHLIGHT_ENABLED) { + // SPHINX_HIGHLIGHT_ENABLED is set in sphinx_highlight.js + localStorage.setItem( + "sphinx_highlight_terms", + [...highlightTerms].join(" "), + ); } // console.debug("SEARCH: searching for:"); @@ -310,7 +341,13 @@ const Search = { /** * execute search (requires search index to be loaded) */ - _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + _performSearch: ( + query, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ) => { const filenames = Search._index.filenames; const docNames = Search._index.docnames; const titles = Search._index.titles; @@ -318,7 +355,7 @@ const Search = { const indexEntries = Search._index.indexentries; // Collect multiple result groups to be sorted separately and then ordered. - // Each is an array of [docname, title, anchor, descr, score, filename]. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. const normalResults = []; const nonMainIndexResults = []; @@ -326,10 +363,15 @@ const Search = { const queryLower = query.toLowerCase().trim(); for (const [title, foundTitles] of Object.entries(allTitles)) { - if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + if ( + title.toLowerCase().trim().includes(queryLower) + && queryLower.length >= title.length / 2 + ) { for (const [file, id] of foundTitles) { - const score = Math.round(Scorer.title * queryLower.length / title.length); - const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + const score = Math.round( + (Scorer.title * queryLower.length) / title.length, + ); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles normalResults.push([ docNames[file], titles[file] !== title ? `${titles[file]} > ${title}` : title, @@ -337,6 +379,7 @@ const Search = { null, score + boost, filenames[file], + SearchResultKind.title, ]); } } @@ -344,9 +387,9 @@ const Search = { // search for explicit entries in index directives for (const [entry, foundEntries] of Object.entries(indexEntries)) { - if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + if (entry.includes(queryLower) && queryLower.length >= entry.length / 2) { for (const [file, id, isMain] of foundEntries) { - const score = Math.round(100 * queryLower.length / entry.length); + const score = Math.round((100 * queryLower.length) / entry.length); const result = [ docNames[file], titles[file], @@ -354,6 +397,7 @@ const Search = { null, score, filenames[file], + SearchResultKind.index, ]; if (isMain) { normalResults.push(result); @@ -366,11 +410,13 @@ const Search = { // lookup as object objectTerms.forEach((term) => - normalResults.push(...Search.performObjectSearch(term, objectTerms)) + normalResults.push(...Search.performObjectSearch(term, objectTerms)), ); // lookup as search terms in fulltext - normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + normalResults.push( + ...Search.performTermsSearch(searchTerms, excludedTerms), + ); // let the scorer override scores with a custom scoring function if (Scorer.score) { @@ -391,7 +437,11 @@ const Search = { // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept let seen = new Set(); results = results.reverse().reduce((acc, result) => { - let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + let resultStr = result + .slice(0, 4) + .concat([result[5]]) + .map((v) => String(v)) + .join(","); if (!seen.has(resultStr)) { acc.push(result); seen.add(resultStr); @@ -403,8 +453,20 @@ const Search = { }, query: (query) => { - const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); - const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + const [ + searchQuery, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ] = Search._parseQuery(query); + const results = Search._performSearch( + searchQuery, + searchTerms, + excludedTerms, + highlightTerms, + objectTerms, + ); // for debugging //Search.lastresults = results.slice(); // a copy @@ -427,7 +489,7 @@ const Search = { const results = []; const objectSearchCallback = (prefix, match) => { - const name = match[4] + const name = match[4]; const fullname = (prefix ? prefix + "." : "") + name; const fullnameLower = fullname.toLowerCase(); if (fullnameLower.indexOf(object) < 0) return; @@ -475,12 +537,11 @@ const Search = { descr, score, filenames[match[0]], + SearchResultKind.object, ]); }; Object.keys(objects).forEach((prefix) => - objects[prefix].forEach((array) => - objectSearchCallback(prefix, array) - ) + objects[prefix].forEach((array) => objectSearchCallback(prefix, array)), ); return results; }, @@ -502,9 +563,17 @@ const Search = { // perform the search on the required terms searchTerms.forEach((word) => { const files = []; + // find documents, if any, containing the query word in their text/title term indices + // use Object.hasOwnProperty to avoid mismatching against prototype properties const arr = [ - { files: terms[word], score: Scorer.term }, - { files: titleTerms[word], score: Scorer.title }, + { + files: terms.hasOwnProperty(word) ? terms[word] : undefined, + score: Scorer.term, + }, + { + files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : undefined, + score: Scorer.title, + }, ]; // add support for partial matches if (word.length > 2) { @@ -536,15 +605,17 @@ const Search = { // set score for the word in each file recordFiles.forEach((file) => { - if (!scoreMap.has(file)) scoreMap.set(file, {}); - scoreMap.get(file)[word] = record.score; + if (!scoreMap.has(file)) scoreMap.set(file, new Map()); + const fileScores = scoreMap.get(file); + fileScores.set(word, record.score); }); }); // create the mapping files.forEach((file) => { if (!fileMap.has(file)) fileMap.set(file, [word]); - else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + else if (fileMap.get(file).indexOf(word) === -1) + fileMap.get(file).push(word); }); }); @@ -555,11 +626,11 @@ const Search = { // as search terms with length < 3 are discarded const filteredTermCount = [...searchTerms].filter( - (term) => term.length > 2 + (term) => term.length > 2, ).length; if ( - wordList.length !== searchTerms.size && - wordList.length !== filteredTermCount + wordList.length !== searchTerms.size + && wordList.length !== filteredTermCount ) continue; @@ -567,16 +638,16 @@ const Search = { if ( [...excludedTerms].some( (term) => - terms[term] === file || - titleTerms[term] === file || - (terms[term] || []).includes(file) || - (titleTerms[term] || []).includes(file) + terms[term] === file + || titleTerms[term] === file + || (terms[term] || []).includes(file) + || (titleTerms[term] || []).includes(file), ) ) break; // select one (max) score for the file. - const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + const score = Math.max(...wordList.map((w) => scoreMap.get(file).get(w))); // add result to the result list results.push([ docNames[file], @@ -585,6 +656,7 @@ const Search = { null, score, filenames[file], + SearchResultKind.text, ]); } return results; @@ -611,7 +683,8 @@ const Search = { let summary = document.createElement("p"); summary.classList.add("context"); - summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + summary.textContent = + top + text.substr(startWithContext, 240).trim() + tail; return summary; }, diff --git a/_build/_static/sphinx_highlight.js b/_build/_static/sphinx_highlight.js index 8a96c69..a74e103 100644 --- a/_build/_static/sphinx_highlight.js +++ b/_build/_static/sphinx_highlight.js @@ -1,7 +1,7 @@ /* Highlighting utilities for Sphinx HTML documentation. */ "use strict"; -const SPHINX_HIGHLIGHT_ENABLED = true +const SPHINX_HIGHLIGHT_ENABLED = true; /** * highlight a given string on a node by wrapping it in @@ -13,9 +13,9 @@ const _highlight = (node, addItems, text, className) => { const parent = node.parentNode; const pos = val.toLowerCase().indexOf(text); if ( - pos >= 0 && - !parent.classList.contains(className) && - !parent.classList.contains("nohighlight") + pos >= 0 + && !parent.classList.contains(className) + && !parent.classList.contains("nohighlight") ) { let span; @@ -30,13 +30,7 @@ const _highlight = (node, addItems, text, className) => { span.appendChild(document.createTextNode(val.substr(pos, text.length))); const rest = document.createTextNode(val.substr(pos + text.length)); - parent.insertBefore( - span, - parent.insertBefore( - rest, - node.nextSibling - ) - ); + parent.insertBefore(span, parent.insertBefore(rest, node.nextSibling)); node.nodeValue = val.substr(0, pos); /* There may be more occurrences of search term in this node. So call this * function recursively on the remaining fragment. @@ -46,7 +40,7 @@ const _highlight = (node, addItems, text, className) => { if (isInSVG) { const rect = document.createElementNS( "http://www.w3.org/2000/svg", - "rect" + "rect", ); const bbox = parent.getBBox(); rect.x.baseVal.value = bbox.x; @@ -65,7 +59,7 @@ const _highlightText = (thisNode, text, className) => { let addItems = []; _highlight(thisNode, addItems, text, className); addItems.forEach((obj) => - obj.parent.insertAdjacentElement("beforebegin", obj.target) + obj.parent.insertAdjacentElement("beforebegin", obj.target), ); }; @@ -73,25 +67,31 @@ const _highlightText = (thisNode, text, className) => { * Small JavaScript module for the documentation. */ const SphinxHighlight = { - /** * highlight the search words provided in localstorage in the text */ highlightSearchWords: () => { - if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight // get and clear terms from localstorage const url = new URL(window.location); const highlight = - localStorage.getItem("sphinx_highlight_terms") - || url.searchParams.get("highlight") - || ""; - localStorage.removeItem("sphinx_highlight_terms") - url.searchParams.delete("highlight"); - window.history.replaceState({}, "", url); + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms"); + // Update history only if '?highlight' is present; otherwise it + // clears text fragments (not set in window.location by the browser) + if (url.searchParams.has("highlight")) { + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + } // get individual terms from highlight string - const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + const terms = highlight + .toLowerCase() + .split(/\s+/) + .filter((x) => x); if (terms.length === 0) return; // nothing to do // There should never be more than one element matching "div.body" @@ -107,11 +107,11 @@ const SphinxHighlight = { document .createRange() .createContextualFragment( - '" - ) + '", + ), ); }, @@ -125,7 +125,7 @@ const SphinxHighlight = { document .querySelectorAll("span.highlighted") .forEach((el) => el.classList.remove("highlighted")); - localStorage.removeItem("sphinx_highlight_terms") + localStorage.removeItem("sphinx_highlight_terms"); }, initEscapeListener: () => { @@ -134,10 +134,15 @@ const SphinxHighlight = { document.addEventListener("keydown", (event) => { // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) + return; // bail with special keys - if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; - if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) + return; + if ( + DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + && event.key === "Escape" + ) { SphinxHighlight.hideSearchWords(); event.preventDefault(); } diff --git a/_build/genindex.html b/_build/genindex.html index c65496e..266d5ad 100644 --- a/_build/genindex.html +++ b/_build/genindex.html @@ -5,11 +5,12 @@ Index — SyncSketch Python API Library 1.0.10.5 documentation - - + + + - - + + @@ -85,6 +86,12 @@

C

+
@@ -99,6 +106,8 @@

D

- + @@ -260,25 +275,24 @@

SyncSketch Python API Library

-

Navigation

-
-

Related Topics

- -
- +

Navigation

+ +
+

Related Topics

+ +
@@ -295,8 +309,8 @@

Quick search

©2024, Brady Endres, Phil Floetotto, Nicholas Kegler dos Santos, Tyler Nickerson. | - Powered by Sphinx 7.4.7 - & Alabaster 0.7.16 + Powered by Sphinx 9.1.0 + & Alabaster 1.0.0 diff --git a/_build/index.html b/_build/index.html index 3108077..2fe628e 100644 --- a/_build/index.html +++ b/_build/index.html @@ -6,11 +6,12 @@ SyncSketch Python API Library documentation — SyncSketch Python API Library 1.0.10.5 documentation - - + + + - - + + @@ -37,7 +38,7 @@

SyncSketch Python API Library documentation
-class syncsketch.SyncSketchAPI
+class syncsketch.SyncSketchAPI

SyncSketchAPI is a class that provides a set of methods to interact with SyncSketch API.

@@ -84,12 +85,12 @@

SyncSketch Python API Library documentation
-class syncsketch.SyncSketchAPI(auth, api_key, host='https://www.syncsketch.com', useExpiringToken=False, debug=False, api_version='v1', use_header_auth=False)
+class syncsketch.SyncSketchAPI(auth, api_key, host='https://www.syncsketch.com', useExpiringToken=False, debug=False, api_version='v1', use_header_auth=False)

Bases: object

Convenience API to communicate with the SyncSketch Service for collaborative online reviews

-is_connected()
+is_connected(raw_response=True)

Convenience function to check if the API is connected to SyncSketch Will check against Status Code 200 and return False if not which most likely would be and authorization error

@@ -105,11 +106,14 @@

SyncSketch Python API Library documentation
-get_tree(withItems=False)
+get_tree(withItems=False, raw_response=False)

Get nested tree of account, projects, reviews and optionally items for the current user

Parameters:
-

withItems (bool) – Include items in the response

+
    +
  • withItems (bool) – Include items in the response

  • +
  • raw_response (bool) – Get whole response from REST API.

  • +
Returns:

Tree data

@@ -122,27 +126,34 @@

SyncSketch Python API Library documentation
-get_accounts()
-

Summary

+get_accounts(fields=None, raw_response=False) +

Get a list of workspaces the user has access to

-
Returns:
-

List of workspaces the user has access to

+
Parameters:
+
    +
  • fields (list|str|int|bool) – fields to fetch from backend

  • +
  • raw_response (bool) – Get whole response from REST API.

  • +
-
Return type:
-

list[dict]

+
Returns:
+

List of workspaces the user has access to

+
+
Return type:
+

list[dict]

-update_account(account_id, data)
+update_account(account_id, data, raw_response=False)

Update a workspace / account

Parameters:
  • account_id (int) – the id of the item

  • data (dict) – normal dict with data for item

  • +
  • raw_response (bool) – Get whole response from REST API.

Returns:
@@ -156,7 +167,7 @@

SyncSketch Python API Library documentation
-create_project(account_id, name, description='', data=None)
+create_project(account_id, name, description='', data=None, raw_response=False)

Add a project to your account. Please make sure to pass the accountId which you can query using the getAccounts command.

Parameters:
@@ -165,6 +176,7 @@

SyncSketch Python API Library documentationReturns: @@ -178,8 +190,8 @@

SyncSketch Python API Library documentation
-get_projects(include_deleted=False, include_archived=False, include_tags=False, include_connections=False, limit=100, offset=0)
-

Get a list of currently active projects

+get_projects(include_deleted=False, include_archived=False, include_tags=False, include_connections=False, limit=100, offset=0, fields=None, raw_response=False) +

Get a list of currently active projects the user has access to

Parameters:
    @@ -189,6 +201,8 @@

    SyncSketch Python API Library documentationReturns: @@ -202,11 +216,15 @@

    SyncSketch Python API Library documentation
    -get_projects_by_name(name)
    -

    Get a project by name regardless of status

    +get_projects_by_name(name, fields=None, raw_response=False) +

    Get a list of projects by name

    Parameters:
    -

    name (str) – Name to search for

    +
      +
    • name (str) – Name to search for

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    List of projects

    @@ -219,11 +237,15 @@

    SyncSketch Python API Library documentation
    -get_project_by_id(project_id)
    +get_project_by_id(project_id, fields=None, raw_response=False)

    Get single project by id

    Parameters:
    -

    project_id (int) – Project id

    +
      +
    • project_id (int) – Project id

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    Project data

    @@ -236,27 +258,38 @@

    SyncSketch Python API Library documentation
    -get_project_storage(project_id)
    +get_project_storage(project_id, raw_response=False)

    Get project storage usage in bytes

    +
    # Example response
    +{'storage': 12345}
    +
    +
    Parameters:
    -

    project_id (int) – Project id

    +
      +
    • project_id (int) – Project ID

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    Storage usage in bytes

    +
    Return type:
    +

    dict[str, int]

    +

    -update_project(project_id, data)
    +update_project(project_id, data, raw_response=False)

    Update a project

    Parameters:
    • project_id (int) – the id of the item

    • data (dict) – dict with new data for item

    • +
    • raw_response (bool) – Get whole response from REST API.

    Returns:
    @@ -270,11 +303,14 @@

    SyncSketch Python API Library documentation
    -delete_project(project_id)
    +delete_project(project_id, raw_response=False)

    Delete a project by id.

    Parameters:
    -

    project_id (int) – Project ID to delete

    +
      +
    • project_id (int) – Project ID to delete

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    @@ -284,7 +320,7 @@

    SyncSketch Python API Library documentation
    -duplicate_project(project_id, name=None, copy_reviews=False, copy_users=False, copy_settings=False)
    +duplicate_project(project_id, name=None, copy_reviews=False, copy_users=False, copy_settings=False, raw_response=False)

    Create a new project from an existing project

    Parameters:
    @@ -294,6 +330,7 @@

    SyncSketch Python API Library documentationReturns: @@ -307,56 +344,110 @@

    SyncSketch Python API Library documentation
    -archive_project(project_id)
    +archive_project(project_id, raw_response=False)

    Archive a project

    Parameters:
    -

    project_id (int)

    +
      +
    • project_id (int)

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:
    -

    +

    Project data

    +
    +
    Return type:
    +

    dict

    -restore_project(project_id)
    +restore_project(project_id, raw_response=False)

    Restore (unarchive) a project

    Parameters:
    -

    project_id (int)

    +
      +
    • project_id (int)

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:
    -

    +

    Project data

    +
    +
    Return type:
    +

    dict

    +
    +
    +
    + +
    +
    +create_review(project_id, name, description='', data=None, raw_response=False)
    +

    Add a review to a project

    +
    +
    Parameters:
    +
      +
    • project_id (int)

    • +
    • name (str)

    • +
    • description (str)

    • +
    • data (dict)

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    +
    +
    Returns:
    +

    Review data

    +
    +
    Return type:
    +

    dict

    -get_reviews_by_project_id(project_id, limit=100, offset=0)
    +get_reviews_by_project_id(project_id, limit=100, offset=0, fields=None, raw_response=False)

    Get list of reviews by project id.

    +
    # Example response
    +{
    +    "meta": {...},
    +    "objects": [...]
    +}
    +
    +
    Parameters:
    -

    project_id (int) – SyncSketch project id

    +
      +
    • project_id (int) – SyncSketch project id

    • +
    • limit (int) – Limit the number of results

    • +
    • offset (int) – Offset the results

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    Dict with meta information and an array of found projects

    Return type:
    -

    list[dict]

    +

    dict

    -get_review_by_name(name)
    -

    Get reviews by name using a case insensitive startswith query

    +get_review_by_name(name, limit=100, offset=0, fields=None, raw_response=False) +

    Get list of reviews by name using a case insensitive startswith query

    Parameters:
    -

    name – String - Name of the review

    +
      +
    • name (str) – Name of the review

    • +
    • limit (int) – Limit the number of results

    • +
    • offset (int) – Offset the results

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    Dict with meta information and an array of found projects

    @@ -366,26 +457,37 @@

    SyncSketch Python API Library documentation
    -get_review_by_id(review_id)
    +get_review_by_id(review_id, fields=None, raw_response=False)

    Get single review by id.

    Parameters:
    -

    review_id – Number

    +
      +
    • review_id – Number

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:
    -

    Review Dict

    +

    Review Data

    +
    +
    Return type:
    +

    dict

    -get_review_by_uuid(uuid)
    +get_review_by_uuid(uuid, fields=None, raw_response=False)

    Get single review by uuid. UUID can be found in the review URL e.g. syncsketch.com/sketch/<uuid>/

    Parameters:
    -

    uuid (str) – UUID of the review.

    +
      +
    • uuid (str) – UUID of the review.

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    Review dict

    @@ -398,30 +500,34 @@

    SyncSketch Python API Library documentation
    -get_review_storage(review_id)
    +get_review_storage(review_id, raw_response=False)

    Get review storage usage in bytes

    Parameters:
    -

    review_id (int) – Review ID

    +
      +
    • review_id (int) – Review ID

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    Storage usage in bytes

    Return type:
    -

    int

    +

    dict[str, int]

    -update_review(review_id, data)
    +update_review(review_id, data, raw_response=False)

    Update a review

    Parameters:
    • review_id (int) – the id of the item

    • data (dict) – dict with data for item

    • +
    • raw_response (bool) – Get whole response from REST API.

    Returns:
    @@ -435,7 +541,7 @@

    SyncSketch Python API Library documentation
    -sort_review_items(review_id, items)
    +sort_review_items(review_id, items, raw_response=False)

    Update a review

    Example items param

    Returns:
    @@ -467,55 +574,165 @@

    SyncSketch Python API Library documentation
    -archive_review(review_id)
    +archive_review(review_id, raw_response=True)

    Archive a review

    Parameters:
    -

    review_id (int)

    +
      +
    • review_id (int)

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:
    -

    empty response

    +

    Response object

    -restore_review(review_id)
    +restore_review(review_id, raw_response=True)

    Restore (unarchive) a review

    Parameters:
    -

    review_id (int)

    +
      +
    • review_id (int)

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:
    -

    empty response

    +

    Response object

    -delete_review(review_id)
    +delete_review(review_id, raw_response=False)

    Delete a review by id.

    Parameters:
    -

    review_id (int) – Review ID to delete

    +
      +
    • review_id (int) – Review ID to delete

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:
    -

    +

    Review data

    +
    +
    Return type:
    +

    dict

    +
    +
    +
    + +
    +
    +create_review_section(review_id, name, item_ids, uuid=None, raw_response=False)
    +

    Create a new review section

    +
    +
    Parameters:
    +
      +
    • review_id (int) – Review ID

    • +
    • name (str) – Section name

    • +
    • item_ids (list) – List of item IDs to add to the section

    • +
    • uuid (str) – Optional UUID for the section

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    +
    +
    Returns:
    +

    Section data

    +
    +
    Return type:
    +

    dict

    +
    +
    +
    + +
    +
    +update_review_sections(review_id, data, raw_response=False)
    +

    Update one or more review sections

    +
    # Example data
    +sections_to_update = [
    +    {
    +        "uuid": "section-uuid",
    +        "name": "New Section Name",
    +        "itemIds": [1, 2, 3],
    +    }
    +]
    +
    +
    +
    +
    Parameters:
    +
      +
    • review_id (int) – Review ID

    • +
    • data (list[dict]) – Section data

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    +
    +
    Returns:
    +

    Section data

    +
    +
    Return type:
    +

    dict

    +
    +
    +
    + +
    +
    +delete_review_section(review_id, section_uuid, raw_response=False)
    +

    Delete a review section

    +
    +
    Parameters:
    +
      +
    • review_id (int) – Review ID

    • +
    • section_uuid (str) – Section UUID

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    +
    +
    Returns:
    +

    Section data

    +
    +
    Return type:
    +

    dict

    +
    +
    +
    + +
    +
    +get_item(item_id, data=None, fields=None, raw_response=False)
    +

    Get single item by id

    +
    +
    Parameters:
    +
      +
    • item_id (int)

    • +
    • data (dict)

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    +
    +
    Returns:
    +

    Item data

    +
    +
    Return type:
    +

    dict

    -update_item(item_id, data)
    +update_item(item_id, data, raw_response=False)

    Update an item

    Parameters:
    • item_id (int) – the id of the item

    • data (dict) – dict with data for item

    • +
    • raw_response (bool) – Get whole response from REST API.

    Returns:
    @@ -529,7 +746,7 @@

    SyncSketch Python API Library documentation
    -add_item(review_id, name, fps, additional_data)
    +add_item(review_id, name, fps, additional_data, raw_response=False)

    create a media item record and connect it to a review. This should be used in case you want to add items with externaly hosted media by passing in the external_url and external_thumbnail_url to the additionalData dict e.g

    additionalData = {
    @@ -560,6 +777,7 @@ 

    SyncSketch Python API Library documentationReturns: @@ -621,6 +839,32 @@

    SyncSketch Python API Library documentation +
    +upload_file(review_id, filepath, file_name='', item_uuid=None, noConvertFlag=False, chunk_size=5242880, max_workers=None)
    +

    Upload a file to a review using multipart upload. +This uses direct to s3 multipart upload to upload large files in chunks.

    +
    +
    Parameters:
    +
      +
    • review_id (int) – Required review_id

    • +
    • filepath (str) – Path for the file on disk e.g /tmp/movie.webm

    • +
    • file_name (str) – The name of the file. Please make sure to pass the correct file extension

    • +
    • item_uuid (str) – Optional UUID for the item. If not provided, a new one will be generated by the server

    • +
    • noConvertFlag (bool) – The video you are uploading is already in a browser compatible format

    • +
    • chunk_size (int) – Size of each chunk in bytes for multipart upload (default: 5MB)

    • +
    • max_workers (int) – Maximum number of parallel upload workers (default: auto-detected based on system capabilities)

    • +
    +
    +
    Returns:
    +

    A dict containing item information including “id” and “uuid” or None on failure

    +
    +
    Return type:
    +

    Optional[dict]

    +
    +
    +

    +
    add_media_v2(review_id, filepath, file_name='', item_uuid=None, noConvertFlag=False)
    @@ -647,15 +891,17 @@

    SyncSketch Python API Library documentation
    -get_media(searchCriteria)
    +get_media(searchCriteria, fields=None, raw_response=False)

    This is a general search function. You can search media items by

    -

    ‘id’ -‘name’ -‘status’ -‘active’ -‘creator’: ALL_WITH_RELATIONS, <– these are foreign key queries -‘reviews’: ALL_WITH_RELATIONS, <– these are foreign key queries -‘created’ using ‘exact’, ‘range’, ‘gt’, ‘gte’, ‘lt’, ‘lte’

    +
      +
    • ‘id’

    • +
    • ‘name’

    • +
    • ‘status’

    • +
    • ‘active’

    • +
    • ‘creator’: ALL_WITH_RELATIONS, <– these are foreign key queries

    • +
    • ‘reviews’: ALL_WITH_RELATIONS, <– these are foreign key queries

    • +
    • ‘created’ using ‘exact’, ‘range’, ‘gt’, ‘gte’, ‘lt’, ‘lte’

    • +

    To query items by foreign keys please use the foreign key syntax described in the Django search definition: https://docs.djangoproject.com/en/1.11/topics/db/queries/

    If you want to query by “review name” for example you would pass in

    @@ -663,12 +909,18 @@

    SyncSketch Python API Library documentation
    results = s.getMedia({'reviews__project__name':'test', 'limit': 1, 'active': 1})
    +
    +

    NOTE: Please make sure to include the active:1 query if you only want active media. Deleted files are currently only deactivated and kept for a certain period of time before they are “purged” from the system.

    Parameters:
    -

    searchCriteria (dict) – Search params

    +
      +
    • searchCriteria (dict) – Search params

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    List of media items

    @@ -681,11 +933,15 @@

    SyncSketch Python API Library documentation
    -get_items_by_review_id(review_id)
    +get_items_by_review_id(review_id, fields=None, raw_response=False)

    Get all items in a review

    Parameters:
    -

    review_id (int) – Review ID

    +
      +
    • review_id (int) – Review ID

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    List of media items

    @@ -698,11 +954,14 @@

    SyncSketch Python API Library documentation
    -delete_item(item_id)
    +delete_item(item_id, raw_response=False)

    Delete a item by id.

    Parameters:
    -

    item_id (int) – Item ID to delete

    +
      +
    • item_id (int) – Item ID to delete

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    @@ -712,11 +971,14 @@

    SyncSketch Python API Library documentation
    -bulk_delete_items(item_ids)
    +bulk_delete_items(item_ids, raw_response=True)

    Delete multiple items by id.

    Parameters:
    -

    item_ids (list[int]) – List of item IDs to delete

    +
      +
    • item_ids (list[int]) – List of item IDs to delete

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    @@ -726,7 +988,7 @@

    SyncSketch Python API Library documentation
    -move_items(new_review_id, item_data)
    +move_items(new_review_id, item_data, raw_response=True)

    Move items from one review to another

    item_data should be a list of dictionaries with the old review id and the item id. The items in the list will be moved to the new review for the param new_review_id

    @@ -745,6 +1007,7 @@

    SyncSketch Python API Library documentation
    • new_review_id (int) – The review id to move the items to

    • item_data (list[dict]) – List of dictionaries with the old review id and the item id

    • +
    • raw_response (bool) – Get whole response from REST API.

    Returns:
    @@ -755,7 +1018,7 @@

    SyncSketch Python API Library documentation
    -add_comment(item_id, text, review_id, frame=0)
    +add_comment(item_id, text, review_id, frame=0, raw_response=False)

    Add a comment to an item

    Parameters:
    @@ -764,6 +1027,7 @@

    SyncSketch Python API Library documentationReturns: @@ -774,7 +1038,7 @@

    SyncSketch Python API Library documentation
    -get_annotations(item_id, revisionId=False, review_id=False)
    +get_annotations(item_id, revisionId=False, review_id=False, raw_response=False)

    Get sketches and comments for an item. Frames have a revision id which signifies a “set of notes”. When querying an item you’ll get the available revisions for this item. If you wish to get only the latest revision, please get the revisionId for the latest revision.

    @@ -784,6 +1048,7 @@

    SyncSketch Python API Library documentationReturns: @@ -794,7 +1059,7 @@

    SyncSketch Python API Library documentation
    -get_flattened_annotations(review_id, item_id, with_tracing_paper=False, return_as_base64=False)
    +get_flattened_annotations(review_id, item_id, with_tracing_paper=False, return_as_base64=False, raw_response=False)

    Returns a list of sketches either as signed urls from s3 or base64 encoded strings. The sketches are composited over the background frame of the item.

    @@ -804,6 +1069,7 @@

    SyncSketch Python API Library documentationReturns: @@ -838,11 +1104,15 @@

    SyncSketch Python API Library documentation
    -get_users_by_name(name)
    +get_users_by_name(name, fields=None, raw_response=False)

    Name is a combined search and will search in first_name, last_name and email

    Parameters:
    -

    name (str) – Name to search for

    +
      +
    • name (str) – Name to search for

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    List of users

    @@ -855,11 +1125,15 @@

    SyncSketch Python API Library documentation
    -get_user_by_email(email)
    +get_user_by_email(email, fields=None, raw_response=True)

    Get user by email

    Parameters:
    -

    email (str) – Email to search for

    +
      +
    • email (str) – Email to search for

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    User data

    @@ -872,11 +1146,14 @@

    SyncSketch Python API Library documentation
    -get_users_by_project_id(project_id)
    +get_users_by_project_id(project_id, raw_response=False)

    Get all users in a project

    Parameters:
    -

    project_id (int)

    +
      +
    • project_id (int)

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    List of users

    @@ -889,7 +1166,7 @@

    SyncSketch Python API Library documentation
    -get_connections_by_user_id(user_id, account_id, include_inactive=None, include_archived=None)
    +get_connections_by_user_id(user_id, account_id, include_inactive=None, include_archived=None, raw_response=False)

    Get all project and account connections for a user. Good for checking access for a user that might have left…

    Parameters:
    @@ -898,6 +1175,7 @@

    SyncSketch Python API Library documentationReturns: @@ -911,11 +1189,15 @@

    SyncSketch Python API Library documentation
    -get_user_by_id(user_id)
    +get_user_by_id(user_id, fields=None, raw_response=False)

    Get a user by ID

    Parameters:
    -

    user_id (int)

    +
      +
    • user_id (int)

    • +
    • fields (list|str|int|bool) – fields to fetch from backend

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    User data

    @@ -928,7 +1210,7 @@

    SyncSketch Python API Library documentation
    -add_users_to_workspace(workspace_id, users, note='')
    +add_users_to_workspace(workspace_id, users, note='', raw_response=False)

    Add Users to Workspace

    users=[{"email":"test@test.de","permission":"admin"}]
     
    @@ -939,6 +1221,7 @@

    SyncSketch Python API Library documentationReturns: @@ -949,7 +1232,7 @@

    SyncSketch Python API Library documentation
    -remove_users_from_workspace(workspace_id, users)
    +remove_users_from_workspace(workspace_id, users, raw_response=False)

    Remove a list of users from a workspace Can remove by id or email

    Returns:
    @@ -970,7 +1254,7 @@

    SyncSketch Python API Library documentation
    -add_users_to_project(project_id, users, note='')
    +add_users_to_project(project_id, users, note='', raw_response=False)

    Add Users to Project

    possible permissions

    @@ -1016,7 +1302,7 @@

    SyncSketch Python API Library documentation
    -shotgrid_create_config(syncsketch_account_id, syncsketch_project_id=None, data=None)
    +shotgrid_create_config(syncsketch_account_id, syncsketch_project_id=None, data=None, raw_response=True)

    Create a new Shotgrid configuration for a SyncSketch workspace and optionally a project

    Parameters:
    @@ -1024,6 +1310,7 @@

    SyncSketch Python API Library documentationReturns: @@ -1034,7 +1321,7 @@

    SyncSketch Python API Library documentation
    -shotgrid_get_playlists(syncsketch_account_id, syncsketch_project_id, shotgun_project_id=None)
    +shotgrid_get_playlists(syncsketch_account_id, syncsketch_project_id, shotgun_project_id=None, raw_response=False)

    Returns list of Shotgrid playlists modified in the last 120 days If the syncsketch project is directly linked to a shotgrid by the workspace admin, the param shotgun_project_id will be ignored and can be omitted during the function call

    @@ -1044,6 +1331,7 @@

    SyncSketch Python API Library documentationReturns: @@ -1054,7 +1342,7 @@

    SyncSketch Python API Library documentation
    -shotgrid_sync_review_notes(review_id)
    +shotgrid_sync_review_notes(review_id, raw_response=False)

    Sync notes from SyncSketch review to the original shotgrid playlist Returns task id to use in get_shotgun_sync_review_notes_progress to get progress

    returns dict with information about the REST API call:

    @@ -1069,7 +1357,10 @@

    SyncSketch Python API Library documentation
    Parameters:
    -

    review_id (int) – SyncSketch review id

    +
      +
    • review_id (int) – SyncSketch review id

    • +
    • raw_response (bool) – Get whole response from REST API.

    • +
    Returns:

    Progress information

    @@ -1082,7 +1373,7 @@

    SyncSketch Python API Library documentation
    -shotgrid_sync_new_item_notes(project_id, review_id, item_id)
    +shotgrid_sync_new_item_notes(project_id, review_id, item_id, raw_response=False)

    Sync new notes from SyncSketch review item to the original shotgrid playlist Returns dict with information about the REST API call

      @@ -1098,6 +1389,7 @@

      SyncSketch Python API Library documentationReturns: @@ -1108,7 +1400,7 @@

      SyncSketch Python API Library documentation
      -get_shotgrid_sync_review_notes_progress(task_id)
      +get_shotgrid_sync_review_notes_progress(task_id, raw_response=False)

      Returns status of review notes sync for the task id provided in shotgun_sync_review_notes

      Returns a dict with the following keys:

        @@ -1122,7 +1414,10 @@

        SyncSketch Python API Library documentation
        Parameters:
        -

        task_id (str) – UUID of the task returned by shotgrid_sync_review_notes

        +
          +
        • task_id (str) – UUID of the task returned by shotgrid_sync_review_notes

        • +
        • raw_response (bool) – Get whole response from REST API.

        • +
        Returns:

        Progress information

        @@ -1267,7 +1562,7 @@

        SyncSketch Python API Library documentation
        -shotgun_sync_new_item_notes(project_id, review_id, item_id)
        +shotgun_sync_new_item_notes(project_id, review_id, item_id, raw_response=False)

        Sync new notes from SyncSketch review item to the original shotgrid playlist Returns dict with information about the REST API call

          @@ -1283,6 +1578,7 @@

          SyncSketch Python API Library documentationReturns: @@ -1293,7 +1589,7 @@

          SyncSketch Python API Library documentation
          -shotgun_sync_review_notes(review_id)
          +shotgun_sync_review_notes(review_id, raw_response=False)

          Sync notes from SyncSketch review to the original shotgrid playlist Returns task id to use in get_shotgun_sync_review_notes_progress to get progress

          returns dict with information about the REST API call:

          @@ -1308,7 +1604,10 @@

          SyncSketch Python API Library documentation
          Parameters:
          -

          review_id (int) – SyncSketch review id

          +
            +
          • review_id (int) – SyncSketch review id

          • +
          • raw_response (bool) – Get whole response from REST API.

          • +
          Returns:

          Progress information

          @@ -1321,7 +1620,7 @@

          SyncSketch Python API Library documentation
          -shotgun_get_playlists(syncsketch_account_id, syncsketch_project_id, shotgun_project_id=None)
          +shotgun_get_playlists(syncsketch_account_id, syncsketch_project_id, shotgun_project_id=None, raw_response=False)

          Returns list of Shotgrid playlists modified in the last 120 days If the syncsketch project is directly linked to a shotgrid by the workspace admin, the param shotgun_project_id will be ignored and can be omitted during the function call

          @@ -1331,6 +1630,7 @@

          SyncSketch Python API Library documentationReturns: @@ -1341,7 +1641,7 @@

          SyncSketch Python API Library documentation
          -shotgun_create_config(syncsketch_account_id, syncsketch_project_id=None, data=None)
          +shotgun_create_config(syncsketch_account_id, syncsketch_project_id=None, data=None, raw_response=True)

          Create a new Shotgrid configuration for a SyncSketch workspace and optionally a project

          Parameters:
          @@ -1349,6 +1649,7 @@

          SyncSketch Python API Library documentationReturns: @@ -1388,25 +1689,24 @@

          SyncSketch Python API Library

          -

          Navigation

          -
          -

          Related Topics

          - -
          - +

          Navigation

          + +
          +

          Related Topics

          + +
          @@ -1423,8 +1723,8 @@

          Quick search

          ©2024, Brady Endres, Phil Floetotto, Nicholas Kegler dos Santos, Tyler Nickerson. | - Powered by Sphinx 7.4.7 - & Alabaster 0.7.16 + Powered by Sphinx 9.1.0 + & Alabaster 1.0.0 | Search — SyncSketch Python API Library 1.0.10.5 documentation - - + + + - - + + @@ -104,8 +105,8 @@

          Related Topics

          ©2024, Brady Endres, Phil Floetotto, Nicholas Kegler dos Santos, Tyler Nickerson. | - Powered by
          Sphinx 7.4.7 - & Alabaster 0.7.16 + Powered by Sphinx 9.1.0 + & Alabaster 1.0.0 diff --git a/_build/searchindex.js b/_build/searchindex.js index 8d853b2..f1b0d26 100644 --- a/_build/searchindex.js +++ b/_build/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles": {"SyncSketch Python API Library documentation": [[0, null]]}, "docnames": ["index"], "envversion": {"sphinx": 62, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["index.rst"], "indexentries": {"add_comment() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.add_comment", false]], "add_item() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.add_item", false]], "add_media() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.add_media", false]], "add_media_by_url() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.add_media_by_url", false]], "add_media_v1() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.add_media_v1", false]], "add_media_v2() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.add_media_v2", false]], "add_users_to_project() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.add_users_to_project", false]], "add_users_to_workspace() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.add_users_to_workspace", false]], "archive_project() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.archive_project", false]], "archive_review() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.archive_review", false]], "bulk_delete_items() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.bulk_delete_items", false]], "create_project() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.create_project", false]], "delete_item() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.delete_item", false]], "delete_project() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.delete_project", false]], "delete_review() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.delete_review", false]], "duplicate_project() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.duplicate_project", false]], "get_accounts() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_accounts", false]], "get_annotations() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_annotations", false]], "get_connections_by_user_id() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_connections_by_user_id", false]], "get_flattened_annotations() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_flattened_annotations", false]], "get_grease_pencil_overlays() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_grease_pencil_overlays", false]], "get_items_by_review_id() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_items_by_review_id", false]], "get_media() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_media", false]], "get_project_by_id() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_project_by_id", false]], "get_project_storage() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_project_storage", false]], "get_projects() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_projects", false]], "get_projects_by_name() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_projects_by_name", false]], "get_review_by_id() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_review_by_id", false]], "get_review_by_name() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_review_by_name", false]], "get_review_by_uuid() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_review_by_uuid", false]], "get_review_storage() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_review_storage", false]], "get_reviews_by_project_id() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_reviews_by_project_id", false]], "get_shotgrid_sync_review_items_progress() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_shotgrid_sync_review_items_progress", false]], "get_shotgrid_sync_review_notes_progress() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_shotgrid_sync_review_notes_progress", false]], "get_shotgun_sync_review_items_progress() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_shotgun_sync_review_items_progress", false]], "get_tree() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_tree", false]], "get_user_by_email() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_user_by_email", false]], "get_user_by_id() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_user_by_id", false]], "get_users_by_name() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_users_by_name", false]], "get_users_by_project_id() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.get_users_by_project_id", false]], "is_connected() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.is_connected", false]], "move_items() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.move_items", false]], "remove_users_from_project() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.remove_users_from_project", false]], "remove_users_from_workspace() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.remove_users_from_workspace", false]], "restore_project() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.restore_project", false]], "restore_review() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.restore_review", false]], "shotgrid_create_config() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.shotgrid_create_config", false]], "shotgrid_get_playlists() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.shotgrid_get_playlists", false]], "shotgrid_sync_new_item_notes() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.shotgrid_sync_new_item_notes", false]], "shotgrid_sync_review_items() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.shotgrid_sync_review_items", false]], "shotgrid_sync_review_notes() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.shotgrid_sync_review_notes", false]], "shotgun_create_config() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.shotgun_create_config", false]], "shotgun_get_playlists() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.shotgun_get_playlists", false]], "shotgun_get_projects() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.shotgun_get_projects", false]], "shotgun_sync_new_item_notes() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.shotgun_sync_new_item_notes", false]], "shotgun_sync_review_items() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.shotgun_sync_review_items", false]], "shotgun_sync_review_notes() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.shotgun_sync_review_notes", false]], "sort_review_items() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.sort_review_items", false]], "syncsketchapi (class in syncsketch)": [[0, "syncsketch.SyncSketchAPI", false]], "update_account() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.update_account", false]], "update_item() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.update_item", false]], "update_project() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.update_project", false]], "update_review() (syncsketch.syncsketchapi method)": [[0, "syncsketch.SyncSketchAPI.update_review", false]]}, "objects": {"syncsketch": [[0, 0, 1, "", "SyncSketchAPI"]], "syncsketch.SyncSketchAPI": [[0, 1, 1, "", "add_comment"], [0, 1, 1, "", "add_item"], [0, 1, 1, "", "add_media"], [0, 1, 1, "", "add_media_by_url"], [0, 1, 1, "", "add_media_v1"], [0, 1, 1, "", "add_media_v2"], [0, 1, 1, "", "add_users_to_project"], [0, 1, 1, "", "add_users_to_workspace"], [0, 1, 1, "", "archive_project"], [0, 1, 1, "", "archive_review"], [0, 1, 1, "", "bulk_delete_items"], [0, 1, 1, "", "create_project"], [0, 1, 1, "", "delete_item"], [0, 1, 1, "", "delete_project"], [0, 1, 1, "", "delete_review"], [0, 1, 1, "", "duplicate_project"], [0, 1, 1, "", "get_accounts"], [0, 1, 1, "", "get_annotations"], [0, 1, 1, "", "get_connections_by_user_id"], [0, 1, 1, "", "get_flattened_annotations"], [0, 1, 1, "", "get_grease_pencil_overlays"], [0, 1, 1, "", "get_items_by_review_id"], [0, 1, 1, "", "get_media"], [0, 1, 1, "", "get_project_by_id"], [0, 1, 1, "", "get_project_storage"], [0, 1, 1, "", "get_projects"], [0, 1, 1, "", "get_projects_by_name"], [0, 1, 1, "", "get_review_by_id"], [0, 1, 1, "", "get_review_by_name"], [0, 1, 1, "", "get_review_by_uuid"], [0, 1, 1, "", "get_review_storage"], [0, 1, 1, "", "get_reviews_by_project_id"], [0, 1, 1, "", "get_shotgrid_sync_review_items_progress"], [0, 1, 1, "", "get_shotgrid_sync_review_notes_progress"], [0, 1, 1, "", "get_shotgun_sync_review_items_progress"], [0, 1, 1, "", "get_tree"], [0, 1, 1, "", "get_user_by_email"], [0, 1, 1, "", "get_user_by_id"], [0, 1, 1, "", "get_users_by_name"], [0, 1, 1, "", "get_users_by_project_id"], [0, 1, 1, "", "is_connected"], [0, 1, 1, "", "move_items"], [0, 1, 1, "", "remove_users_from_project"], [0, 1, 1, "", "remove_users_from_workspace"], [0, 1, 1, "", "restore_project"], [0, 1, 1, "", "restore_review"], [0, 1, 1, "", "shotgrid_create_config"], [0, 1, 1, "", "shotgrid_get_playlists"], [0, 1, 1, "", "shotgrid_sync_new_item_notes"], [0, 1, 1, "", "shotgrid_sync_review_items"], [0, 1, 1, "", "shotgrid_sync_review_notes"], [0, 1, 1, "", "shotgun_create_config"], [0, 1, 1, "", "shotgun_get_playlists"], [0, 1, 1, "", "shotgun_get_projects"], [0, 1, 1, "", "shotgun_sync_new_item_notes"], [0, 1, 1, "", "shotgun_sync_review_items"], [0, 1, 1, "", "shotgun_sync_review_notes"], [0, 1, 1, "", "sort_review_items"], [0, 1, 1, "", "update_account"], [0, 1, 1, "", "update_item"], [0, 1, 1, "", "update_project"], [0, 1, 1, "", "update_review"]]}, "objnames": {"0": ["py", "class", "Python class"], "1": ["py", "method", "Python method"]}, "objtypes": {"0": "py:class", "1": "py:method"}, "terms": {"": 0, "0": 0, "03": 0, "1": 0, "10": 0, "100": 0, "1024": 0, "11": 0, "120": 0, "12345": 0, "2": 0, "200": 0, "2015": 0, "2017": 0, "24": 0, "3": 0, "3d": 0, "51": 0, "52": 0, "720": 0, "98": 0, "A": 0, "For": 0, "If": 0, "In": 0, "It": 0, "TO": 0, "The": 0, "To": 0, "Will": 0, "__": 0, "__init__": 0, "about": 0, "access": 0, "account": 0, "account_id": 0, "accountid": 0, "activ": 0, "ad": 0, "add": 0, "add_com": 0, "add_item": 0, "add_media": 0, "add_media_by_url": 0, "add_media_v1": 0, "add_media_v2": 0, "add_users_to_project": 0, "add_users_to_workspac": 0, "addit": 0, "additem": 0, "additional_data": 0, "additionaldata": 0, "admin": 0, "after": 0, "against": 0, "all": 0, "all_with_rel": 0, "alreadi": 0, "also": 0, "alwai": 0, "an": 0, "ani": 0, "annot": 0, "anoth": 0, "api_kei": 0, "api_vers": 0, "applic": 0, "ar": 0, "archiv": 0, "archive_project": 0, "archive_review": 0, "arrai": 0, "artist": 0, "artist_nam": 0, "associ": 0, "attach": 0, "auth": 0, "authent": 0, "author": 0, "autodesk": 0, "autodoc_member_ord": 0, "automat": 0, "avail": 0, "aw": 0, "background": 0, "base": 0, "base64": 0, "befor": 0, "being": 0, "bool": 0, "bradi": 0, "browser": 0, "bulk_delete_item": 0, "bysourc": 0, "byte": 0, "caa": 0, "call": 0, "can": 0, "case": 0, "certain": 0, "check": 0, "class": 0, "cloud": 0, "cloudhelp": 0, "code": 0, "collabor": 0, "com": 0, "combin": 0, "command": 0, "comment": 0, "commun": 0, "compat": 0, "complet": 0, "composit": 0, "configur": 0, "connect": 0, "constructor": 0, "contain": 0, "content": 0, "conveni": 0, "convert": 0, "copi": 0, "copy_review": 0, "copy_set": 0, "copy_us": 0, "correct": 0, "creat": 0, "create_project": 0, "creator": 0, "current": 0, "dai": 0, "data": 0, "db": 0, "de": 0, "deactiv": 0, "debug": 0, "definit": 0, "delet": 0, "delete_item": 0, "delete_project": 0, "delete_review": 0, "deprecationwarn": 0, "describ": 0, "descript": 0, "determin": 0, "dict": 0, "dictionari": 0, "directli": 0, "disk": 0, "django": 0, "djangoproject": 0, "doc": 0, "doe": 0, "done": 0, "down": 0, "download": 0, "dropbox": 0, "duplicate_project": 0, "durat": 0, "dure": 0, "e": 0, "either": 0, "email": 0, "empti": 0, "en": 0, "enabl": 0, "encod": 0, "endr": 0, "enu": 0, "error": 0, "etc": 0, "even": 0, "exact": 0, "exampl": 0, "exist": 0, "expir": 0, "explor": 0, "extens": 0, "external_thumbnail_url": 0, "external_url": 0, "externali": 0, "fail": 0, "failur": 0, "fals": 0, "field": 0, "file": 0, "file_nam": 0, "filepath": 0, "find": 0, "first_nam": 0, "float": 0, "follow": 0, "foreign": 0, "format": 0, "found": 0, "fp": 0, "frame": 0, "from": 0, "full": 0, "function": 0, "g": 0, "gener": 0, "get": 0, "get_account": 0, "get_annot": 0, "get_connections_by_user_id": 0, "get_flattened_annot": 0, "get_grease_pencil_overlai": 0, "get_items_by_review_id": 0, "get_media": 0, "get_project": 0, "get_project_by_id": 0, "get_project_storag": 0, "get_projects_by_nam": 0, "get_review_by_id": 0, "get_review_by_nam": 0, "get_review_by_uuid": 0, "get_review_storag": 0, "get_reviews_by_project_id": 0, "get_shotgrid_sync_review_items_progress": 0, "get_shotgrid_sync_review_notes_progress": 0, "get_shotgun_sync_review_items_progress": 0, "get_shotgun_sync_review_notes_progress": 0, "get_tre": 0, "get_user_by_email": 0, "get_user_by_id": 0, "get_users_by_nam": 0, "get_users_by_project_id": 0, "getaccount": 0, "getmedia": 0, "good": 0, "greas": 0, "greasepencil": 0, "gt": 0, "gte": 0, "ha": 0, "have": 0, "header": 0, "height": 0, "here": 0, "homedir": 0, "host": 0, "hour": 0, "htm": 0, "html": 0, "http": 0, "i": 0, "id": 0, "ignor": 0, "imag": 0, "import": 0, "improv": 0, "inact": 0, "includ": 0, "include_archiv": 0, "include_connect": 0, "include_delet": 0, "include_inact": 0, "include_tag": 0, "info": 0, "inform": 0, "insensit": 0, "instead": 0, "int": 0, "interact": 0, "intern": 0, "invit": 0, "is_connect": 0, "is_publ": 0, "item": 0, "item_data": 0, "item_id": 0, "item_nam": 0, "item_uuid": 0, "itemparentid": 0, "items_to_mov": 0, "jpg": 0, "kei": 0, "kept": 0, "knowledg": 0, "last": 0, "last_nam": 0, "latest": 0, "learn": 0, "left": 0, "like": 0, "limit": 0, "link": 0, "list": 0, "ll": 0, "load": 0, "lt": 0, "lte": 0, "make": 0, "manag": 0, "maya": 0, "media": 0, "media_url": 0, "mediaurl": 0, "member": 0, "messag": 0, "meta": 0, "method": 0, "might": 0, "mode": 0, "model": 0, "modifi": 0, "more": 0, "most": 0, "move": 0, "move_item": 0, "movi": 0, "much": 0, "multipl": 0, "name": 0, "narrow": 0, "need": 0, "nest": 0, "new": 0, "new_review_id": 0, "noconvertflag": 0, "none": 0, "normal": 0, "note": 0, "number": 0, "obj": 0, "object": 0, "offset": 0, "old": 0, "omit": 0, "one": 0, "onli": 0, "onlin": 0, "option": 0, "origin": 0, "out": 0, "output": 0, "over": 0, "overlai": 0, "own": 0, "paper": 0, "param": 0, "paramet": 0, "pass": 0, "path": 0, "payload": 0, "pencil": 0, "per": 0, "percent": 0, "percent_complet": 0, "perform": 0, "period": 0, "permiss": 0, "player": 0, "playlist": 0, "playlist_cod": 0, "playlist_id": 0, "pleas": 0, "png": 0, "possibl": 0, "process": 0, "progress": 0, "progress_url": 0, "project": 0, "project_id": 0, "provid": 0, "pull": 0, "purg": 0, "queri": 0, "rain": 0, "rang": 0, "recommend": 0, "record": 0, "regardless": 0, "remaining_item": 0, "remov": 0, "remove_users_from_project": 0, "remove_users_from_workspac": 0, "requir": 0, "respons": 0, "rest": 0, "restor": 0, "restore_project": 0, "restore_review": 0, "result": 0, "retriev": 0, "return": 0, "return_as_base64": 0, "review": 0, "review_id": 0, "review_link": 0, "reviews__nam": 0, "reviews__project__nam": 0, "revis": 0, "revisionid": 0, "s3": 0, "schema": 0, "search": 0, "searchcriteria": 0, "second": 0, "server": 0, "servic": 0, "set": 0, "shotgrid": 0, "shotgrid_create_config": 0, "shotgrid_get_playlist": 0, "shotgrid_sync_new_item_not": 0, "shotgrid_sync_review_item": 0, "shotgrid_sync_review_not": 0, "shotgun_create_config": 0, "shotgun_get_playlist": 0, "shotgun_get_project": 0, "shotgun_project_id": 0, "shotgun_sync_new_item_not": 0, "shotgun_sync_review_item": 0, "shotgun_sync_review_not": 0, "should": 0, "sign": 0, "signifi": 0, "similar": 0, "singl": 0, "size": 0, "sketch": 0, "sketch_upload_error": 0, "some": 0, "sort": 0, "sort_review_item": 0, "sortord": 0, "sourc": 0, "specif": 0, "speed": 0, "stabil": 0, "start": 0, "startswith": 0, "statu": 0, "storag": 0, "str": 0, "string": 0, "success": 0, "summari": 0, "support": 0, "sure": 0, "sync": 0, "syncsketch_account_id": 0, "syncsketch_project_id": 0, "syncsketchapi": 0, "syntax": 0, "system": 0, "tag": 0, "task": 0, "task_id": 0, "test": 0, "text": 0, "thei": 0, "thi": 0, "time": 0, "tmp": 0, "token": 0, "tool": 0, "topic": 0, "total_item": 0, "trace": 0, "tree": 0, "true": 0, "try": 0, "type": 0, "unarch": 0, "unlik": 0, "up": 0, "updat": 0, "update_account": 0, "update_item": 0, "update_project": 0, "update_review": 0, "updated_item": 0, "upload": 0, "url": 0, "us": 0, "usag": 0, "use_header_auth": 0, "useexpiringtoken": 0, "user": 0, "user_id": 0, "usernam": 0, "uuid": 0, "v1": 0, "valu": 0, "veri": 0, "version": 0, "video": 0, "viewer": 0, "visit": 0, "wa": 0, "want": 0, "we": 0, "webm": 0, "when": 0, "whether": 0, "which": 0, "width": 0, "wish": 0, "with_tracing_pap": 0, "withitem": 0, "without": 0, "work": 0, "workspac": 0, "workspace_id": 0, "would": 0, "wp": 0, "writabl": 0, "www": 0, "xml": 0, "yet": 0, "you": 0, "your": 0, "zip": 0}, "titles": ["SyncSketch Python API Library documentation"], "titleterms": {"api": 0, "document": 0, "librari": 0, "python": 0, "syncsketch": 0}}) \ No newline at end of file +Search.setIndex({"alltitles":{"SyncSketch Python API Library documentation":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{"add_comment() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.add_comment",false]],"add_item() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.add_item",false]],"add_media() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.add_media",false]],"add_media_by_url() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.add_media_by_url",false]],"add_media_v1() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.add_media_v1",false]],"add_media_v2() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.add_media_v2",false]],"add_users_to_project() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.add_users_to_project",false]],"add_users_to_workspace() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.add_users_to_workspace",false]],"archive_project() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.archive_project",false]],"archive_review() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.archive_review",false]],"bulk_delete_items() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.bulk_delete_items",false]],"create_project() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.create_project",false]],"create_review() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.create_review",false]],"create_review_section() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.create_review_section",false]],"delete_item() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.delete_item",false]],"delete_project() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.delete_project",false]],"delete_review() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.delete_review",false]],"delete_review_section() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.delete_review_section",false]],"duplicate_project() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.duplicate_project",false]],"get_accounts() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_accounts",false]],"get_annotations() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_annotations",false]],"get_connections_by_user_id() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_connections_by_user_id",false]],"get_flattened_annotations() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_flattened_annotations",false]],"get_grease_pencil_overlays() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_grease_pencil_overlays",false]],"get_item() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_item",false]],"get_items_by_review_id() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_items_by_review_id",false]],"get_media() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_media",false]],"get_project_by_id() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_project_by_id",false]],"get_project_storage() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_project_storage",false]],"get_projects() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_projects",false]],"get_projects_by_name() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_projects_by_name",false]],"get_review_by_id() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_review_by_id",false]],"get_review_by_name() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_review_by_name",false]],"get_review_by_uuid() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_review_by_uuid",false]],"get_review_storage() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_review_storage",false]],"get_reviews_by_project_id() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_reviews_by_project_id",false]],"get_shotgrid_sync_review_items_progress() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_shotgrid_sync_review_items_progress",false]],"get_shotgrid_sync_review_notes_progress() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_shotgrid_sync_review_notes_progress",false]],"get_shotgun_sync_review_items_progress() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_shotgun_sync_review_items_progress",false]],"get_tree() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_tree",false]],"get_user_by_email() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_user_by_email",false]],"get_user_by_id() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_user_by_id",false]],"get_users_by_name() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_users_by_name",false]],"get_users_by_project_id() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.get_users_by_project_id",false]],"is_connected() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.is_connected",false]],"move_items() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.move_items",false]],"remove_users_from_project() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.remove_users_from_project",false]],"remove_users_from_workspace() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.remove_users_from_workspace",false]],"restore_project() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.restore_project",false]],"restore_review() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.restore_review",false]],"shotgrid_create_config() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.shotgrid_create_config",false]],"shotgrid_get_playlists() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.shotgrid_get_playlists",false]],"shotgrid_sync_new_item_notes() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.shotgrid_sync_new_item_notes",false]],"shotgrid_sync_review_items() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.shotgrid_sync_review_items",false]],"shotgrid_sync_review_notes() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.shotgrid_sync_review_notes",false]],"shotgun_create_config() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.shotgun_create_config",false]],"shotgun_get_playlists() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.shotgun_get_playlists",false]],"shotgun_get_projects() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.shotgun_get_projects",false]],"shotgun_sync_new_item_notes() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.shotgun_sync_new_item_notes",false]],"shotgun_sync_review_items() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.shotgun_sync_review_items",false]],"shotgun_sync_review_notes() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.shotgun_sync_review_notes",false]],"sort_review_items() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.sort_review_items",false]],"syncsketchapi (class in syncsketch)":[[0,"syncsketch.SyncSketchAPI",false]],"update_account() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.update_account",false]],"update_item() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.update_item",false]],"update_project() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.update_project",false]],"update_review() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.update_review",false]],"update_review_sections() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.update_review_sections",false]],"upload_file() (syncsketch.syncsketchapi method)":[[0,"syncsketch.SyncSketchAPI.upload_file",false]]},"objects":{"syncsketch":[[0,0,1,"","SyncSketchAPI"]],"syncsketch.SyncSketchAPI":[[0,1,1,"","add_comment"],[0,1,1,"","add_item"],[0,1,1,"","add_media"],[0,1,1,"","add_media_by_url"],[0,1,1,"","add_media_v1"],[0,1,1,"","add_media_v2"],[0,1,1,"","add_users_to_project"],[0,1,1,"","add_users_to_workspace"],[0,1,1,"","archive_project"],[0,1,1,"","archive_review"],[0,1,1,"","bulk_delete_items"],[0,1,1,"","create_project"],[0,1,1,"","create_review"],[0,1,1,"","create_review_section"],[0,1,1,"","delete_item"],[0,1,1,"","delete_project"],[0,1,1,"","delete_review"],[0,1,1,"","delete_review_section"],[0,1,1,"","duplicate_project"],[0,1,1,"","get_accounts"],[0,1,1,"","get_annotations"],[0,1,1,"","get_connections_by_user_id"],[0,1,1,"","get_flattened_annotations"],[0,1,1,"","get_grease_pencil_overlays"],[0,1,1,"","get_item"],[0,1,1,"","get_items_by_review_id"],[0,1,1,"","get_media"],[0,1,1,"","get_project_by_id"],[0,1,1,"","get_project_storage"],[0,1,1,"","get_projects"],[0,1,1,"","get_projects_by_name"],[0,1,1,"","get_review_by_id"],[0,1,1,"","get_review_by_name"],[0,1,1,"","get_review_by_uuid"],[0,1,1,"","get_review_storage"],[0,1,1,"","get_reviews_by_project_id"],[0,1,1,"","get_shotgrid_sync_review_items_progress"],[0,1,1,"","get_shotgrid_sync_review_notes_progress"],[0,1,1,"","get_shotgun_sync_review_items_progress"],[0,1,1,"","get_tree"],[0,1,1,"","get_user_by_email"],[0,1,1,"","get_user_by_id"],[0,1,1,"","get_users_by_name"],[0,1,1,"","get_users_by_project_id"],[0,1,1,"","is_connected"],[0,1,1,"","move_items"],[0,1,1,"","remove_users_from_project"],[0,1,1,"","remove_users_from_workspace"],[0,1,1,"","restore_project"],[0,1,1,"","restore_review"],[0,1,1,"","shotgrid_create_config"],[0,1,1,"","shotgrid_get_playlists"],[0,1,1,"","shotgrid_sync_new_item_notes"],[0,1,1,"","shotgrid_sync_review_items"],[0,1,1,"","shotgrid_sync_review_notes"],[0,1,1,"","shotgun_create_config"],[0,1,1,"","shotgun_get_playlists"],[0,1,1,"","shotgun_get_projects"],[0,1,1,"","shotgun_sync_new_item_notes"],[0,1,1,"","shotgun_sync_review_items"],[0,1,1,"","shotgun_sync_review_notes"],[0,1,1,"","sort_review_items"],[0,1,1,"","update_account"],[0,1,1,"","update_item"],[0,1,1,"","update_project"],[0,1,1,"","update_review"],[0,1,1,"","update_review_sections"],[0,1,1,"","upload_file"]]},"objnames":{"0":["py","class","Python class"],"1":["py","method","Python method"]},"objtypes":{"0":"py:class","1":"py:method"},"terms":{"3d":0,"5mb":0,"A":0,"For":0,"If":0,"In":0,"It":0,"TO":0,"The":0,"This":0,"To":0,"We":0,"When":0,"You":0,"__":0,"__init__":0,"access":0,"account":0,"account_id":0,"accountid":0,"activ":0,"add":0,"add_com":0,"add_item":0,"add_media":0,"add_media_by_url":0,"add_media_v1":0,"add_media_v2":0,"add_users_to_project":0,"add_users_to_workspac":0,"addit":0,"additem":0,"additional_data":0,"additionaldata":0,"admin":0,"all_with_rel":0,"alreadi":0,"also":0,"alway":0,"ani":0,"annot":0,"anoth":0,"api_key":0,"api_vers":0,"applic":0,"archiv":0,"archive_project":0,"archive_review":0,"array":0,"artist":0,"artist_nam":0,"associ":0,"attach":0,"auth":0,"authent":0,"author":0,"auto":0,"autodesk":0,"autodoc_member_ord":0,"automat":0,"avail":0,"aw":0,"backend":0,"background":0,"base":0,"base64":0,"befor":0,"bool":0,"bradi":0,"browser":0,"bulk_delete_item":0,"bysourc":0,"byte":0,"caa":0,"call":0,"can":0,"capabl":0,"case":0,"certain":0,"check":0,"chunk":0,"chunk_siz":0,"class":0,"cloud":0,"cloudhelp":0,"code":0,"collabor":0,"com":0,"combin":0,"command":0,"comment":0,"communic":0,"compat":0,"complet":0,"composit":0,"configur":0,"connect":0,"constructor":0,"contain":0,"content":0,"conveni":0,"convert":0,"copi":0,"copy_review":0,"copy_set":0,"copy_us":0,"correct":0,"creat":0,"create_project":0,"create_review":0,"create_review_sect":0,"creator":0,"current":0,"data":0,"day":0,"db":0,"de":0,"deactiv":0,"debug":0,"default":0,"definit":0,"delet":0,"delete_item":0,"delete_project":0,"delete_review":0,"delete_review_sect":0,"deprecationwarn":0,"describ":0,"descript":0,"detect":0,"determin":0,"dict":0,"dictionari":0,"direct":0,"disk":0,"django":0,"djangoproject":0,"doc":0,"doe":0,"done":0,"download":0,"dropbox":0,"duplicate_project":0,"durat":0,"dure":0,"e":0,"either":0,"email":0,"en":0,"enabl":0,"encod":0,"endr":0,"enu":0,"error":0,"etc":0,"even":0,"exact":0,"exampl":0,"exist":0,"expir":0,"explor":0,"extens":0,"external_thumbnail_url":0,"external_url":0,"externali":0,"fail":0,"failur":0,"fals":0,"fetch":0,"field":0,"file":0,"file_nam":0,"filepath":0,"find":0,"first_nam":0,"float":0,"follow":0,"foreign":0,"format":0,"found":0,"fps":0,"frame":0,"full":0,"function":0,"g":0,"general":0,"generat":0,"get":0,"get_account":0,"get_annot":0,"get_connections_by_user_id":0,"get_flattened_annot":0,"get_grease_pencil_overlay":0,"get_item":0,"get_items_by_review_id":0,"get_media":0,"get_project":0,"get_project_by_id":0,"get_project_storag":0,"get_projects_by_nam":0,"get_review_by_id":0,"get_review_by_nam":0,"get_review_by_uuid":0,"get_review_storag":0,"get_reviews_by_project_id":0,"get_shotgrid_sync_review_items_progress":0,"get_shotgrid_sync_review_notes_progress":0,"get_shotgun_sync_review_items_progress":0,"get_shotgun_sync_review_notes_progress":0,"get_tre":0,"get_user_by_email":0,"get_user_by_id":0,"get_users_by_nam":0,"get_users_by_project_id":0,"getaccount":0,"getmedia":0,"good":0,"greas":0,"greasepencil":0,"gt":0,"gte":0,"header":0,"height":0,"homedir":0,"host":0,"hour":0,"htm":0,"html":0,"http":0,"https":0,"id":0,"ignor":0,"imag":0,"import":0,"improv":0,"inact":0,"includ":0,"include_archiv":0,"include_connect":0,"include_delet":0,"include_inact":0,"include_tag":0,"info":0,"inform":0,"insensit":0,"instead":0,"int":0,"interact":0,"internal":0,"invit":0,"is_connect":0,"is_publ":0,"item":0,"item_data":0,"item_id":0,"item_nam":0,"item_uuid":0,"itemid":0,"itemparentid":0,"items_to_mov":0,"jpg":0,"kept":0,"key":0,"knowledg":0,"larg":0,"last":0,"last_nam":0,"latest":0,"learn":0,"left":0,"like":0,"limit":0,"link":0,"list":0,"ll":0,"load":0,"lt":0,"lte":0,"make":0,"manag":0,"max_work":0,"maximum":0,"maya":0,"media":0,"media_url":0,"mediaurl":0,"member":0,"messag":0,"meta":0,"method":0,"might":0,"mode":0,"model":0,"modifi":0,"move":0,"move_item":0,"movi":0,"much":0,"multipart":0,"multipl":0,"name":0,"narrow":0,"need":0,"nest":0,"new":0,"new_review_id":0,"noconvertflag":0,"none":0,"normal":0,"note":0,"number":0,"obj":0,"object":0,"offset":0,"old":0,"omit":0,"one":0,"onli":0,"onlin":0,"option":0,"origin":0,"output":0,"overlay":0,"paper":0,"parallel":0,"param":0,"paramet":0,"pass":0,"path":0,"payload":0,"pencil":0,"per":0,"percent":0,"percent_complet":0,"perform":0,"period":0,"permiss":0,"player":0,"playlist":0,"playlist_cod":0,"playlist_id":0,"pleas":0,"png":0,"possibl":0,"process":0,"progress":0,"progress_url":0,"project":0,"project_id":0,"provid":0,"pull":0,"purg":0,"queri":0,"rain":0,"rang":0,"raw_respons":0,"recommend":0,"record":0,"remaining_item":0,"remov":0,"remove_users_from_project":0,"remove_users_from_workspac":0,"requir":0,"respons":0,"rest":0,"restor":0,"restore_project":0,"restore_review":0,"result":0,"retriev":0,"return":0,"return_as_base64":0,"review":0,"review_id":0,"review_link":0,"reviews__nam":0,"reviews__project__nam":0,"revis":0,"revisionid":0,"s":0,"s3":0,"schema":0,"search":0,"searchcriteria":0,"second":0,"section":0,"section_uuid":0,"sections_to_upd":0,"server":0,"servic":0,"set":0,"shotgrid":0,"shotgrid_create_config":0,"shotgrid_get_playlist":0,"shotgrid_sync_new_item_not":0,"shotgrid_sync_review_item":0,"shotgrid_sync_review_not":0,"shotgun_create_config":0,"shotgun_get_playlist":0,"shotgun_get_project":0,"shotgun_project_id":0,"shotgun_sync_new_item_not":0,"shotgun_sync_review_item":0,"shotgun_sync_review_not":0,"sign":0,"signifi":0,"similar":0,"singl":0,"size":0,"sketch":0,"sketch_upload_error":0,"sort":0,"sort_review_item":0,"sortord":0,"sourc":0,"specif":0,"speed":0,"stabil":0,"start":0,"startswith":0,"status":0,"storag":0,"str":0,"string":0,"success":0,"support":0,"sure":0,"sync":0,"syncsketch_account_id":0,"syncsketch_project_id":0,"syncsketchapi":0,"syntax":0,"system":0,"tag":0,"task":0,"task_id":0,"test":0,"text":0,"time":0,"tmp":0,"token":0,"tool":0,"topic":0,"total_item":0,"trace":0,"tree":0,"tri":0,"true":0,"type":0,"unarch":0,"unlik":0,"updat":0,"update_account":0,"update_item":0,"update_project":0,"update_review":0,"update_review_sect":0,"updated_item":0,"upload":0,"upload_fil":0,"url":0,"usag":0,"use":0,"use_header_auth":0,"useexpiringtoken":0,"user":0,"user_id":0,"usernam":0,"uuid":0,"v1":0,"valu":0,"veri":0,"version":0,"video":0,"viewer":0,"visit":0,"want":0,"webm":0,"whether":0,"whole":0,"width":0,"will":0,"wish":0,"with_tracing_pap":0,"withitem":0,"without":0,"work":0,"worker":0,"workspac":0,"workspace_id":0,"wp":0,"writabl":0,"www":0,"xml":0,"yet":0,"zip":0},"titles":["SyncSketch Python API Library documentation"],"titleterms":{"api":0,"document":0,"librari":0,"python":0,"syncsketch":0}}) \ No newline at end of file diff --git a/build/lib/syncsketch/__init__.py b/build/lib/syncsketch/__init__.py index c51c39a..d5c1749 100644 --- a/build/lib/syncsketch/__init__.py +++ b/build/lib/syncsketch/__init__.py @@ -6,8 +6,20 @@ from __future__ import absolute_import +import sys +import warnings + +if sys.version_info < (3, 8): + warnings.warn( + "SyncSketch: Python %d.%d is deprecated. " + "New features will only target Python 3.8+. " + "Please upgrade." % (sys.version_info[0], sys.version_info[1]), + DeprecationWarning, + stacklevel=2, + ) + from .syncsketch import SyncSketchAPI -__version__ = "1.0.11.2" +__version__ = "1.0.12.0" __author__ = "SyncSketch Dev Team" __credits__ = "Philip Floetotto, Yafes Sahin, Brady Endres, Eric Palakovich Carr" diff --git a/build/lib/syncsketch/syncsketch.py b/build/lib/syncsketch/syncsketch.py index 0fd34f0..e7d2967 100644 --- a/build/lib/syncsketch/syncsketch.py +++ b/build/lib/syncsketch/syncsketch.py @@ -50,7 +50,7 @@ def _worker(self): try: task_id, fn, args, kwargs = self.tasks.get(block=False) self.results[task_id] = fn(*args, **kwargs) - except: + except Exception: pass def __enter__(self): @@ -130,6 +130,13 @@ def __init__( def get_api_base_url(self, api_version=None): return self.join_url_path(self.HOST, "/api/{}/".format(api_version or self.api_version)) + _SENSITIVE_KEYS = frozenset({"api_key", "token", "username", "email", "Authorization"}) + + @staticmethod + def _redact_dict(d): + """Return a copy of dict *d* with sensitive values replaced by '***'.""" + return {k: ("***" if k in SyncSketchAPI._SENSITIVE_KEYS else v) for k, v in d.items()} + @staticmethod def join_url_path(base, *path_segments): """Takes one more more strings and returns a properly terminated url path. Handles strings regardless @@ -206,8 +213,8 @@ def _get_json_response( "{method} URL: {url}, params: {params}, headers: {headers}, status_code: {status_code}".format( method=method, url=url, - params=params, - headers=headers, + params=self._redact_dict(params), + headers=self._redact_dict(headers), status_code=r.status_code, ) ) @@ -221,8 +228,7 @@ def _get_json_response( except Exception as e: if self.debug: print(e) - - print("Error: %s" % r.text) + print("Error: %s" % r.text) return {"objects": []} @@ -958,22 +964,22 @@ def add_media( if itemParentId: get_params.update({"itemParentId": itemParentId}) - uploadURL = "%s/items/uploadToReview/%s/?%s" % ( + uploadURL = "%s/items/uploadToReview/%s/" % ( self.HOST, review_id, - urlencode(get_params), ) files = {"reviewFile": open(filepath, "rb")} r = requests.post( uploadURL, + params=get_params, files=files, data=dict(artist=artist_name, name=file_name), headers=self.headers, ) if self.debug: - print("URL: %s, params: %s" % (uploadURL, get_params)) + print("URL: %s, params: %s" % (uploadURL, self._redact_dict(get_params))) try: return json.loads(r.text) @@ -1002,15 +1008,15 @@ def add_media_by_url(self, review_id, media_url, artist_name="", noConvertFlag=F if noConvertFlag: get_params.update({"noConvertFlag": 1}) - upload_url = "%s/items/uploadToReview/%s/?%s" % ( + upload_url = "%s/items/uploadToReview/%s/" % ( self.HOST, review_id, - urlencode(get_params), ) r = requests.post( upload_url, - {"media_url": media_url, "artist": artist_name}, + params=get_params, + data={"media_url": media_url, "artist": artist_name}, headers=self.headers, ) @@ -1411,8 +1417,7 @@ def _get_s3_signed_url( """ Internal method. Use to retrieve s3 signed url for file upload in `add_media_via_s3`. """ - request_data = self.api_params.copy() - additional_request_data = { + post_data = { "review_id": review_id, "item_name": item_name, "item_data": { @@ -1422,13 +1427,12 @@ def _get_s3_signed_url( "noConvertFlag": no_convert, }, } - request_data.update(additional_request_data) request_url = "{}/uploads/get-s3-signed-url/".format(self.HOST) return self._get_json_response( url=request_url, - postData=request_data, + postData=post_data, raw_response=raw_response, ) @@ -1721,10 +1725,11 @@ def get_grease_pencil_overlays(self, review_id, item_id, homedir=None): if result.get("status") == "done": data = result.get("data") - # storing locally - local_filename = "/tmp/%s.zip" % data["fileName"] + # storing locally - sanitize fileName to prevent path traversal + safe_name = os.path.basename(data["fileName"]) + local_filename = "/tmp/%s.zip" % safe_name if homedir: - local_filename = os.path.join(homedir, "{}.zip".format(data["fileName"])) + local_filename = os.path.join(homedir, "{}.zip".format(safe_name)) r = requests.get(data["s3Path"], stream=True) with open(local_filename, "wb") as f: 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): try: data = response.json() return data.get("objects")[0] - except: + except Exception: return None def get_users_by_project_id(self, project_id, raw_response=False): diff --git a/dist/syncsketch-1.0.12.0-py3-none-any.whl b/dist/syncsketch-1.0.12.0-py3-none-any.whl new file mode 100644 index 0000000..3100d1e Binary files /dev/null and b/dist/syncsketch-1.0.12.0-py3-none-any.whl differ diff --git a/dist/syncsketch-1.0.12.0.tar.gz b/dist/syncsketch-1.0.12.0.tar.gz new file mode 100644 index 0000000..34b0f5d Binary files /dev/null and b/dist/syncsketch-1.0.12.0.tar.gz differ diff --git a/setup.py b/setup.py index 5a8c79e..a25354a 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ setup( name="syncsketch", - version="1.0.11.2", + version="1.0.12.0", description="SyncSketch Python API", author="Philip Floetotto", author_email="phil@syncsketch.com", @@ -28,12 +28,26 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ], - python_requires=">=2.7, <=3.12", + python_requires=">=2.7, <3.15", long_description=readme_text, long_description_content_type="text/markdown", url="https://github.com/syncsketch/python-api", packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), - install_requires=["requests>=2.20.0"], + install_requires=[ + 'requests>=2.20.0,<2.28; python_version < "3.0"', + 'requests>=2.20.0; python_version >= "3.7" and python_version < "3.9"', + 'requests>=2.32.0; python_version >= "3.9"', + 'urllib3>=2.6.3; python_version >= "3.9"', + ], + extras_require={ + "test": [ + "pytest>=7.0,<10.0", + "pytest-cov>=4.0", + "responses>=0.20.0", + ], + }, license="BSD-3-Clause", ) diff --git a/syncsketch.egg-info/PKG-INFO b/syncsketch.egg-info/PKG-INFO index 84c0c30..4a2507f 100644 --- a/syncsketch.egg-info/PKG-INFO +++ b/syncsketch.egg-info/PKG-INFO @@ -1,6 +1,6 @@ -Metadata-Version: 2.2 +Metadata-Version: 2.4 Name: syncsketch -Version: 1.0.11.2 +Version: 1.0.12.0 Summary: SyncSketch Python API Home-page: https://github.com/syncsketch/python-api Author: Philip Floetotto @@ -18,10 +18,20 @@ Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 -Requires-Python: >=2.7, <=3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Requires-Python: >=2.7, <3.15 Description-Content-Type: text/markdown License-File: LICENSE -Requires-Dist: requests>=2.20.0 +Requires-Dist: requests<2.28,>=2.20.0; python_version < "3.0" +Requires-Dist: requests>=2.20.0; python_version >= "3.7" and python_version < "3.9" +Requires-Dist: requests>=2.32.0; python_version >= "3.9" +Requires-Dist: urllib3>=2.6.3; python_version >= "3.9" +Provides-Extra: test +Requires-Dist: pytest<9.0,>=7.0; python_version < "3.9" and extra == "test" +Requires-Dist: pytest<10.0,>=9.0.3; python_version >= "3.9" and extra == "test" +Requires-Dist: pytest-cov>=4.0; extra == "test" +Requires-Dist: responses>=0.20.0; extra == "test" Dynamic: author Dynamic: author-email Dynamic: classifier @@ -29,6 +39,8 @@ Dynamic: description Dynamic: description-content-type Dynamic: home-page Dynamic: license +Dynamic: license-file +Dynamic: provides-extra Dynamic: requires-dist Dynamic: requires-python Dynamic: summary @@ -61,14 +73,17 @@ SyncSketch is a synchronized visual review tool for the Film/TV/Games industry. #### Compatibility This library was tested with and confirmed on python versions: -- 2.7.14+ -- 3.6 -- 3.7 +- 2.7.14+ (see note below) +- 3.7 (see note below) - 3.8 - 3.9 - 3.10 - 3.11 - 3.12 +- 3.13 +- 3.14 + +> **Python 2.7 & 3.7 Deprecation Notice:** Python 2.7 and 3.7 have reached end-of-life and are no longer actively supported by the Python community. While existing functionality in this library will continue to work on these versions, new features and improvements will only be tested against Python 3.8 and above. We recommend upgrading to a supported Python version. #### Installation @@ -193,9 +208,7 @@ You can upload a file to the created review with the review id, we provided one item_data = s.upload_file(review['id'], 'examples/test.webm') ``` -If all steps were successful, you should see the following in the web-app. - -![alt text](https://github.com/syncsketch/python-api/blob/documentation/examples/resources/exampleResult.jpg?raw=true) +If all steps were successful, you should see the new item under the review in the web-app. ### Additional Examples @@ -261,3 +274,29 @@ projects = s.get_projects() for project in projects['objects']: print(project) ``` + +### Publishing a New Release + +1. Update the version in both `setup.py` and `syncsketch/__init__.py` (keep them in sync). + +2. Build the distribution: +```bash +python -m build +``` + +3. Verify the build artifacts in `dist/`: +```bash +ls dist/syncsketch-* +``` + +4. Upload to PyPI: +```bash +python -m twine upload dist/syncsketch-* +``` + +To test with TestPyPI first: +```bash +python -m twine upload --repository testpypi dist/syncsketch-* +``` + +Requires the `build` and `twine` packages (`pip install build twine`). diff --git a/syncsketch.egg-info/SOURCES.txt b/syncsketch.egg-info/SOURCES.txt index 2d067e6..dd8eacb 100644 --- a/syncsketch.egg-info/SOURCES.txt +++ b/syncsketch.egg-info/SOURCES.txt @@ -7,4 +7,19 @@ syncsketch.egg-info/PKG-INFO syncsketch.egg-info/SOURCES.txt syncsketch.egg-info/dependency_links.txt syncsketch.egg-info/requires.txt -syncsketch.egg-info/top_level.txt \ No newline at end of file +syncsketch.egg-info/top_level.txt +tests/test_accounts.py +tests/test_annotations.py +tests/test_auth.py +tests/test_backward_compat.py +tests/test_connection.py +tests/test_get_json_response.py +tests/test_items.py +tests/test_media_upload.py +tests/test_projects.py +tests/test_py27_smoke.py +tests/test_real_file_io.py +tests/test_reviews.py +tests/test_shotgrid.py +tests/test_tree.py +tests/test_users.py \ No newline at end of file diff --git a/syncsketch.egg-info/requires.txt b/syncsketch.egg-info/requires.txt index 111f18b..885fa3b 100644 --- a/syncsketch.egg-info/requires.txt +++ b/syncsketch.egg-info/requires.txt @@ -1 +1,20 @@ + +[:python_version < "3.0"] +requests<2.28,>=2.20.0 + +[:python_version >= "3.7" and python_version < "3.9"] requests>=2.20.0 + +[:python_version >= "3.9"] +requests>=2.32.0 +urllib3>=2.6.3 + +[test] +pytest-cov>=4.0 +responses>=0.20.0 + +[test:python_version < "3.9"] +pytest<9.0,>=7.0 + +[test:python_version >= "3.9"] +pytest<10.0,>=9.0.3 diff --git a/syncsketch/__init__.py b/syncsketch/__init__.py index c51c39a..d5c1749 100644 --- a/syncsketch/__init__.py +++ b/syncsketch/__init__.py @@ -6,8 +6,20 @@ from __future__ import absolute_import +import sys +import warnings + +if sys.version_info < (3, 8): + warnings.warn( + "SyncSketch: Python %d.%d is deprecated. " + "New features will only target Python 3.8+. " + "Please upgrade." % (sys.version_info[0], sys.version_info[1]), + DeprecationWarning, + stacklevel=2, + ) + from .syncsketch import SyncSketchAPI -__version__ = "1.0.11.2" +__version__ = "1.0.12.0" __author__ = "SyncSketch Dev Team" __credits__ = "Philip Floetotto, Yafes Sahin, Brady Endres, Eric Palakovich Carr" diff --git a/syncsketch/syncsketch.py b/syncsketch/syncsketch.py index 0fd34f0..e7d2967 100644 --- a/syncsketch/syncsketch.py +++ b/syncsketch/syncsketch.py @@ -50,7 +50,7 @@ def _worker(self): try: task_id, fn, args, kwargs = self.tasks.get(block=False) self.results[task_id] = fn(*args, **kwargs) - except: + except Exception: pass def __enter__(self): @@ -130,6 +130,13 @@ def __init__( def get_api_base_url(self, api_version=None): return self.join_url_path(self.HOST, "/api/{}/".format(api_version or self.api_version)) + _SENSITIVE_KEYS = frozenset({"api_key", "token", "username", "email", "Authorization"}) + + @staticmethod + def _redact_dict(d): + """Return a copy of dict *d* with sensitive values replaced by '***'.""" + return {k: ("***" if k in SyncSketchAPI._SENSITIVE_KEYS else v) for k, v in d.items()} + @staticmethod def join_url_path(base, *path_segments): """Takes one more more strings and returns a properly terminated url path. Handles strings regardless @@ -206,8 +213,8 @@ def _get_json_response( "{method} URL: {url}, params: {params}, headers: {headers}, status_code: {status_code}".format( method=method, url=url, - params=params, - headers=headers, + params=self._redact_dict(params), + headers=self._redact_dict(headers), status_code=r.status_code, ) ) @@ -221,8 +228,7 @@ def _get_json_response( except Exception as e: if self.debug: print(e) - - print("Error: %s" % r.text) + print("Error: %s" % r.text) return {"objects": []} @@ -958,22 +964,22 @@ def add_media( if itemParentId: get_params.update({"itemParentId": itemParentId}) - uploadURL = "%s/items/uploadToReview/%s/?%s" % ( + uploadURL = "%s/items/uploadToReview/%s/" % ( self.HOST, review_id, - urlencode(get_params), ) files = {"reviewFile": open(filepath, "rb")} r = requests.post( uploadURL, + params=get_params, files=files, data=dict(artist=artist_name, name=file_name), headers=self.headers, ) if self.debug: - print("URL: %s, params: %s" % (uploadURL, get_params)) + print("URL: %s, params: %s" % (uploadURL, self._redact_dict(get_params))) try: return json.loads(r.text) @@ -1002,15 +1008,15 @@ def add_media_by_url(self, review_id, media_url, artist_name="", noConvertFlag=F if noConvertFlag: get_params.update({"noConvertFlag": 1}) - upload_url = "%s/items/uploadToReview/%s/?%s" % ( + upload_url = "%s/items/uploadToReview/%s/" % ( self.HOST, review_id, - urlencode(get_params), ) r = requests.post( upload_url, - {"media_url": media_url, "artist": artist_name}, + params=get_params, + data={"media_url": media_url, "artist": artist_name}, headers=self.headers, ) @@ -1411,8 +1417,7 @@ def _get_s3_signed_url( """ Internal method. Use to retrieve s3 signed url for file upload in `add_media_via_s3`. """ - request_data = self.api_params.copy() - additional_request_data = { + post_data = { "review_id": review_id, "item_name": item_name, "item_data": { @@ -1422,13 +1427,12 @@ def _get_s3_signed_url( "noConvertFlag": no_convert, }, } - request_data.update(additional_request_data) request_url = "{}/uploads/get-s3-signed-url/".format(self.HOST) return self._get_json_response( url=request_url, - postData=request_data, + postData=post_data, raw_response=raw_response, ) @@ -1721,10 +1725,11 @@ def get_grease_pencil_overlays(self, review_id, item_id, homedir=None): if result.get("status") == "done": data = result.get("data") - # storing locally - local_filename = "/tmp/%s.zip" % data["fileName"] + # storing locally - sanitize fileName to prevent path traversal + safe_name = os.path.basename(data["fileName"]) + local_filename = "/tmp/%s.zip" % safe_name if homedir: - local_filename = os.path.join(homedir, "{}.zip".format(data["fileName"])) + local_filename = os.path.join(homedir, "{}.zip".format(safe_name)) r = requests.get(data["s3Path"], stream=True) with open(local_filename, "wb") as f: 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): try: data = response.json() return data.get("objects")[0] - except: + except Exception: return None def get_users_by_project_id(self, project_id, raw_response=False): diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..2d9004f --- /dev/null +++ b/tests/README.md @@ -0,0 +1,84 @@ +# SyncSketch API Tests + +Unit tests for the SyncSketch Python API using **pytest** with mocked HTTP via the **responses** library. +These tests are not a true reflection of the live API behavior but are designed to validate the client-side logic and ensure consistent behavior across Python versions. + +## Setup + +```bash +pip install -e ".[test]" + +# Install tox +pip install tox +``` + +## Running Tests + +```bash +# All tests +pytest tests/ -v + +# With coverage +pytest tests/ --cov=syncsketch --cov-report=term-missing + +# Single file +pytest tests/test_projects.py -v +``` + +## Multi-version Testing (tox) + +```bash +# All configured Python versions (3.7-3.13) +tox + +# Specific version +tox -e py313 +``` + +Requires the target Python versions to be installed (e.g. via pyenv). Missing interpreters are skipped automatically. + +## Python 2.7 Smoke Test + +The full pytest suite requires Python 3.8+. For Python 2.7, a standalone smoke test is provided that verifies import, construction, and core utilities without any test framework dependencies. + +```bash +# Requires only the `requests` package installed for Python 2.7 +python2.7 tests/test_py27_smoke.py +``` + +### Running via Docker + +If you don't have Python 2.7 or 3.7 installed locally, you can use Docker: + +```bash +# Python 2.7 smoke tests +docker run --rm -v "$(pwd)":/app -w /app python:2.7 sh -c \ + 'pip install "requests>=2.20.0,<2.28.0" && python tests/test_py27_smoke.py' + +# Python 3.7+ full test suite (replace tag with desired version) +docker run --rm -v "$(pwd)":/app -w /app python:3.7 sh -c \ + 'pip install -e ".[test]" && pytest tests/ -v' +``` + +## Test Structure + +| File | Covers | +|------|--------| +| `conftest.py` | Shared fixtures, sample data, helpers | +| `test_auth.py` | Constructor and 3 auth modes (query param, expiring token, header) | +| `test_get_json_response.py` | Core HTTP dispatch, auth injection, response parsing | +| `test_connection.py` | `is_connected`, URL utilities, `_update_params` | +| `test_accounts.py` | Workspace/account CRUD | +| `test_projects.py` | Project CRUD, archive/restore, duplicate | +| `test_reviews.py` | Review CRUD, sections, archive/restore | +| `test_items.py` | Item CRUD, bulk delete, move | +| `test_media_upload.py` | `add_media`, `add_media_v2`, `upload_file` multipart S3 flow | +| `test_annotations.py` | Comments, annotations, grease pencil overlays | +| `test_users.py` | User lookup, workspace/project user management | +| `test_shotgrid.py` | ShotGrid config, sync, deprecation | +| `test_tree.py` | `get_tree` | +| `test_backward_compat.py` | camelCase backward-compatibility alias checks | + +## Mocking Approach + +All HTTP calls are intercepted at the `requests` transport layer using the [responses](https://github.com/getsentry/responses) library. This catches both calls through `_get_json_response()` and direct `requests.*` calls in upload/annotation methods. File I/O and `time.sleep` in polling loops are patched with `unittest.mock`. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..e9afbe2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,109 @@ +import pytest +import responses + +from syncsketch import SyncSketchAPI + +HOST = "https://test.syncsketch.com" +USERNAME = "testuser" +API_KEY = "testapikey123" + + +@pytest.fixture +def api(): + """Standard API client using default query-param auth.""" + return SyncSketchAPI( + auth=USERNAME, + api_key=API_KEY, + host=HOST, + ) + + +@pytest.fixture +def api_token(): + """API client using expiring token auth.""" + return SyncSketchAPI( + auth="test@example.com", + api_key="expiring-token-123", + host=HOST, + useExpiringToken=True, + ) + + +@pytest.fixture +def api_header(): + """API client using header-based auth.""" + return SyncSketchAPI( + auth=USERNAME, + api_key=API_KEY, + host=HOST, + use_header_auth=True, + ) + + +@pytest.fixture +def api_debug(): + """API client with debug enabled.""" + return SyncSketchAPI( + auth=USERNAME, + api_key=API_KEY, + host=HOST, + debug=True, + ) + + +@pytest.fixture +def mocked(): + """Activate the responses mock for the duration of a test.""" + with responses.RequestsMock() as rsps: + yield rsps + + +def make_list_response(objects, total_count=None): + """Build a standard Tastypie list response envelope.""" + if total_count is None: + total_count = len(objects) + return { + "meta": { + "limit": 100, + "next": None, + "offset": 0, + "previous": None, + "total_count": total_count, + }, + "objects": objects, + } + + +SAMPLE_PROJECT = { + "id": 123, + "name": "Test Project", + "description": "A test project", + "active": True, + "is_archived": False, + "account_id": 1, +} + +SAMPLE_REVIEW = { + "id": 456, + "name": "Test Review", + "description": "A test review", + "active": True, + "project": "/api/v1/project/123/", + "uuid": "abc-def-ghi", +} + +SAMPLE_ITEM = { + "id": 789, + "name": "test_clip.mp4", + "active": True, + "fps": 24.0, + "status": "done", + "revision_id": 101, +} + +SAMPLE_USER = { + "id": 42, + "first_name": "Test", + "last_name": "User", + "email": "test@example.com", +} diff --git a/tests/test_accounts.py b/tests/test_accounts.py new file mode 100644 index 0000000..9ca5ffc --- /dev/null +++ b/tests/test_accounts.py @@ -0,0 +1,39 @@ +"""Tests for workspace/account methods.""" + +import responses + +from tests.conftest import HOST, SAMPLE_PROJECT, make_list_response + + +class TestGetAccounts: + @responses.activate + def test_get_accounts(self, api): + body = make_list_response([{"id": 1, "name": "Workspace"}]) + responses.add(responses.GET, HOST + "/api/v1/account/", json=body) + result = api.get_accounts() + assert result["objects"][0]["name"] == "Workspace" + assert responses.calls[0].request.params["active"] == "1" + + @responses.activate + def test_get_accounts_with_fields(self, api): + responses.add(responses.GET, HOST + "/api/v1/account/", json=make_list_response([])) + api.get_accounts(fields=["id", "name"]) + assert responses.calls[0].request.params["fields"] == "id,name" + + @responses.activate + def test_get_accounts_raw_response(self, api): + responses.add(responses.GET, HOST + "/api/v1/account/", json={}) + result = api.get_accounts(raw_response=True) + assert hasattr(result, "status_code") + + +class TestUpdateAccount: + @responses.activate + def test_update_account(self, api): + responses.add(responses.PATCH, HOST + "/api/v1/account/1/", json={"id": 1, "name": "Updated"}) + result = api.update_account(1, {"name": "Updated"}) + assert result["name"] == "Updated" + + def test_update_account_non_dict_returns_false(self, api): + result = api.update_account(1, "not a dict") + assert result is False diff --git a/tests/test_annotations.py b/tests/test_annotations.py new file mode 100644 index 0000000..0c648e9 --- /dev/null +++ b/tests/test_annotations.py @@ -0,0 +1,145 @@ +"""Tests for comments, annotations, and grease pencil overlays.""" + +import json +import os +from unittest.mock import mock_open, patch + +import responses + +from tests.conftest import HOST, SAMPLE_ITEM, make_list_response + + +class TestAddComment: + @responses.activate + def test_add_comment(self, api): + # get_item call + responses.add(responses.GET, HOST + "/api/v1/item/789/", json=SAMPLE_ITEM) + # post frame + frame_data = {"id": 1, "text": "Great work!"} + responses.add(responses.POST, HOST + "/api/v1/frame/", json=frame_data) + + result = api.add_comment(789, "Great work!", 456, frame=5) + assert result["text"] == "Great work!" + + # Verify the POST body + body = json.loads(responses.calls[1].request.body) + assert body["item"] == "/api/v1/item/789/" + assert body["frame"] == 5 + assert body["revision"] == "/api/v1/revision/101/" + assert body["type"] == "comment" + assert body["text"] == "Great work!" + + @responses.activate + def test_no_revision_id_returns_error(self, api): + item_no_revision = {"id": 789, "name": "test.mp4"} + responses.add(responses.GET, HOST + "/api/v1/item/789/", json=item_no_revision) + result = api.add_comment(789, "text", 456) + assert result == "error" + + +class TestGetAnnotations: + @responses.activate + def test_get_annotations(self, api): + responses.add(responses.GET, HOST + "/api/v1/frame/", json=make_list_response([{"id": 1}])) + api.get_annotations(789) + params = responses.calls[0].request.params + assert params["item__id"] == "789" + assert params["active"] == "1" + + @responses.activate + def test_with_revision_id(self, api): + responses.add(responses.GET, HOST + "/api/v1/frame/", json=make_list_response([])) + api.get_annotations(789, revisionId=101) + assert responses.calls[0].request.params["revision__id"] == "101" + + @responses.activate + def test_with_review_id(self, api): + responses.add(responses.GET, HOST + "/api/v1/frame/", json=make_list_response([])) + api.get_annotations(789, review_id=456) + assert responses.calls[0].request.params["revision__review_id"] == "456" + + +class TestGetFlattenedAnnotations: + @responses.activate + @patch("time.sleep") + def test_success(self, mock_sleep, api): + flattened_url = HOST + "/api/v2/downloads/flattenedSketches/456/789/" + # POST to start celery task + responses.add(responses.POST, flattened_url, json="task-id-123") + # First GET: processing + check_url = HOST + "/api/v2/downloads/flattenedSketches/task-id-123/" + responses.add(responses.GET, check_url, json={"status": "processing"}) + # Second GET: done + responses.add(responses.GET, check_url, json={"status": "done", "data": ["sketch1.png"]}) + + result = api.get_flattened_annotations(456, 789) + assert result["status"] == "done" + + @responses.activate + def test_failed(self, api): + flattened_url = HOST + "/api/v2/downloads/flattenedSketches/456/789/" + responses.add(responses.POST, flattened_url, json="task-id-123") + check_url = HOST + "/api/v2/downloads/flattenedSketches/task-id-123/" + responses.add(responses.GET, check_url, json={"status": "failed"}) + + result = api.get_flattened_annotations(456, 789) + assert result is None + + +class TestGetGreasePencilOverlays: + @responses.activate + @patch("time.sleep") + def test_success(self, mock_sleep, api): + gp_url = HOST + "/api/v2/downloads/greasePencil/456/789/" + responses.add(responses.POST, gp_url, json="task-id-456") + + check_url = HOST + "/api/v2/downloads/greasePencil/task-id-456/" + responses.add( + responses.GET, + check_url, + json={ + "status": "done", + "data": {"fileName": "overlay", "s3Path": "https://s3.example.com/overlay.zip"}, + }, + ) + responses.add(responses.GET, "https://s3.example.com/overlay.zip", body=b"zipdata") + + m = mock_open() + with patch("syncsketch.syncsketch.open", m): + result = api.get_grease_pencil_overlays(456, 789) + + assert result == "/tmp/overlay.zip" + m.assert_called_once_with("/tmp/overlay.zip", "wb") + + @responses.activate + def test_failed(self, api): + gp_url = HOST + "/api/v2/downloads/greasePencil/456/789/" + responses.add(responses.POST, gp_url, json="task-id-456") + check_url = HOST + "/api/v2/downloads/greasePencil/task-id-456/" + responses.add(responses.GET, check_url, json={"status": "failed"}) + + result = api.get_grease_pencil_overlays(456, 789) + assert result is False + + @responses.activate + @patch("time.sleep") + def test_custom_homedir(self, mock_sleep, api): + gp_url = HOST + "/api/v2/downloads/greasePencil/456/789/" + responses.add(responses.POST, gp_url, json="task-id-456") + check_url = HOST + "/api/v2/downloads/greasePencil/task-id-456/" + responses.add( + responses.GET, + check_url, + json={ + "status": "done", + "data": {"fileName": "overlay", "s3Path": "https://s3.example.com/overlay.zip"}, + }, + ) + responses.add(responses.GET, "https://s3.example.com/overlay.zip", body=b"zipdata") + + m = mock_open() + with patch("syncsketch.syncsketch.open", m): + result = api.get_grease_pencil_overlays(456, 789, homedir="/custom/path") + + expected_path = os.path.join("/custom/path", "overlay.zip") + assert result == expected_path diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..e92358b --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,64 @@ +"""Tests for SyncSketchAPI constructor and authentication modes.""" + +from syncsketch import SyncSketchAPI + +HOST = "https://test.syncsketch.com" + + +class TestDefaultAuth: + def test_query_params_set(self, api): + assert api.api_params == {"api_key": "testapikey123", "username": "testuser"} + + def test_headers_empty(self, api): + assert api.headers == {} + + +class TestExpiringTokenAuth: + def test_token_params_set(self, api_token): + assert api_token.api_params == { + "token": "expiring-token-123", + "email": "test@example.com", + } + + def test_headers_empty(self, api_token): + assert api_token.headers == {} + + +class TestHeaderAuth: + def test_authorization_header_set(self, api_header): + assert api_header.headers == { + "Authorization": "apikey testuser:testapikey123", + } + + def test_api_params_empty(self, api_header): + assert api_header.api_params == {} + + def test_header_auth_with_expiring_token(self): + api = SyncSketchAPI( + auth="test@example.com", + api_key="expiring-token-123", + host=HOST, + useExpiringToken=True, + use_header_auth=True, + ) + assert api.headers["Authorization"] == "token test@example.com:expiring-token-123" + assert api.api_params == {} + + +class TestConstructorOptions: + def test_host_trailing_slash_stripped(self): + api = SyncSketchAPI(auth="u", api_key="k", host="https://test.syncsketch.com/") + assert api.HOST == "https://test.syncsketch.com" + + def test_default_api_version(self, api): + assert api.api_version == "v1" + + def test_custom_api_version(self): + api = SyncSketchAPI(auth="u", api_key="k", host=HOST, api_version="v2") + assert api.api_version == "v2" + + def test_debug_default_false(self, api): + assert api.debug is False + + def test_debug_true(self, api_debug): + assert api_debug.debug is True diff --git a/tests/test_backward_compat.py b/tests/test_backward_compat.py new file mode 100644 index 0000000..2f1bd01 --- /dev/null +++ b/tests/test_backward_compat.py @@ -0,0 +1,64 @@ +"""Tests that camelCase aliases point to the correct snake_case methods.""" + +import pytest + +from syncsketch import SyncSketchAPI + + +@pytest.mark.parametrize( + "alias,target", + [ + # Core + ("isConnected", "is_connected"), + # Accounts + ("getAccounts", "get_accounts"), + # Projects + ("getProjects", "get_projects"), + ("getProjectsByName", "get_projects_by_name"), + ("getProjectById", "get_project_by_id"), + ("addProject", "create_project"), + ("deleteProject", "delete_project"), + # Reviews + ("addReview", "create_review"), + ("getReviewsByProjectId", "get_reviews_by_project_id"), + ("getReviewByName", "get_review_by_name"), + ("getReviewById", "get_review_by_id"), + ("deleteReview", "delete_review"), + # Items + ("getItem", "get_item"), + ("addItem", "add_item"), + ("updateItem", "update_item"), + ("addMedia", "add_media"), + ("addMediaByURL", "add_media_by_url"), + ("getMediaByReviewId", "get_items_by_review_id"), + ("get_media_by_review_id", "get_items_by_review_id"), + ("getMedia", "get_media"), + ("connectItemToReview", "connect_item_to_review"), + ("deleteItem", "delete_item"), + # v1 alias + ("add_media_v1", "add_media"), + # Users + ("getUsersByName", "get_users_by_name"), + ("getUsersByProjectId", "get_users_by_project_id"), + ("getUserById", "get_user_by_id"), + ("addUsers", "add_users"), + ("getCurrentUser", "get_current_user"), + # Annotations + ("addComment", "add_comment"), + ("getGreasePencilOverlays", "get_grease_pencil_overlays"), + ("getAnnotations", "get_annotations"), + # Tree + ("getTree", "get_tree"), + # Shotgrid/Shotgun aliases + ("get_shotgun_sync_review_items_progress", "get_shotgrid_sync_review_items_progress"), + ("shotgun_sync_review_items", "shotgrid_sync_review_items"), + ("get_shotgun_sync_review_notes_progress", "get_shotgrid_sync_review_notes_progress"), + ("shotgun_sync_new_item_notes", "shotgrid_sync_new_item_notes"), + ("shotgun_sync_review_notes", "shotgrid_sync_review_notes"), + ("shotgun_get_playlists", "shotgrid_get_playlists"), + ("shotgun_create_config", "shotgrid_create_config"), + ("shotgun_get_projects", "shotgrid_get_projects"), + ], +) +def test_backward_compat_alias(alias, target): + assert getattr(SyncSketchAPI, alias) is getattr(SyncSketchAPI, target) diff --git a/tests/test_connection.py b/tests/test_connection.py new file mode 100644 index 0000000..5dba98b --- /dev/null +++ b/tests/test_connection.py @@ -0,0 +1,65 @@ +"""Tests for connection, URL utilities, and _update_params.""" + +import responses + +from syncsketch import SyncSketchAPI + +HOST = "https://test.syncsketch.com" + + +class TestIsConnected: + @responses.activate + def test_raw_response_returns_response_object(self, api): + responses.add(responses.GET, HOST + "/api/v1/person/connected/", json={}, status=200) + result = api.is_connected(raw_response=True) + assert hasattr(result, "status_code") + assert result.status_code == 200 + + +class TestGetApiBaseUrl: + def test_default_version(self, api): + assert api.get_api_base_url() == HOST + "/api/v1/" + + def test_custom_version(self, api): + assert api.get_api_base_url("v2") == HOST + "/api/v2/" + + +class TestJoinUrlPath: + def test_single_segment(self): + assert SyncSketchAPI.join_url_path("abc") == "abc/" + + def test_two_segments(self): + assert SyncSketchAPI.join_url_path("abc", "123") == "abc/123/" + + def test_strips_slashes(self): + assert SyncSketchAPI.join_url_path("abc", "/123/", "/xyz/") == "abc/123/xyz/" + + def test_already_terminated(self): + assert SyncSketchAPI.join_url_path("abc/") == "abc/" + + +class TestUpdateParams: + def test_string_value(self): + params = {} + SyncSketchAPI._update_params("key", "val", params) + assert params == {"key": "val"} + + def test_list_value_joined(self): + params = {} + SyncSketchAPI._update_params("key", ["a", "b", "c"], params) + assert params == {"key": "a,b,c"} + + def test_tuple_value_joined(self): + params = {} + SyncSketchAPI._update_params("key", ("x", "y"), params) + assert params == {"key": "x,y"} + + def test_none_does_not_update(self): + params = {"existing": 1} + SyncSketchAPI._update_params("key", None, params) + assert params == {"existing": 1} + + def test_falsy_zero_does_not_update(self): + params = {} + SyncSketchAPI._update_params("key", 0, params) + assert params == {} diff --git a/tests/test_get_json_response.py b/tests/test_get_json_response.py new file mode 100644 index 0000000..51ab9c5 --- /dev/null +++ b/tests/test_get_json_response.py @@ -0,0 +1,133 @@ +"""Tests for _get_json_response HTTP dispatch logic.""" + +import json + +import responses + +HOST = "https://test.syncsketch.com" + + +class TestHTTPMethodRouting: + @responses.activate + def test_get_request_default(self, api): + responses.add(responses.GET, HOST + "/api/v1/test/", json={"ok": True}) + result = api._get_json_response("/api/v1/test/") + assert result == {"ok": True} + assert responses.calls[0].request.method == "GET" + + @responses.activate + def test_post_request_with_post_data(self, api): + responses.add(responses.POST, HOST + "/api/v1/test/", json={"created": True}) + result = api._get_json_response("/api/v1/test/", postData={"key": "val"}) + assert result == {"created": True} + req = responses.calls[0].request + assert req.method == "POST" + assert json.loads(req.body) == {"key": "val"} + + @responses.activate + def test_patch_request_with_patch_data(self, api): + responses.add(responses.PATCH, HOST + "/api/v1/test/", json={"updated": True}) + result = api._get_json_response("/api/v1/test/", patchData={"key": "val"}) + assert result == {"updated": True} + assert responses.calls[0].request.method == "PATCH" + + @responses.activate + def test_put_request_with_put_data(self, api): + responses.add(responses.PUT, HOST + "/api/v1/test/", json={"updated": True}) + result = api._get_json_response("/api/v1/test/", putData={"key": "val"}) + assert result == {"updated": True} + assert responses.calls[0].request.method == "PUT" + + @responses.activate + def test_delete_request(self, api): + responses.add(responses.DELETE, HOST + "/api/v1/test/", json={"deleted": True}) + result = api._get_json_response("/api/v1/test/", method="delete") + assert result == {"deleted": True} + assert responses.calls[0].request.method == "DELETE" + + @responses.activate + def test_explicit_post_method_no_data(self, api): + responses.add(responses.POST, HOST + "/api/v1/test/", json={"ok": True}) + api._get_json_response("/api/v1/test/", method="post") + assert responses.calls[0].request.method == "POST" + + +class TestAuthParamInjection: + @responses.activate + def test_get_data_merged_with_auth_params(self, api): + responses.add(responses.GET, HOST + "/api/v1/test/", json={}) + api._get_json_response("/api/v1/test/", getData={"limit": 10}) + params = responses.calls[0].request.params + assert params["api_key"] == "testapikey123" + assert params["username"] == "testuser" + assert params["limit"] == "10" + + @responses.activate + def test_header_auth_sends_authorization_header(self, api_header): + responses.add(responses.GET, HOST + "/api/v1/test/", json={}) + api_header._get_json_response("/api/v1/test/") + headers = responses.calls[0].request.headers + assert "Authorization" in headers + assert headers["Authorization"] == "apikey testuser:testapikey123" + # No auth query params + params = responses.calls[0].request.params + assert "api_key" not in params + assert "username" not in params + + @responses.activate + def test_token_auth_sends_token_params(self, api_token): + responses.add(responses.GET, HOST + "/api/v1/test/", json={}) + api_token._get_json_response("/api/v1/test/") + params = responses.calls[0].request.params + assert params["token"] == "expiring-token-123" + assert params["email"] == "test@example.com" + + +class TestContentType: + @responses.activate + def test_default_content_type(self, api): + responses.add(responses.GET, HOST + "/api/v1/test/", json={}) + api._get_json_response("/api/v1/test/") + assert responses.calls[0].request.headers["Content-Type"] == "application/json" + + @responses.activate + def test_custom_content_type(self, api): + responses.add(responses.GET, HOST + "/api/v1/test/", json={}) + api._get_json_response("/api/v1/test/", content_type="text/plain") + assert responses.calls[0].request.headers["Content-Type"] == "text/plain" + + +class TestResponseHandling: + @responses.activate + def test_raw_response_returns_response_object(self, api): + responses.add(responses.GET, HOST + "/api/v1/test/", json={"ok": True}) + result = api._get_json_response("/api/v1/test/", raw_response=True) + assert hasattr(result, "status_code") + assert result.status_code == 200 + assert result.json() == {"ok": True} + + @responses.activate + def test_json_parse_success(self, api): + responses.add(responses.GET, HOST + "/api/v1/test/", json={"data": [1, 2, 3]}) + result = api._get_json_response("/api/v1/test/") + assert result == {"data": [1, 2, 3]} + + @responses.activate + def test_json_parse_failure_returns_empty_objects(self, api): + responses.add(responses.GET, HOST + "/api/v1/test/", body="not json", status=200) + result = api._get_json_response("/api/v1/test/") + assert result == {"objects": []} + + +class TestURLHandling: + @responses.activate + def test_full_url_passthrough(self, api): + responses.add(responses.GET, "https://other.example.com/api/test/", json={}) + api._get_json_response("https://other.example.com/api/test/") + assert "other.example.com" in responses.calls[0].request.url + + @responses.activate + def test_relative_url_gets_host_prefix(self, api): + responses.add(responses.GET, HOST + "/api/v1/project/", json={}) + api._get_json_response("/api/v1/project/") + assert responses.calls[0].request.url.startswith(HOST) diff --git a/tests/test_items.py b/tests/test_items.py new file mode 100644 index 0000000..36c579a --- /dev/null +++ b/tests/test_items.py @@ -0,0 +1,106 @@ +"""Tests for item CRUD methods (excluding uploads).""" + +import json + +import responses + +from tests.conftest import HOST, SAMPLE_ITEM, make_list_response + + +class TestGetItem: + @responses.activate + def test_get_item(self, api): + responses.add(responses.GET, HOST + "/api/v1/item/789/", json=SAMPLE_ITEM) + result = api.get_item(789) + assert result["id"] == 789 + + @responses.activate + def test_get_item_with_data_and_fields(self, api): + responses.add(responses.GET, HOST + "/api/v1/item/789/", json=SAMPLE_ITEM) + api.get_item(789, data={"review_id": 456}, fields=["id", "name"]) + params = responses.calls[0].request.params + assert params["review_id"] == "456" + assert params["fields"] == "id,name" + + +class TestUpdateItem: + @responses.activate + def test_update_item(self, api): + responses.add(responses.PATCH, HOST + "/api/v1/item/789/", json={"id": 789, "name": "Updated"}) + result = api.update_item(789, {"name": "Updated"}) + assert result["name"] == "Updated" + + def test_non_dict_returns_false(self, api): + assert api.update_item(789, "not a dict") is False + + +class TestAddItem: + @responses.activate + def test_add_item(self, api): + responses.add(responses.POST, HOST + "/api/v1/item/", json=SAMPLE_ITEM) + additional_data = {"external_url": "https://example.com/video.mp4"} + result = api.add_item(456, "test_clip.mp4", 24.0, additional_data) + assert result["id"] == 789 + body = json.loads(responses.calls[0].request.body) + assert body["reviewId"] == 456 + assert body["name"] == "test_clip.mp4" + assert body["fps"] == 24.0 + assert body["status"] == "done" + assert body["external_url"] == "https://example.com/video.mp4" + + +class TestGetMedia: + @responses.activate + def test_get_media(self, api): + responses.add(responses.GET, HOST + "/api/v1/item/", json=make_list_response([SAMPLE_ITEM])) + result = api.get_media({"reviews__project__name": "test", "limit": 1, "active": 1}) + assert len(result["objects"]) == 1 + + +class TestGetItemsByReviewId: + @responses.activate + def test_get_items_by_review_id(self, api): + responses.add(responses.GET, HOST + "/api/v1/item/", json=make_list_response([SAMPLE_ITEM])) + api.get_items_by_review_id(456) + params = responses.calls[0].request.params + assert params["reviews__id"] == "456" + assert params["active"] == "1" + + +class TestDeleteItem: + @responses.activate + def test_delete_item(self, api): + responses.add(responses.PATCH, HOST + "/api/v1/item/789/", json={}) + api.delete_item(789) + body = json.loads(responses.calls[0].request.body) + assert body["active"] is False + + +class TestBulkDeleteItems: + @responses.activate + def test_bulk_delete_items(self, api): + responses.add(responses.POST, HOST + "/api/v2/bulk-delete-items/", json={}, status=200) + result = api.bulk_delete_items([1, 2, 3]) + # Default raw_response=True + assert hasattr(result, "status_code") + body = json.loads(responses.calls[0].request.body) + assert body["item_ids"] == [1, 2, 3] + + +class TestConnectItemToReview: + def test_deprecated(self, api): + result = api.connect_item_to_review(789, 456) + assert result == "Deprecated" + + +class TestMoveItems: + @responses.activate + def test_move_items(self, api): + item_data = [{"review_id": 1, "item_id": 1}, {"review_id": 1, "item_id": 2}] + responses.add(responses.POST, HOST + "/api/v2/move-review-items/", json={}, status=200) + result = api.move_items(999, item_data) + # Default raw_response=True + assert hasattr(result, "status_code") + body = json.loads(responses.calls[0].request.body) + assert body["new_review_id"] == 999 + assert body["item_data"] == item_data diff --git a/tests/test_media_upload.py b/tests/test_media_upload.py new file mode 100644 index 0000000..25ff92e --- /dev/null +++ b/tests/test_media_upload.py @@ -0,0 +1,205 @@ +"""Tests for media upload methods: add_media, add_media_by_url, add_media_v2, upload_file.""" + +import io +import json +import os +from unittest.mock import MagicMock, mock_open, patch + +import pytest +import responses + +from tests.conftest import HOST, SAMPLE_ITEM + +UPLOAD_URL_PREFIX = HOST + "/items/uploadToReview/" + +# Patch targets in the syncsketch module +OPEN_PATCH = "syncsketch.syncsketch.open" +STAT_PATCH = "syncsketch.syncsketch.os.stat" +MIMETYPES_PATCH = "syncsketch.syncsketch.mimetypes.guess_type" + + +def fake_open(data=b"fake file data"): + """Return a callable that produces BytesIO objects (works both as direct call and context manager).""" + + def _open(*args, **kwargs): + return io.BytesIO(data) + + return _open + + +class TestAddMedia: + @responses.activate + def test_add_media_basic(self, api): + responses.add(responses.POST, UPLOAD_URL_PREFIX + "456/", json=SAMPLE_ITEM) + + with patch(OPEN_PATCH, side_effect=fake_open()): + result = api.add_media(456, "/tmp/test.mp4", artist_name="Artist", file_name="test.mp4") + + assert result["id"] == 789 + + @responses.activate + def test_add_media_with_no_convert_flag(self, api): + responses.add(responses.POST, UPLOAD_URL_PREFIX + "456/", json=SAMPLE_ITEM) + + with patch(OPEN_PATCH, side_effect=fake_open()): + api.add_media(456, "/tmp/test.mp4", noConvertFlag=True) + + assert "noConvertFlag=1" in responses.calls[0].request.url + + @responses.activate + def test_add_media_with_item_parent_id(self, api): + responses.add(responses.POST, UPLOAD_URL_PREFIX + "456/", json=SAMPLE_ITEM) + + with patch(OPEN_PATCH, side_effect=fake_open()): + api.add_media(456, "/tmp/test.mp4", itemParentId=10) + + assert "itemParentId=10" in responses.calls[0].request.url + + +class TestAddMediaByUrl: + @responses.activate + def test_add_media_by_url(self, api): + responses.add(responses.POST, UPLOAD_URL_PREFIX + "456/", json=SAMPLE_ITEM) + result = api.add_media_by_url(456, "https://example.com/video.mp4", artist_name="Artist") + assert result["id"] == 789 + + def test_missing_review_id_raises(self, api): + with pytest.raises(Exception, match="review id"): + api.add_media_by_url(None, "https://example.com/video.mp4") + + def test_missing_media_url_raises(self, api): + with pytest.raises(Exception, match="media_url"): + api.add_media_by_url(456, "") + + @responses.activate + def test_with_no_convert_flag(self, api): + responses.add(responses.POST, UPLOAD_URL_PREFIX + "456/", json=SAMPLE_ITEM) + api.add_media_by_url(456, "https://example.com/v.mp4", noConvertFlag=True) + assert "noConvertFlag=1" in responses.calls[0].request.url + + +class TestAddMediaV2: + def test_no_header_auth_returns_none(self, api): + result = api.add_media_v2(456, "/tmp/test.mp4") + assert result is None + + @responses.activate + @patch(MIMETYPES_PATCH, return_value=("video/mp4", None)) + @patch(STAT_PATCH) + def test_large_file_delegates_to_v1(self, mock_stat, mock_mime, api_header): + mock_stat.return_value = MagicMock(st_size=6_000_000) + responses.add(responses.POST, UPLOAD_URL_PREFIX + "456/", json={"id": 10, "uuid": "abc"}) + + with patch(OPEN_PATCH, side_effect=fake_open()): + result = api_header.add_media_v2(456, "/tmp/test.mp4") + assert result["id"] == 10 + assert result["uuid"] == "abc" + + @responses.activate + @patch(MIMETYPES_PATCH, return_value=("video/mp4", None)) + @patch(STAT_PATCH) + def test_small_file_s3_flow(self, mock_stat, mock_mime, api_header): + mock_stat.return_value = MagicMock(st_size=1000) + + signed_url_response = { + "url": "https://s3.amazonaws.com/bucket", + "fields": { + "key": "uploads/test.mp4", + "x-amz-meta-item-id": "10", + "x-amz-meta-item-uuid": "abc-123", + }, + } + responses.add(responses.POST, HOST + "/uploads/get-s3-signed-url/", json=signed_url_response) + responses.add(responses.POST, "https://s3.amazonaws.com/bucket", status=204) + + with patch(OPEN_PATCH, side_effect=fake_open()): + result = api_header.add_media_v2(456, "/tmp/test.mp4") + + assert result == {"id": "10", "uuid": "abc-123"} + + +class TestUploadFile: + def test_no_header_auth_returns_none(self, api): + result = api.upload_file(456, "/tmp/test.mp4") + assert result is None + + @responses.activate + @patch("time.sleep") + @patch(MIMETYPES_PATCH, return_value=("video/mp4", None)) + @patch(STAT_PATCH) + def test_success_flow(self, mock_stat, mock_mime, mock_sleep, api_header): + mock_stat.return_value = MagicMock(st_size=100) + + # Step 1: start upload + responses.add( + responses.POST, + HOST + "/uploads/stats/upload-start/", + json={"item_id": 10, "item_uuid": "abc-123"}, + ) + # Step 2: init multipart + responses.add( + responses.POST, + HOST + "/uploads/multipart-upload/", + json={"uploadId": "upload-1", "key": "upload-key"}, + ) + # Step 3: sign part + responses.add( + responses.GET, + HOST + "/uploads/multipart-upload/upload-1/sign-part/1/", + json={"url": "https://s3.example.com/part1"}, + ) + # Upload part to S3 + responses.add( + responses.PUT, + "https://s3.example.com/part1", + headers={"ETag": '"etag1"'}, + status=200, + ) + # Step 5: complete multipart + responses.add( + responses.POST, + HOST + "/uploads/multipart-upload/upload-1/complete/", + json={"ok": True}, + ) + # Final: get_item + responses.add(responses.GET, HOST + "/api/v1/item/10/", json=SAMPLE_ITEM) + + with patch(OPEN_PATCH, mock_open(read_data=b"chunk")): + result = api_header.upload_file(456, "/tmp/test.mp4", max_workers=1) + + assert result["id"] == 789 + + @responses.activate + @patch(MIMETYPES_PATCH, return_value=("video/mp4", None)) + @patch(STAT_PATCH) + def test_start_upload_failure(self, mock_stat, mock_mime, api_header): + mock_stat.return_value = MagicMock(st_size=100) + responses.add( + responses.POST, + HOST + "/uploads/stats/upload-start/", + json={"error": "fail"}, + status=400, + ) + with patch(OPEN_PATCH, mock_open(read_data=b"chunk")): + result = api_header.upload_file(456, "/tmp/test.mp4", max_workers=1) + assert result is None + + @responses.activate + @patch(MIMETYPES_PATCH, return_value=("video/mp4", None)) + @patch(STAT_PATCH) + def test_multipart_init_failure(self, mock_stat, mock_mime, api_header): + mock_stat.return_value = MagicMock(st_size=100) + responses.add( + responses.POST, + HOST + "/uploads/stats/upload-start/", + json={"item_id": 10, "item_uuid": "abc"}, + ) + responses.add( + responses.POST, + HOST + "/uploads/multipart-upload/", + json={"error": "fail"}, + status=400, + ) + with patch(OPEN_PATCH, mock_open(read_data=b"chunk")): + result = api_header.upload_file(456, "/tmp/test.mp4", max_workers=1) + assert result is None diff --git a/tests/test_projects.py b/tests/test_projects.py new file mode 100644 index 0000000..88304fa --- /dev/null +++ b/tests/test_projects.py @@ -0,0 +1,158 @@ +"""Tests for project CRUD methods.""" + +import json + +import responses + +from tests.conftest import HOST, SAMPLE_PROJECT, make_list_response + + +class TestCreateProject: + @responses.activate + def test_create_project(self, api): + responses.add(responses.POST, HOST + "/api/v1/project/", json=SAMPLE_PROJECT) + result = api.create_project(1, "Test Project", "A test project") + assert result["id"] == 123 + body = json.loads(responses.calls[0].request.body) + assert body["name"] == "Test Project" + assert body["description"] == "A test project" + assert body["account_id"] == 1 + + @responses.activate + def test_create_project_with_extra_data(self, api): + responses.add(responses.POST, HOST + "/api/v1/project/", json=SAMPLE_PROJECT) + api.create_project(1, "Test", data={"is_public": True}) + body = json.loads(responses.calls[0].request.body) + assert body["is_public"] is True + + +class TestGetProjects: + @responses.activate + def test_default_params(self, api): + responses.add(responses.GET, HOST + "/api/v1/project/", json=make_list_response([SAMPLE_PROJECT])) + api.get_projects() + params = responses.calls[0].request.params + assert params["active"] == "1" + assert params["is_archived"] == "0" + assert params["account__active"] == "1" + assert params["limit"] == "100" + assert params["offset"] == "0" + + @responses.activate + def test_include_deleted(self, api): + responses.add(responses.GET, HOST + "/api/v1/project/", json=make_list_response([])) + api.get_projects(include_deleted=True) + params = responses.calls[0].request.params + assert "active" not in params + + @responses.activate + def test_include_archived(self, api): + responses.add(responses.GET, HOST + "/api/v1/project/", json=make_list_response([])) + api.get_projects(include_archived=True) + params = responses.calls[0].request.params + assert "active" not in params + assert "is_archived" not in params + + @responses.activate + def test_include_tags(self, api): + responses.add(responses.GET, HOST + "/api/v1/project/", json=make_list_response([])) + api.get_projects(include_tags=True) + assert responses.calls[0].request.params["include_tags"] == "1" + + @responses.activate + def test_include_connections(self, api): + responses.add(responses.GET, HOST + "/api/v1/project/", json=make_list_response([])) + api.get_projects(include_connections=True) + assert responses.calls[0].request.params["withFullConnections"] == "True" + + @responses.activate + def test_custom_limit_offset(self, api): + responses.add(responses.GET, HOST + "/api/v1/project/", json=make_list_response([])) + api.get_projects(limit=50, offset=10) + params = responses.calls[0].request.params + assert params["limit"] == "50" + assert params["offset"] == "10" + + @responses.activate + def test_with_fields(self, api): + responses.add(responses.GET, HOST + "/api/v1/project/", json=make_list_response([])) + api.get_projects(fields=["id", "name"]) + assert responses.calls[0].request.params["fields"] == "id,name" + + +class TestGetProjectsByName: + @responses.activate + def test_search_by_name(self, api): + responses.add(responses.GET, HOST + "/api/v1/project/", json=make_list_response([SAMPLE_PROJECT])) + api.get_projects_by_name("Test") + assert responses.calls[0].request.params["name__istartswith"] == "Test" + + +class TestGetProjectById: + @responses.activate + def test_get_by_id(self, api): + responses.add(responses.GET, HOST + "/api/v1/project/123/", json=SAMPLE_PROJECT) + result = api.get_project_by_id(123) + assert result["id"] == 123 + + +class TestGetProjectStorage: + @responses.activate + def test_get_storage(self, api): + responses.add(responses.GET, HOST + "/api/v2/project/123/storage/", json={"storage": 12345}) + result = api.get_project_storage(123) + assert result["storage"] == 12345 + + +class TestUpdateProject: + @responses.activate + def test_update_project(self, api): + responses.add(responses.PATCH, HOST + "/api/v1/project/123/", json={"id": 123, "name": "Updated"}) + result = api.update_project(123, {"name": "Updated"}) + assert result["name"] == "Updated" + + def test_non_dict_returns_false(self, api): + assert api.update_project(123, "not a dict") is False + + +class TestDeleteProject: + @responses.activate + def test_delete_project(self, api): + responses.add(responses.PATCH, HOST + "/api/v1/project/123/", json={}) + api.delete_project(123) + body = json.loads(responses.calls[0].request.body) + assert body["active"] is False + + +class TestDuplicateProject: + @responses.activate + def test_duplicate_project(self, api): + responses.add(responses.POST, HOST + "/api/v2/project/123/duplicate/", json={"id": 999}) + result = api.duplicate_project(123, copy_reviews=True, copy_users=True) + assert result["id"] == 999 + body = json.loads(responses.calls[0].request.body) + assert body["reviews"] is True + assert body["users"] is True + + @responses.activate + def test_duplicate_with_name(self, api): + responses.add(responses.POST, HOST + "/api/v2/project/123/duplicate/", json={"id": 999}) + api.duplicate_project(123, name="Copy of Project") + body = json.loads(responses.calls[0].request.body) + assert body["name"] == "Copy of Project" + + +class TestArchiveRestoreProject: + @responses.activate + def test_archive_project(self, api): + responses.add(responses.PATCH, HOST + "/api/v1/project/123/", json={}) + api.archive_project(123) + body = json.loads(responses.calls[0].request.body) + assert body["is_archived"] is True + + @responses.activate + def test_restore_project(self, api): + responses.add(responses.PATCH, HOST + "/api/v1/project/123/", json={}) + api.restore_project(123) + body = json.loads(responses.calls[0].request.body) + assert body["is_archived"] is False diff --git a/tests/test_py27_smoke.py b/tests/test_py27_smoke.py new file mode 100644 index 0000000..7d49d07 --- /dev/null +++ b/tests/test_py27_smoke.py @@ -0,0 +1,94 @@ +""" +Minimal smoke test for Python 2.7 compatibility. + +Run directly: python2.7 tests/test_py27_smoke.py + +This verifies that the module imports and core construction works +under Python 2.7 without requiring pytest or other test dependencies. +""" + +import sys +import os + +# Ensure the package root is on the path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from syncsketch import SyncSketchAPI + + +def test_default_auth(): + api = SyncSketchAPI("testuser", "testapikey") + assert api.api_params == {"api_key": "testapikey", "username": "testuser"} + assert api.headers == {} + assert api.HOST == "https://www.syncsketch.com" + assert api.api_version == "v1" + + +def test_expiring_token_auth(): + api = SyncSketchAPI("test@example.com", "token123", useExpiringToken=True) + assert api.api_params == {"token": "token123", "email": "test@example.com"} + + +def test_header_auth(): + api = SyncSketchAPI("testuser", "testapikey", use_header_auth=True) + assert api.headers == {"Authorization": "apikey testuser:testapikey"} + assert api.api_params == {} + + +def test_host_trailing_slash(): + api = SyncSketchAPI("u", "k", host="https://test.syncsketch.com/") + assert api.HOST == "https://test.syncsketch.com" + + +def test_join_url_path(): + assert SyncSketchAPI.join_url_path("abc") == "abc/" + assert SyncSketchAPI.join_url_path("abc", "123") == "abc/123/" + assert SyncSketchAPI.join_url_path("abc", "/123/", "/xyz/") == "abc/123/xyz/" + + +def test_get_api_base_url(): + api = SyncSketchAPI("u", "k", host="https://test.syncsketch.com") + assert api.get_api_base_url() == "https://test.syncsketch.com/api/v1/" + assert api.get_api_base_url("v2") == "https://test.syncsketch.com/api/v2/" + + +def test_backward_compat_aliases(): + # In Python 2, class attribute access wraps functions in unbound methods, + # so we compare via __func__ when available, otherwise direct identity. + def get_fn(attr): + return getattr(attr, "__func__", attr) + + assert get_fn(SyncSketchAPI.isConnected) is get_fn(SyncSketchAPI.is_connected) + assert get_fn(SyncSketchAPI.getAccounts) is get_fn(SyncSketchAPI.get_accounts) + assert get_fn(SyncSketchAPI.getProjects) is get_fn(SyncSketchAPI.get_projects) + assert get_fn(SyncSketchAPI.addProject) is get_fn(SyncSketchAPI.create_project) + assert get_fn(SyncSketchAPI.addReview) is get_fn(SyncSketchAPI.create_review) + assert get_fn(SyncSketchAPI.getItem) is get_fn(SyncSketchAPI.get_item) + assert get_fn(SyncSketchAPI.addMedia) is get_fn(SyncSketchAPI.add_media) + assert get_fn(SyncSketchAPI.getCurrentUser) is get_fn(SyncSketchAPI.get_current_user) + + +if __name__ == "__main__": + tests = [ + test_default_auth, + test_expiring_token_auth, + test_header_auth, + test_host_trailing_slash, + test_join_url_path, + test_get_api_base_url, + test_backward_compat_aliases, + ] + + passed = 0 + failed = 0 + for test in tests: + try: + test() + passed += 1 + print(" PASS %s" % test.__name__) + except Exception as e: + failed += 1 + print(" FAIL %s: %s" % (test.__name__, e)) + + print("\n%d passed, %d failed (Python %s)" % (passed, failed, sys.version.split()[0])) + sys.exit(1 if failed else 0) diff --git a/tests/test_real_file_io.py b/tests/test_real_file_io.py new file mode 100644 index 0000000..fa66ed9 --- /dev/null +++ b/tests/test_real_file_io.py @@ -0,0 +1,260 @@ +"""Tests using real temporary files to exercise os.stat, mimetypes.guess_type, and io.open +across Python versions. Only HTTP calls are mocked.""" + +import json +import os +import tempfile + +import responses + +from tests.conftest import HOST, SAMPLE_ITEM + + +def _make_temp_file(suffix=".mp4", content=b"fake video content", size=None): + """Create a real temp file. If size is given, write exactly that many bytes.""" + f = tempfile.NamedTemporaryFile(suffix=suffix, delete=False) + if size is not None: + f.write(b"\x00" * size) + else: + f.write(content) + f.close() + return f.name + + +class TestAddMediaRealFile: + @responses.activate + def test_upload_real_file(self, api): + filepath = _make_temp_file(suffix=".mp4") + try: + responses.add( + responses.POST, + HOST + "/items/uploadToReview/456/", + json=SAMPLE_ITEM, + ) + result = api.add_media(456, filepath, artist_name="Artist", file_name="test.mp4") + assert result["id"] == 789 + # Verify the file was actually read and sent as multipart + assert responses.calls[0].request.body is not None + finally: + os.unlink(filepath) + + @responses.activate + def test_various_extensions(self, api): + """Verify mimetypes.guess_type works for common media types across Python versions.""" + for suffix in [".mp4", ".mov", ".png", ".jpg", ".webm", ".pdf"]: + filepath = _make_temp_file(suffix=suffix, content=b"data") + try: + responses.add( + responses.POST, + HOST + "/items/uploadToReview/456/", + json=SAMPLE_ITEM, + ) + result = api.add_media(456, filepath) + assert result is not None + finally: + os.unlink(filepath) + + +class TestAddMediaV2RealFile: + @responses.activate + def test_small_file_real_io(self, api_header): + """Exercise os.stat and mimetypes.guess_type with a real small file.""" + filepath = _make_temp_file(suffix=".mp4", size=1000) + try: + signed_url_response = { + "url": "https://s3.amazonaws.com/bucket", + "fields": { + "key": "uploads/test.mp4", + "x-amz-meta-item-id": "10", + "x-amz-meta-item-uuid": "abc-123", + }, + } + responses.add(responses.POST, HOST + "/uploads/get-s3-signed-url/", json=signed_url_response) + responses.add(responses.POST, "https://s3.amazonaws.com/bucket", status=204) + + result = api_header.add_media_v2(456, filepath) + assert result == {"id": "10", "uuid": "abc-123"} + + # Verify os.stat was used correctly — the signed URL request should contain the real size + body = json.loads(responses.calls[0].request.body) + assert body["item_data"]["content_length"] == 1000 + assert body["item_data"]["content_type"] is not None + finally: + os.unlink(filepath) + + @responses.activate + def test_large_file_falls_back_to_v1(self, api_header): + """Files > 5MB should fall back to add_media_v1 (direct upload).""" + filepath = _make_temp_file(suffix=".mp4", size=6_000_000) + try: + responses.add( + responses.POST, + HOST + "/items/uploadToReview/456/", + json={"id": 10, "uuid": "abc"}, + ) + result = api_header.add_media_v2(456, filepath) + assert result["id"] == 10 + finally: + os.unlink(filepath) + + @responses.activate + def test_file_name_passed_as_is(self, api_header): + """add_media_v2 passes file_name directly without appending extension.""" + filepath = _make_temp_file(suffix=".webm", size=500) + try: + signed_url_response = { + "url": "https://s3.amazonaws.com/bucket", + "fields": { + "key": "uploads/test.webm", + "x-amz-meta-item-id": "10", + "x-amz-meta-item-uuid": "abc-123", + }, + } + responses.add(responses.POST, HOST + "/uploads/get-s3-signed-url/", json=signed_url_response) + responses.add(responses.POST, "https://s3.amazonaws.com/bucket", status=204) + + result = api_header.add_media_v2(456, filepath, file_name="my_video") + assert result is not None + + body = json.loads(responses.calls[0].request.body) + # add_media_v2 does NOT append extension (unlike upload_file) + assert body["item_name"] == "my_video" + finally: + os.unlink(filepath) + + +class TestUploadFileRealFile: + @responses.activate + def test_multipart_upload_real_file(self, api_header): + """Full multipart upload flow with a real file — exercises open, os.stat, mimetypes.""" + filepath = _make_temp_file(suffix=".mp4", size=100) + try: + # Step 1: start upload + responses.add( + responses.POST, + HOST + "/uploads/stats/upload-start/", + json={"item_id": 10, "item_uuid": "abc-123"}, + ) + # Step 2: init multipart + responses.add( + responses.POST, + HOST + "/uploads/multipart-upload/", + json={"uploadId": "upload-1", "key": "upload-key"}, + ) + # Step 3: sign part + responses.add( + responses.GET, + HOST + "/uploads/multipart-upload/upload-1/sign-part/1/", + json={"url": "https://s3.example.com/part1"}, + ) + # Upload part to S3 + responses.add( + responses.PUT, + "https://s3.example.com/part1", + headers={"ETag": '"etag1"'}, + status=200, + ) + # Step 5: complete multipart + responses.add( + responses.POST, + HOST + "/uploads/multipart-upload/upload-1/complete/", + json={"ok": True}, + ) + # Final: get_item + responses.add(responses.GET, HOST + "/api/v1/item/10/", json=SAMPLE_ITEM) + + result = api_header.upload_file(456, filepath, max_workers=1) + assert result["id"] == 789 + + # Verify the start-upload request has correct file metadata + start_body = json.loads(responses.calls[0].request.body) + assert start_body["item_data"]["size"] == 100 + assert start_body["item_data"]["content_type"] is not None + + # Verify the actual file content was uploaded to S3 + s3_body = responses.calls[3].request.body + assert len(s3_body) == 100 + finally: + os.unlink(filepath) + + @responses.activate + def test_chunking_with_multiple_parts(self, api_header): + """Verify file is split into correct number of chunks.""" + # 15 bytes with chunk_size=5 = 3 parts + filepath = _make_temp_file(suffix=".mp4", size=15) + try: + responses.add( + responses.POST, + HOST + "/uploads/stats/upload-start/", + json={"item_id": 10, "item_uuid": "abc-123"}, + ) + responses.add( + responses.POST, + HOST + "/uploads/multipart-upload/", + json={"uploadId": "upload-1", "key": "upload-key"}, + ) + # 3 sign-part + 3 S3 PUT calls + for i in range(1, 4): + responses.add( + responses.GET, + HOST + "/uploads/multipart-upload/upload-1/sign-part/%d/" % i, + json={"url": "https://s3.example.com/part%d" % i}, + ) + responses.add( + responses.PUT, + "https://s3.example.com/part%d" % i, + headers={"ETag": '"etag%d"' % i}, + status=200, + ) + responses.add( + responses.POST, + HOST + "/uploads/multipart-upload/upload-1/complete/", + json={"ok": True}, + ) + responses.add(responses.GET, HOST + "/api/v1/item/10/", json=SAMPLE_ITEM) + + result = api_header.upload_file(456, filepath, chunk_size=5, max_workers=1) + assert result["id"] == 789 + + # Verify complete request has 3 parts + complete_body = json.loads(responses.calls[8].request.body) + assert len(complete_body["parts"]) == 3 + for i, part in enumerate(complete_body["parts"], 1): + assert part["PartNumber"] == i + finally: + os.unlink(filepath) + + +class TestOsStatBehavior: + def test_stat_returns_correct_size(self): + """Verify os.stat works consistently across Python versions.""" + for size in [0, 1, 1024, 5 * 1024 * 1024]: + filepath = _make_temp_file(size=size) + try: + assert os.stat(filepath).st_size == size + finally: + os.unlink(filepath) + + +class TestMimetypesConsistency: + def test_common_media_types(self): + """Verify mimetypes.guess_type returns expected types across Python versions.""" + import mimetypes + + expected = { + ".mp4": "video/mp4", + ".mov": "video/quicktime", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webm": "video/webm", + ".pdf": "application/pdf", + ".gif": "image/gif", + } + for ext, expected_type in expected.items(): + filepath = _make_temp_file(suffix=ext, content=b"x") + try: + guessed = mimetypes.guess_type(filepath, strict=False)[0] + assert guessed == expected_type, "Expected %s for %s, got %s" % (expected_type, ext, guessed) + finally: + os.unlink(filepath) diff --git a/tests/test_reviews.py b/tests/test_reviews.py new file mode 100644 index 0000000..021361c --- /dev/null +++ b/tests/test_reviews.py @@ -0,0 +1,167 @@ +"""Tests for review CRUD and section methods.""" + +import json + +import responses + +from tests.conftest import HOST, SAMPLE_REVIEW, make_list_response + + +class TestCreateReview: + @responses.activate + def test_create_review(self, api): + responses.add(responses.POST, HOST + "/api/v1/review/", json=SAMPLE_REVIEW) + result = api.create_review(123, "Test Review", "A test review") + assert result["id"] == 456 + body = json.loads(responses.calls[0].request.body) + assert body["project"] == "/api/v1/project/123/" + assert body["name"] == "Test Review" + assert body["description"] == "A test review" + + @responses.activate + def test_create_review_with_extra_data(self, api): + responses.add(responses.POST, HOST + "/api/v1/review/", json=SAMPLE_REVIEW) + api.create_review(123, "Test", data={"deadline": "2026-01-01"}) + body = json.loads(responses.calls[0].request.body) + assert body["deadline"] == "2026-01-01" + + +class TestGetReviews: + @responses.activate + def test_get_reviews_by_project_id(self, api): + responses.add(responses.GET, HOST + "/api/v1/review/", json=make_list_response([SAMPLE_REVIEW])) + api.get_reviews_by_project_id(123) + params = responses.calls[0].request.params + assert params["project__id"] == "123" + assert params["project__active"] == "1" + assert params["project__is_archived"] == "0" + + @responses.activate + def test_get_reviews_with_pagination(self, api): + responses.add(responses.GET, HOST + "/api/v1/review/", json=make_list_response([])) + api.get_reviews_by_project_id(123, limit=50, offset=10) + params = responses.calls[0].request.params + assert params["limit"] == "50" + assert params["offset"] == "10" + + @responses.activate + def test_get_review_by_name(self, api): + responses.add(responses.GET, HOST + "/api/v1/review/", json=make_list_response([SAMPLE_REVIEW])) + api.get_review_by_name("Test") + params = responses.calls[0].request.params + assert params["name__istartswith"] == "Test" + assert params["active"] == "True" + + @responses.activate + def test_get_review_by_id(self, api): + responses.add(responses.GET, HOST + "/api/v1/review/456/", json=SAMPLE_REVIEW) + result = api.get_review_by_id(456) + assert result["id"] == 456 + + +class TestGetReviewByUuid: + @responses.activate + def test_found(self, api): + responses.add( + responses.GET, + HOST + "/api/v1/review/", + json=make_list_response([SAMPLE_REVIEW]), + ) + result = api.get_review_by_uuid("abc-def-ghi") + assert result["uuid"] == "abc-def-ghi" + + @responses.activate + def test_not_found(self, api): + responses.add(responses.GET, HOST + "/api/v1/review/", json=make_list_response([])) + result = api.get_review_by_uuid("nonexistent") + assert result is None + + @responses.activate + def test_raw_response(self, api): + responses.add(responses.GET, HOST + "/api/v1/review/", json=make_list_response([SAMPLE_REVIEW])) + result = api.get_review_by_uuid("abc-def-ghi", raw_response=True) + assert hasattr(result, "status_code") + + +class TestReviewStorage: + @responses.activate + def test_get_review_storage(self, api): + responses.add(responses.GET, HOST + "/api/v2/review/456/storage/", json={"storage": 5000}) + result = api.get_review_storage(456) + assert result["storage"] == 5000 + + +class TestUpdateReview: + @responses.activate + def test_update_review(self, api): + responses.add(responses.PATCH, HOST + "/api/v1/review/456/", json={"id": 456, "name": "Updated"}) + result = api.update_review(456, {"name": "Updated"}) + assert result["name"] == "Updated" + + def test_non_dict_returns_false(self, api): + assert api.update_review(456, "not a dict") is False + + +class TestSortReviewItems: + @responses.activate + def test_sort_items(self, api): + items = [{"id": 1, "sortorder": 0}, {"id": 2, "sortorder": 1}] + responses.add(responses.PUT, HOST + "/api/v2/review/456/sort_items/", json={"updated_items": 2}) + result = api.sort_review_items(456, items) + assert result["updated_items"] == 2 + + def test_non_list_returns_false(self, api): + assert api.sort_review_items(456, "not a list") is False + + +class TestArchiveRestoreDeleteReview: + @responses.activate + def test_archive_review(self, api): + responses.add(responses.POST, HOST + "/api/v2/review/456/archive/", json={}, status=200) + result = api.archive_review(456) + # Default raw_response=True + assert hasattr(result, "status_code") + + @responses.activate + def test_restore_review(self, api): + responses.add(responses.POST, HOST + "/api/v2/review/456/restore/", json={}, status=200) + result = api.restore_review(456) + assert hasattr(result, "status_code") + + @responses.activate + def test_delete_review(self, api): + responses.add(responses.PATCH, HOST + "/api/v1/review/456/", json={}) + api.delete_review(456) + body = json.loads(responses.calls[0].request.body) + assert body["active"] is False + + +class TestReviewSections: + @responses.activate + def test_create_section(self, api): + responses.add(responses.POST, HOST + "/api/v2/review/456/sections/create/", json={"name": "Section 1"}) + result = api.create_review_section(456, "Section 1", [1, 2, 3]) + body = json.loads(responses.calls[0].request.body) + assert body["name"] == "Section 1" + assert body["itemIds"] == [1, 2, 3] + assert "uuid" not in body + + @responses.activate + def test_create_section_with_uuid(self, api): + responses.add(responses.POST, HOST + "/api/v2/review/456/sections/create/", json={}) + api.create_review_section(456, "Section", [1], uuid="my-uuid") + body = json.loads(responses.calls[0].request.body) + assert body["uuid"] == "my-uuid" + + @responses.activate + def test_update_sections(self, api): + data = [{"uuid": "sec-1", "name": "Updated", "itemIds": [1, 2]}] + responses.add(responses.PUT, HOST + "/api/v2/review/456/sections/bulk-update/", json={}) + api.update_review_sections(456, data) + assert responses.calls[0].request.method == "PUT" + + @responses.activate + def test_delete_section(self, api): + responses.add(responses.DELETE, HOST + "/api/v2/review/456/sections/sec-uuid-1/", json={}) + api.delete_review_section(456, "sec-uuid-1") + assert responses.calls[0].request.method == "DELETE" diff --git a/tests/test_shotgrid.py b/tests/test_shotgrid.py new file mode 100644 index 0000000..254b968 --- /dev/null +++ b/tests/test_shotgrid.py @@ -0,0 +1,130 @@ +"""Tests for Shotgrid integration methods.""" + +import json + +import pytest +import responses + +from tests.conftest import HOST + + +class TestShotgridCreateConfig: + @responses.activate + def test_success(self, api): + data = {"url": "https://sg.example.com", "username": "script_user", "key": "script_key"} + # Test endpoint + responses.add(responses.POST, HOST + "/api/v2/shotgun/config/test/", json={"ok": True}, status=200) + # Config endpoint + responses.add(responses.POST, HOST + "/api/v2/shotgun/config/", json={"id": 1}, status=200) + + result = api.shotgrid_create_config(1, syncsketch_project_id=10, data=data) + assert hasattr(result, "status_code") # raw_response=True default + assert result.status_code == 200 + + @responses.activate + def test_test_fails_raises(self, api): + data = {"url": "https://sg.example.com", "username": "user", "key": "key"} + responses.add(responses.POST, HOST + "/api/v2/shotgun/config/test/", json={}, status=400) + + with pytest.raises(Exception, match="configuration test failed"): + api.shotgrid_create_config(1, data=data) + + def test_missing_url_asserts(self, api): + with pytest.raises(AssertionError): + api.shotgrid_create_config(1, data={"username": "u", "key": "k"}) + + def test_missing_username_asserts(self, api): + with pytest.raises(AssertionError): + api.shotgrid_create_config(1, data={"url": "http://sg", "key": "k"}) + + def test_missing_key_asserts(self, api): + with pytest.raises(AssertionError): + api.shotgrid_create_config(1, data={"url": "http://sg", "username": "u"}) + + def test_non_dict_asserts(self, api): + with pytest.raises(AssertionError): + api.shotgrid_create_config(1, data="not a dict") + + +class TestShotgridGetPlaylists: + @responses.activate + def test_get_playlists(self, api): + responses.add(responses.GET, HOST + "/api/v2/shotgun/playlists/1/10/", json={"playlists": []}) + result = api.shotgrid_get_playlists(1, 10) + assert "playlists" in result + + @responses.activate + def test_with_shotgun_project_id(self, api): + responses.add(responses.GET, HOST + "/api/v2/shotgun/playlists/1/10/", json={}) + api.shotgrid_get_playlists(1, 10, shotgun_project_id=99) + assert responses.calls[0].request.params["shotgun_project_id"] == "99" + + +class TestShotgridSyncReviewNotes: + @responses.activate + def test_sync_notes(self, api): + url = HOST + "/api/v2/shotgun/sync-review-notes/review/456/" + responses.add(responses.POST, url, json={"status": "processing", "task_id": "t1"}) + result = api.shotgrid_sync_review_notes(456) + assert result["task_id"] == "t1" + + +class TestShotgridSyncNewItemNotes: + @responses.activate + def test_sync_item_notes(self, api): + url = HOST + "/api/v2/shotgun/sync-notes/project/123/review/456/789/" + responses.add(responses.POST, url, json={"comments": 3, "sketches": 1}) + result = api.shotgrid_sync_new_item_notes(123, 456, 789) + assert result["comments"] == 3 + + +class TestGetShotgridSyncReviewNotesProgress: + @responses.activate + def test_get_progress(self, api): + url = HOST + "/api/v2/shotgun/sync-review-notes/task-1/" + responses.add(responses.GET, url, json={"status": "done", "percent_complete": 100}) + result = api.get_shotgrid_sync_review_notes_progress("task-1") + assert result["percent_complete"] == 100 + + +class TestShotgridSyncReviewItems: + @responses.activate + def test_sync_items(self, api): + check_url = HOST + "/api/v2/shotgun/sync-items/project/123/check/" + responses.add( + responses.POST, + check_url, + json={ + "review_id": 456, + "items": [{"id": 1, "name": "shot1"}, {"id": 2, "name": "shot2"}], + }, + ) + # Per-item sync calls + sync_url = HOST + "/api/v2/shotgun/sync-items/project/123/review/456/" + responses.add(responses.POST, sync_url, json={"id": 101}) + responses.add(responses.POST, sync_url, json={"id": 102}) + + result = api.shotgrid_sync_review_items(123, "playlist_code", 999) + assert result["review_id"] == 456 + assert result["items"] == [101, 102] + assert result["total_items"] == 2 + assert result["status"] == "done" + + @responses.activate + def test_with_review_id(self, api): + check_url = HOST + "/api/v2/shotgun/sync-items/project/123/review/456/check/" + responses.add(responses.POST, check_url, json={"review_id": 456, "items": []}) + result = api.shotgrid_sync_review_items(123, "code", 999, review_id=456) + assert result["review_id"] == 456 + + +class TestShotgridGetProjects: + def test_raises_deprecation(self, api): + with pytest.raises(DeprecationWarning): + api.shotgrid_get_projects(123) + + +class TestGetShotgridSyncReviewItemsProgress: + def test_raises_deprecation(self, api): + with pytest.raises(DeprecationWarning): + api.get_shotgrid_sync_review_items_progress("task-1") diff --git a/tests/test_tree.py b/tests/test_tree.py new file mode 100644 index 0000000..05c5ed5 --- /dev/null +++ b/tests/test_tree.py @@ -0,0 +1,27 @@ +"""Tests for get_tree method.""" + +import responses + +from tests.conftest import HOST + + +class TestGetTree: + @responses.activate + def test_without_items(self, api): + tree_data = {"accounts": [{"id": 1, "projects": []}]} + responses.add(responses.GET, HOST + "/api/v1/person/tree/", json=tree_data) + result = api.get_tree() + assert result == tree_data + assert "fetchItems" not in responses.calls[0].request.params + + @responses.activate + def test_with_items(self, api): + responses.add(responses.GET, HOST + "/api/v1/person/tree/", json={}) + api.get_tree(withItems=True) + assert responses.calls[0].request.params["fetchItems"] == "1" + + @responses.activate + def test_raw_response(self, api): + responses.add(responses.GET, HOST + "/api/v1/person/tree/", json={}) + result = api.get_tree(raw_response=True) + assert hasattr(result, "status_code") diff --git a/tests/test_users.py b/tests/test_users.py new file mode 100644 index 0000000..5f5601e --- /dev/null +++ b/tests/test_users.py @@ -0,0 +1,146 @@ +"""Tests for user lookup and access control methods.""" + +import json + +import responses + +from tests.conftest import HOST, SAMPLE_USER, make_list_response + + +class TestGetUsersByName: + @responses.activate + def test_search_by_name(self, api): + responses.add(responses.GET, HOST + "/api/v1/simpleperson/", json=make_list_response([SAMPLE_USER])) + api.get_users_by_name("Test") + assert responses.calls[0].request.params["name"] == "Test" + + +class TestGetUserByEmail: + @responses.activate + def test_found(self, api): + responses.add( + responses.GET, + HOST + "/api/v1/simpleperson/", + json=make_list_response([SAMPLE_USER]), + ) + result = api.get_user_by_email("test@example.com") + assert result["email"] == "test@example.com" + assert responses.calls[0].request.params["email__iexact"] == "test@example.com" + + @responses.activate + def test_not_found(self, api): + responses.add( + responses.GET, + HOST + "/api/v1/simpleperson/", + json=make_list_response([]), + ) + result = api.get_user_by_email("nobody@example.com") + assert result is None + + +class TestGetUsersByProjectId: + @responses.activate + def test_get_users(self, api): + responses.add(responses.GET, HOST + "/api/v2/all-project-users/123/", json=[SAMPLE_USER]) + result = api.get_users_by_project_id(123) + assert result[0]["id"] == 42 + + +class TestGetConnectionsByUserId: + @responses.activate + def test_basic(self, api): + responses.add(responses.GET, HOST + "/api/v2/user/42/connections/account/1/", json=[]) + api.get_connections_by_user_id(42, 1) + assert "/api/v2/user/42/connections/account/1/" in responses.calls[0].request.url + + @responses.activate + def test_with_flags(self, api): + responses.add(responses.GET, HOST + "/api/v2/user/42/connections/account/1/", json=[]) + api.get_connections_by_user_id(42, 1, include_inactive=True, include_archived=False) + params = responses.calls[0].request.params + assert params["include_inactive"] == "true" + assert params["include_archived"] == "false" + + +class TestGetUserById: + @responses.activate + def test_get_by_id(self, api): + responses.add(responses.GET, HOST + "/api/v1/simpleperson/42/", json=SAMPLE_USER) + result = api.get_user_by_id(42) + assert result["id"] == 42 + + +class TestGetCurrentUser: + @responses.activate + def test_get_current_user(self, api): + responses.add(responses.GET, HOST + "/api/v1/simpleperson/currentUser/", json=SAMPLE_USER) + result = api.get_current_user() + assert result["email"] == "test@example.com" + + +class TestAddUsersToWorkspace: + @responses.activate + def test_add_users(self, api): + users = [{"email": "new@test.de", "permission": "admin"}] + responses.add(responses.POST, HOST + "/api/v2/add-users/", json={"success": True}) + api.add_users_to_workspace(1, users, note="Welcome!") + body = json.loads(responses.calls[0].request.body) + assert body["which"] == "account" + assert body["entity_id"] == 1 + assert body["note"] == "Welcome!" + assert json.loads(body["users"]) == users + + def test_non_list_returns_false(self, api): + assert api.add_users_to_workspace(1, "not a list") is False + + +class TestRemoveUsersFromWorkspace: + @responses.activate + def test_remove_users(self, api): + users = [{"email": "old@test.de"}] + responses.add(responses.POST, HOST + "/api/v2/remove-users/", json={"success": True}) + api.remove_users_from_workspace(1, users) + body = json.loads(responses.calls[0].request.body) + assert body["which"] == "account" + assert body["entity_id"] == 1 + + def test_non_list_returns_false(self, api): + assert api.remove_users_from_workspace(1, "not a list") is False + + +class TestAddUsersToProject: + @responses.activate + def test_add_users(self, api): + users = [{"email": "new@test.de", "permission": "viewer"}] + responses.add(responses.POST, HOST + "/api/v2/add-users/", json={"success": True}) + api.add_users_to_project(123, users, note="Invite") + body = json.loads(responses.calls[0].request.body) + assert body["which"] == "project" + assert body["entity_id"] == 123 + + def test_non_list_returns_false(self, api): + assert api.add_users_to_project(123, "not a list") is False + + +class TestRemoveUsersFromProject: + @responses.activate + def test_remove_users(self, api): + users = [{"email": "old@test.de"}] + responses.add(responses.POST, HOST + "/api/v2/remove-users/", json={"success": True}) + api.remove_users_from_project(123, users) + body = json.loads(responses.calls[0].request.body) + assert body["which"] == "project" + assert body["entity_id"] == 123 + + def test_non_list_returns_false(self, api): + assert api.remove_users_from_project(123, "not a list") is False + + +class TestAddUsersDeprecated: + @responses.activate + def test_delegates_to_add_users_to_project(self, api): + users = [{"email": "test@test.de", "permission": "viewer"}] + responses.add(responses.POST, HOST + "/api/v2/add-users/", json={"success": True}) + api.add_users(123, users) + body = json.loads(responses.calls[0].request.body) + assert body["which"] == "project" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..457e847 --- /dev/null +++ b/tox.ini @@ -0,0 +1,11 @@ +[tox] +envlist = py27, py37, py38, py39, py310, py311, py312, py313, py314 +skip_missing_interpreters = true + +[testenv] +deps = + pytest>=7.0,<9.0 + pytest-cov>=4.0 + responses>=0.20.0 +commands = + pytest {posargs:tests/ --cov=syncsketch --cov-report=term-missing}