pax_global_header00006660000000000000000000000064147215506600014520gustar00rootroot0000000000000052 comment=a817be3ae54d3432887b355316741e82bc44f756 python-gspread-6.1.4/000077500000000000000000000000001472155066000144745ustar00rootroot00000000000000python-gspread-6.1.4/HISTORY.rst000066400000000000000000001173361472155066000164020ustar00rootroot00000000000000Release History =============== 6.1.4 (2024-10-21) ------------------ * remove dependency on requests-2.27.0 6.1.3 (2024-10-03) ------------------ * ignore jinja CVE by @lavigne958 in https://github.com/burnash/gspread/pull/1481 * Remove passing exception as args to super in APIError by @mike-flowers-airbnb in https://github.com/burnash/gspread/pull/1477 * better handler API error parsing. by @lavigne958 in https://github.com/burnash/gspread/pull/1510 * Add test on receiving an invalid JSON in the APIError exception handler. by @lavigne958 in https://github.com/burnash/gspread/pull/1512 6.1.2 (2024-05-17) ------------------ * add note about runnings tests to contrib guide by @alifeee in https://github.com/burnash/gspread/pull/1465 * Some updates on `get_notes` by @nbwzx in https://github.com/burnash/gspread/pull/1461 6.1.1 (2024-05-16) ------------------ * Add some missing typing in code by @lavigne958 in https://github.com/burnash/gspread/pull/1448 * More fixes for `Worksheet.update` argument ordering & single cell updating (i.e. now `Worksheet.update_acell`) by @alexmalins in https://github.com/burnash/gspread/pull/1449 * Added 'add_data_validation` to `Workhsheet` [Issue #1420] by @muddi900 in https://github.com/burnash/gspread/pull/1444 * Bump typing-extensions from 4.10.0 to 4.11.0 by @dependabot in https://github.com/burnash/gspread/pull/1450 * Bump black from 23.3.0 to 24.4.0 by @dependabot in https://github.com/burnash/gspread/pull/1452 * Fix incorrect version number in HISTORY.rst from 6.0.1 to 6.1.0 by @yhay81 in https://github.com/burnash/gspread/pull/1455 * add `get_notes` by @nbwzx in https://github.com/burnash/gspread/pull/1451 * Bump mypy from 1.9.0 to 1.10.0 by @dependabot in https://github.com/burnash/gspread/pull/1459 * Bump black from 24.4.0 to 24.4.2 by @dependabot in https://github.com/burnash/gspread/pull/1460 * bugfix: handle domain name in spreadsheet copy permissions by @lavigne958 in https://github.com/burnash/gspread/pull/1458 * Fix/api key auth version by @alifeee in https://github.com/burnash/gspread/pull/1463 * Ignore pip vulnerabilities in CI. by @lavigne958 in https://github.com/burnash/gspread/pull/1464 * Remove StrEnum dependency and added custom class[issue #1462] by @muddi900 in https://github.com/burnash/gspread/pull/1469 6.1.0 (2024-03-28) ------------------ * Add py.typed marker by @lavigne958 in https://github.com/burnash/gspread/pull/1422 * Improve back-off client by @lavigne958 in https://github.com/burnash/gspread/pull/1415 * Add new auth method API key by @lavigne958 in https://github.com/burnash/gspread/pull/1428 * Bugfix/add set timeout by @lavigne958 in https://github.com/burnash/gspread/pull/1417 * Fix wrapper `cast_to_a1_notation` by @lavigne958 in https://github.com/burnash/gspread/pull/1427 * Bump bandit from 1.7.5 to 1.7.8 by @dependabot in https://github.com/burnash/gspread/pull/1433 * Bump mypy from 1.6.1 to 1.9.0 by @dependabot in https://github.com/burnash/gspread/pull/1432 * Bump typing-extensions from 4.8.0 to 4.10.0 by @dependabot in https://github.com/burnash/gspread/pull/1424 * Bump flake8 from 5.0.4 to 7.0.0 by @dependabot in https://github.com/burnash/gspread/pull/1375 * fix error message readability by @imrehg in https://github.com/burnash/gspread/pull/1435 * Add missing method `import_csv()` by @lavigne958 in https://github.com/burnash/gspread/pull/1426 * update readme examples by @alifeee in https://github.com/burnash/gspread/pull/1431 * Add user friendly message when we can't override a test cassette by @lavigne958 in https://github.com/burnash/gspread/pull/1438 * Allow "warning" type protected ranges by @alifeee in https://github.com/burnash/gspread/pull/1439 * Improve README and documentation with value render options by @lavigne958 in https://github.com/burnash/gspread/pull/1446 6.0.2 (2024-02-14) ------------------ * Fixup gspread client init arguments by @lavigne958 in https://github.com/burnash/gspread/pull/1412 6.0.1 (2024-02-06) ------------------ * Allow client to use external Session object by @lavigne958 in https://github.com/burnash/gspread/pull/1384 * Remove-py-3.7-support by @alifeee in https://github.com/burnash/gspread/pull/1396 * bugfix/client export by @lavigne958 in https://github.com/burnash/gspread/pull/1392 * Fix oauth flow typo by @alifeee in https://github.com/burnash/gspread/pull/1397 * check oauth creds type using `isinstance` by @alifeee in https://github.com/burnash/gspread/pull/1398 * Fix type hints at find method in worksheet.py by @deftfitf in https://github.com/burnash/gspread/pull/1407 * Fixup get empty cell value is `None` by @lavigne958 in https://github.com/burnash/gspread/pull/1404 * Fix missing attribute `spreadsheet` in `Worksheet`. by @lavigne958 in https://github.com/burnash/gspread/pull/1402 * update migration guide by @alifeee in https://github.com/burnash/gspread/pull/1409 6.0.0 (2024-01-28) ------------------ New Contributor * Remove deprecated method delete_row by @cgkoutzigiannis in https://github.com/burnash/gspread/pull/1062 * Initial typing in client.py by @OskarBrzeski in https://github.com/burnash/gspread/pull/1159 * Split client http client by @lavigne958 in https://github.com/burnash/gspread/pull/1190 * Spelling fix & update docs with date_time_render_option behaviour by @alifeee in https://github.com/burnash/gspread/pull/1187 * #966 Add sketch typing for utils.py by @butvinm in https://github.com/burnash/gspread/pull/1196 * Remove accepted_kwargs decorator by @lavigne958 in https://github.com/burnash/gspread/pull/1229 * Remove/python-3.7 by @alifeee in https://github.com/burnash/gspread/pull/1234 * Bump isort from 5.11.4 to 5.12.0 by @dependabot in https://github.com/burnash/gspread/pull/1165 * bump flake8 to 6.0.0 by @alifeee in https://github.com/burnash/gspread/pull/1236 * merge master into 6.0.0 by @lavigne958 in https://github.com/burnash/gspread/pull/1241 * Remplace named tuples with enums by @lavigne958 in https://github.com/burnash/gspread/pull/1250 * Feature/add type hints worksheets by @lavigne958 in https://github.com/burnash/gspread/pull/1254 * Implement hex color conversion by @idonec in https://github.com/burnash/gspread/pull/1270 * remove lastUpdateTime by @alifeee in https://github.com/burnash/gspread/pull/1295 * Merge `master` into `feature/release_6_0_0` by @alifeee in https://github.com/burnash/gspread/pull/1320 * Add type checking to lint by @alifeee in https://github.com/burnash/gspread/pull/1337 * Warning/update swapped args by @alifeee in https://github.com/burnash/gspread/pull/1336 * Improve `Worksheet.sort()` signature by @lavigne958 in https://github.com/burnash/gspread/pull/1342 * Make `get_values` and alias of `get` by @alifeee in https://github.com/burnash/gspread/pull/1296 * fix type issue (remove `.first()` function) by @alifeee in https://github.com/burnash/gspread/pull/1344 * Remove/get records use index by @alifeee in https://github.com/burnash/gspread/pull/1345 * increase warning stacklevel from 1 to 2 by @alifeee in https://github.com/burnash/gspread/pull/1361 * Feature/merge master by @lavigne958 in https://github.com/burnash/gspread/pull/1371 * feature/merge master by @lavigne958 in https://github.com/burnash/gspread/pull/1372 * Simplify get records by @alifeee in https://github.com/burnash/gspread/pull/1374 * Add util function `to_records` to build records by @lavigne958 in https://github.com/burnash/gspread/pull/1377 * feature/add utils get records by @lavigne958 in https://github.com/burnash/gspread/pull/1378 * Add migration guide for get_all_records by @lavigne958 in https://github.com/burnash/gspread/pull/1379 * feature/merge master into release 6 0 0 by @lavigne958 in https://github.com/burnash/gspread/pull/1381 * Feature/release 6 0 0 by @lavigne958 in https://github.com/burnash/gspread/pull/1382 5.12.4 (2023-12-31) ------------------- * Bump actions/setup-python from 4 to 5 by @dependabot in https://github.com/burnash/gspread/pull/1370 * Fixed default value of merge_type parameter in merge_cells function docstring. by @neolooong in https://github.com/burnash/gspread/pull/1373 5.12.3 (2023-12-15) ------------------- * 1363 get all records retrieves a large number of empty rows after the end of the data by @alifeee in https://github.com/burnash/gspread/pull/1364 5.12.2 (2023-12-04) ------------------- * Many fixes for `get_records` by @alifeee in https://github.com/burnash/gspread/pull/1357 * change `worksheet.update` migration guide by @alifeee in https://github.com/burnash/gspread/pull/1362 5.12.1 (2023-11-29) ------------------- * feature/readme migration v6 by @lavigne958 in https://github.com/burnash/gspread/pull/1297 * add deprecation warnings for lastUpdateTime... by @alifeee in https://github.com/burnash/gspread/pull/1333 * remove `use_index` and references to it in `get_records` by @alifeee in https://github.com/burnash/gspread/pull/1343 * make deprecation warning dependent on if kwarg is used for client_factory by @alifeee in https://github.com/burnash/gspread/pull/1349 * fix 1352 expected headers broken by @alifeee in https://github.com/burnash/gspread/pull/1353 * fix `combine_merged_cells` when using from a range that doesn't start at `A1` by @alifeee in https://github.com/burnash/gspread/pull/1335 5.12.0 (2023-10-22) ------------------- * feature -- adding `worksheet.get_records` to get specific row ranges by @AndrewBasem1 in https://github.com/burnash/gspread/pull/1301 * Fix list_spreadsheet_files return value by @mephinet in https://github.com/burnash/gspread/pull/1308 * Fix warning message for `worksheet.update` method by @ksj20 in https://github.com/burnash/gspread/pull/1312 * change lambda function to dict (fix pyupgrade issue) by @alifeee in https://github.com/burnash/gspread/pull/1319 * allows users to silence deprecation warnings by @lavigne958 in https://github.com/burnash/gspread/pull/1324 * Add `maintain_size` to keep asked for size in `get`, `get_values` by @alifeee in https://github.com/burnash/gspread/pull/1305 5.11.3 (2023-09-29) ------------------- * Fix list_spreadsheet_files return value by @mephinet in https://github.com/burnash/gspread/pull/1308 5.11.2 (2023-09-18) ------------------- * Fix merge_combined_cells in get_values (AND 5.11.2 RELEASE) by @alifeee in https://github.com/burnash/gspread/pull/1299 5.11.1 (2023-09-06) ------------------- * Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/burnash/gspread/pull/1288 * remove Drive API access on Spreadsheet init (FIX - VERSION 5.11.1) by @alifeee in https://github.com/burnash/gspread/pull/1291 5.11.0 (2023-09-04) ------------------- * add docs/build to .gitignore by @alifeee in https://github.com/burnash/gspread/pull/1246 * add release process to CONTRIBUTING.md by @alifeee in https://github.com/burnash/gspread/pull/1247 * Update/clean readme badges by @lavigne958 in https://github.com/burnash/gspread/pull/1251 * add test_fill_gaps and docstring for fill_gaps by @alifeee in https://github.com/burnash/gspread/pull/1256 * Remove API calls from `creationTime`/`lastUpdateTime` by @alifeee in https://github.com/burnash/gspread/pull/1255 * Fix Worksheet ID Type Inconsistencies by @FlantasticDan in https://github.com/burnash/gspread/pull/1269 * Add `column_count` prop as well as `col_count` by @alifeee in https://github.com/burnash/gspread/pull/1274 * Add required kwargs with no default value by @lavigne958 in https://github.com/burnash/gspread/pull/1271 * Add deprecation warnings for colors by @alifeee in https://github.com/burnash/gspread/pull/1278 * Add better Exceptions on opening spreadsheets by @alifeee in https://github.com/burnash/gspread/pull/1277 5.10.0 (2023-06-29) ------------------- * Fix rows_auto_resize in worksheet.py by removing redundant self by @MagicMc23 in https://github.com/burnash/gspread/pull/1194 * Add deprecation warning for future release 6.0.x by @lavigne958 in https://github.com/burnash/gspread/pull/1195 * FEATURE: show/hide gridlines (#1197) by @alifeee in https://github.com/burnash/gspread/pull/1202 * CLEANUP: cleanup tox.ini, and ignore ./env by @alifeee in https://github.com/burnash/gspread/pull/1200 * Refactor/update-contributing-guide by @alifeee in https://github.com/burnash/gspread/pull/1206 * Spelling fix (with legacy option) by @alifeee in https://github.com/burnash/gspread/pull/1210 * 457-fetch-without-hidden-worksheets by @alifeee in https://github.com/burnash/gspread/pull/1207 * Add_deprecated_warning_sort_method by @lavigne958 in https://github.com/burnash/gspread/pull/1198 * Update (and test for) internal properties on change by @alifeee in https://github.com/burnash/gspread/pull/1211 * Feature: Add "Remove tab colour" method by @alifeee in https://github.com/burnash/gspread/pull/1199 * Refresh-test-cassettes by @alifeee in https://github.com/burnash/gspread/pull/1217 * update self._properties after batch_update by @alifeee in https://github.com/burnash/gspread/pull/1221 * 700-fill-merged-cells by @alifeee in https://github.com/burnash/gspread/pull/1215 * Fix/update-internal-properties by @alifeee in https://github.com/burnash/gspread/pull/1225 * Add breaking change warning in Worksheet.update() by @lavigne958 in https://github.com/burnash/gspread/pull/1226 * Bump codespell from 2.2.4 to 2.2.5 by @dependabot in https://github.com/burnash/gspread/pull/1232 * Add/refresh last update time by @alifeee in https://github.com/burnash/gspread/pull/1233 * Update-build-tools by @alifeee in https://github.com/burnash/gspread/pull/1231 * add read the doc configuration file by @lavigne958 in https://github.com/burnash/gspread/pull/1235 * update licence year by @alifeee in https://github.com/burnash/gspread/pull/1237 * remove deprecated methods from tests by @alifeee in https://github.com/burnash/gspread/pull/1238 5.9.0 (2023-05-11) ------------------ * Bugfix/fix get last update time by @lavigne958 in https://github.com/burnash/gspread/pull/1186 * Add batch notes insert/update/clear by @lavigne958 in https://github.com/burnash/gspread/pull/1189 5.8.0 (2023-04-05) ------------------ * Bump black from 22.10.0 to 22.12.0 by @dependabot in https://github.com/burnash/gspread/pull/1154 * Bump isort from 5.10.1 to 5.11.3 by @dependabot in https://github.com/burnash/gspread/pull/1155 * Bump isort from 5.11.3 to 5.11.4 by @dependabot in https://github.com/burnash/gspread/pull/1157 * #1104: added a delete by worksheet id method by @muddi900 in https://github.com/burnash/gspread/pull/1148 * improve CI workflow - upgrade setuptools to fix CVE by @lavigne958 in https://github.com/burnash/gspread/pull/1179 * Bump codespell from 2.2.2 to 2.2.4 by @dependabot in https://github.com/burnash/gspread/pull/1178 * Bump bandit from 1.7.4 to 1.7.5 by @dependabot in https://github.com/burnash/gspread/pull/1177 * Bump black from 22.12.0 to 23.1.0 by @dependabot in https://github.com/burnash/gspread/pull/1168 * Update user-guide.rst to include a warning by @alsaenko in https://github.com/burnash/gspread/pull/1181 * Fixed typo in docs/user-guide.rst by @raboba2re in https://github.com/burnash/gspread/pull/1182 * Bump black from 23.1.0 to 23.3.0 by @dependabot in https://github.com/burnash/gspread/pull/1183 * Handle cases when rgbColor is not set by @lavigne958 in https://github.com/burnash/gspread/pull/1184 5.7.2 (2022-12-03) ------------------ * Fix: `hidden` property might not be set from the API by @lavigne958 in https://github.com/burnash/gspread/pull/1151 5.7.1 (2022-11-17) ------------------ * Fix dependencies required version by @lavigne958 in https://github.com/burnash/gspread/pull/1147 5.7.0 (2022-11-13) ------------------ * chore: Update outdated LICENSE year by @bluzir in https://github.com/burnash/gspread/pull/1124 * add dependabot to maintain dependencies by @lavigne958 in https://github.com/burnash/gspread/pull/1126 * improve trigger on CI by @lavigne958 in https://github.com/burnash/gspread/pull/1134 * Bump bandit from 1.7.0 to 1.7.4 by @dependabot in https://github.com/burnash/gspread/pull/1133 * cancel previous run on same ref by @lavigne958 in https://github.com/burnash/gspread/pull/1135 * Bump actions/setup-python from 2 to 4 by @dependabot in https://github.com/burnash/gspread/pull/1127 * Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/burnash/gspread/pull/1128 * Bump black from 22.3.0 to 22.10.0 by @dependabot in https://github.com/burnash/gspread/pull/1132 * Bump isort from 5.9.3 to 5.10.1 by @dependabot in https://github.com/burnash/gspread/pull/1131 * Bump codespell from 2.1.0 to 2.2.2 by @dependabot in https://github.com/burnash/gspread/pull/1130 * add named tuple for `DateTimeRenderOption` by @lavigne958 in https://github.com/burnash/gspread/pull/1136 * Feature/copy cut paste by @lavigne958 in https://github.com/burnash/gspread/pull/1138 * isSheetHidden method added to worksheet.py by @SazidAF in https://github.com/burnash/gspread/pull/1140 5.6.2 (2022-10-23) ------------------ * update parent folder for `client.copy` method by @lavigne958 in https://github.com/burnash/gspread/pull/1123 5.6.0 (2022-09-10) ------------------ * Fix `clear_note` method when using numeric boundaries by @lavigne958 in https://github.com/burnash/gspread/pull/1106 * Fix a typo in the permissions:create API payload by @jiananma in https://github.com/burnash/gspread/pull/1107 * Fix spreadsheet URL by @lavigne958 in https://github.com/burnash/gspread/pull/1110 * Return created permission on `Spreadsheet.share()` by @lavigne958 in https://github.com/burnash/gspread/pull/1111 * (fixed #1113) Supply correct Google API v3 permission for domains by @NickCrews in https://github.com/burnash/gspread/pull/1115 * Bugfix/numericese all by @lavigne958 in https://github.com/burnash/gspread/pull/1119 New Contributors **************** * @jiananma made their first contribution in https://github.com/burnash/gspread/pull/1107 * @NickCrews made their first contribution in https://github.com/burnash/gspread/pull/1115 5.5.0 (2022-08-31) ------------------ * Use pathlib by @lavigne958 in https://github.com/burnash/gspread/pull/1057 * Migrate to drive API V3 by @lavigne958 in https://github.com/burnash/gspread/pull/1060 * Implement __eq__ method for `Cell` by @chisvi in https://github.com/burnash/gspread/pull/1063 * Add missing documentation on `set_timeout` by @lavigne958 in https://github.com/burnash/gspread/pull/1070 * Add method to transfer / accept ownership of a spreadsheet by @lavigne958 in https://github.com/burnash/gspread/pull/1068 * Add `client_factory` param to `auth` methods by @jlumbroso in https://github.com/burnash/gspread/pull/1075 * Fix `list_protected_ranges` by @lavigne958 in https://github.com/burnash/gspread/pull/1076 * Add function to convert column letter to column index by @lavigne958 in https://github.com/burnash/gspread/pull/1077 * Fix docstring name of named_range() param by @dgilman in https://github.com/burnash/gspread/pull/1081 * Fix grammar in docstring for client.export by @dgilman in https://github.com/burnash/gspread/pull/1080 * Many typo fixes to worksheet docstrings by @dgilman in https://github.com/burnash/gspread/pull/1083 * Fix function `numericise_all` by @lavigne958 in https://github.com/burnash/gspread/pull/1082 * Fix documentation about `oauth_from_dict` by @lavigne958 in https://github.com/burnash/gspread/pull/1088 * inherit_from_before option for insert_row/insert_rows by @yongrenjie in https://github.com/burnash/gspread/pull/1092 * add method to change the color of a tab by @lavigne958 in https://github.com/burnash/gspread/pull/1095 * docs: Fix a few typos by @timgates42 in https://github.com/burnash/gspread/pull/1094 * Fix typo in `Worksheet.batch_format` method by @lavigne958 in https://github.com/burnash/gspread/pull/1101 New Contributors **************** * @chisvi made their first contribution in https://github.com/burnash/gspread/pull/1063 * @jlumbroso made their first contribution in https://github.com/burnash/gspread/pull/1075 * @yongrenjie made their first contribution in https://github.com/burnash/gspread/pull/1092 5.4.0 (2022-06-01) ------------------ * fix typo by @joswlv in https://github.com/burnash/gspread/pull/1031 * Fix error message in `get_all_records` by @lavigne958 in https://github.com/burnash/gspread/pull/1028 * Added feature request #1022. Auto resizing is now available for rows … by @mketer1 in https://github.com/burnash/gspread/pull/1033 * add new method to hide/show a worksheet by @lavigne958 in https://github.com/burnash/gspread/pull/1030 * feat: Download PDF from Spreadsheet #1035 by @100paperkite in https://github.com/burnash/gspread/pull/1036 * Add test on `auto_resize_columns` by @lavigne958 in https://github.com/burnash/gspread/pull/1039 * Add method to unmerge cells by @lavigne958 in https://github.com/burnash/gspread/pull/1040 * Add method to delete a protected range by @lavigne958 in https://github.com/burnash/gspread/pull/1042 * Feature/clean organize documentation by @lavigne958 in https://github.com/burnash/gspread/pull/1043 * Add warning about deprecated oauth flow by @lavigne958 in https://github.com/burnash/gspread/pull/1047 * Add new `batch_format` method. by @lavigne958 in https://github.com/burnash/gspread/pull/1049 * Encode string to utf-8 when importing CSV content by @lavigne958 in https://github.com/burnash/gspread/pull/1054 New Contributors **************** * @joswlv made their first contribution in https://github.com/burnash/gspread/pull/1031 * @mketer1 made their first contribution in https://github.com/burnash/gspread/pull/1033 * @100paperkite made their first contribution in https://github.com/burnash/gspread/pull/1036 5.3.2 (2022-04-12) ------------------ * Bugfix/black python3.10 by @lavigne958 in https://github.com/burnash/gspread/pull/1020 * Automate releases by @lavigne958 in https://github.com/burnash/gspread/pull/1025 * Bugfix/get all record duplicated columns by @lavigne958 in https://github.com/burnash/gspread/pull/1021 5.3.0 (2022-03-28) ------------------ * Feature/rework test cassettes recording by @lavigne958 in https://github.com/burnash/gspread/pull/1004 * add method list protected ranges by @lavigne958 in https://github.com/burnash/gspread/pull/1008 * Add new methods to add/list/delete dimensionGroups by @lavigne958 in https://github.com/burnash/gspread/pull/1010 * Add method to hide rows/columns by @lavigne958 in https://github.com/burnash/gspread/pull/1012 * Add ability to rename Spreadsheets (via a new Spreadsheet.update_title) by @jansim in https://github.com/burnash/gspread/pull/1013 ## New Contributors * @jansim made their first contribution in https://github.com/burnash/gspread/pull/1013 5.2.0 (2022-02-27) ------------------ * Copy comments when during spreadsheet copy by @lavigne958 in https://github.com/burnash/gspread/pull/979 * Update user-guide.rst by @maky-hnou in https://github.com/burnash/gspread/pull/980 * merge setup test cassettes by @lavigne958 in https://github.com/burnash/gspread/pull/982 * Feature/add header validation get all records by @lavigne958 in https://github.com/burnash/gspread/pull/984 * Add timeout to client by @lavigne958 in https://github.com/burnash/gspread/pull/987 * Feature/update timezone and locale by @lavigne958 in https://github.com/burnash/gspread/pull/989 * Feature/make case comparison in find by @lavigne958 in https://github.com/burnash/gspread/pull/990 * Updated API rate limits by @hvinayan in https://github.com/burnash/gspread/pull/993 * Feature/prevent insert row to sheet with colon by @lavigne958 in https://github.com/burnash/gspread/pull/992 ## New Contributors * @maky-hnou made their first contribution in https://github.com/burnash/gspread/pull/980 * @hvinayan made their first contribution in https://github.com/burnash/gspread/pull/993 5.1.1 (2021-12-22) ------------------ * Fix documentation about oauth (#975 by @lavigne958) 5.1.0 (2021-12-22) ------------------ * Codespell skip docs build folder (#962 by @lavigne958) * Update contributing guidelines (#964 by @lavigne958) * Add oauth from dict (#967 by @lavigne958) * Update README.md to include badges (#970 by @lavigne958) * Add new method to get all values as a list of Cells (#968 by @lavigne958) * automatic conversion of a cell letter to uppercase (#972 by @Burovytskyi) 5.0.0 (2021-11-26) ------------------ * Fix a typo in HISTORY.rst (#904 by @TurnrDev) * Fix typo and fix return value written in docstrings (#903 by @rariyama) * Add deprecation warning for delete_row method in documentation (#909 by @javad94) * split files `models.py` and `test.py` (#912 by @lavigne958) * parent 39d1ecb59ca3149a8f46094c720efab883a0dc11 author Christian Clauss 1621149013 +0200 commit ter Christian Clauss 1630103641 +0200 (#869 by @cclaus) * Enable code linter in CI (#915 by @lavigne958) * isort your imports (again), so you don't have to (#914 by @cclaus) * lint_python.yml: Try 'tox -e py' to test current Python (#916 by @cclaus) * Add more flake8 tests (#917 by @cclaus) * Update test suite (#918 by @cclaus) * Avoid IndexError when row_values() returns an empty row (#920 by @cclaus) * Bugfix - remove wrong argument in `batch_update` docstring (#912 by @lavigne958) * Improvement - Add `Worksheet.index` property (#922 by @lavigne958) * Add ability to create directory if it does not exist before saving the credentials to disk. (#925 by @benhoman) * Update test framework and VCR and cassettes (#926 by @lavigne958) * Delete .travis.yml (#928 by @cclaus) * Update tox.ini with all linting commands under lint env (by @lavigne958) * Build package and docs in CI (#930 by @lavigne958) * Update oauth2.rst (#933 by @amlestin) * Update the link to the Google Developers Console (#934 by @Croebh) * allow tests to run on windows, add and improve tests in WorksheetTests, add test on unbounded range, use canonical range as specified in the API, add test cassettes, prevent InvalidGridRange, improve code formatting (#937 by @Fendse) * fix fully qualified class names in API documentation (#944 by @geoffbeier) * fix editor_users_emails - get only from list not all users added to spreadsheet (#939 by @Lukasz) * add shadow method to get a named range from a speadsheet instance (#941 by @lavigne958) * auto_resize_columns (#948 by @FelipeSantos75) * add functions for defining, deleting and listing named ranges (#945 by @p-doyle) * Implement `open` sheet within Drive folder (#951 by @datavaluepeople) * Fix get range for unbounded ranges (#954 by @lavigne958) * remove potential I/O when reading spreadsheet title (956 by @lavigne958) * Add include_values_in_response to append_row & append_rows (#957 by @martimarkov) * replace raw string "ROWS" & "COLUMNS" to Dimension named tuple, replace raw string "FORMATTED_VALUE", "UNFORMATTED_VALUE", "FORMULA" to ValueRenderOption named tuple, replace raw string "RAW", "USER_ENTERED" to ValueInputOption named tuple (#958 by @ccppoo) 4.0.1 (2021-08-07) ------------------ * Do not overwrite original value when trying to convert to a number (#902 by @lavigne958) 4.0.0 (2021-08-01) ------------------ * Changed `Worksheet.find()` method returns `None` if nothing is found (#899 by @GastonBC) * Add `Worksheet.batch_clear()` to clear multiple ranges. (#897 by @lavigne958) * Fix `copy_permission` argument comparison in `Client.copy()` method (#898 by @lavigne958) * Allow creation of spreadhsheets in a shared drive (#895 by @lavigne958) * Allow `gspread.oauth()` to accept a custom credential file (#891 by @slmtpz) * Update `tox.ini`, remove python2 from env list (#887 by @cclaus) * Add `SpreadSheet.get_worksheet_by_id()` method (#857 by @a-crovetto) * Fix `store_credentials()` when `authorized_user_filename` is passed (#884 by @neuenmuller) * Remove python2 (#879 by @lavigne958) * Use `Makefile` to run tests (#883 by @lavigne958) * Update documentation `Authentication:For End Users` using OAuth Client ID (#835 by @ManuNaEira) * Allow fetching named ranges from `Worksheet.range()` (#809 by @agatti) * Update README to only mention python3.3+ (#877 by @lavigne958) * Fetch `creation` and `lastUpdate` time from `SpreadSheet` on open (#872 by @lavigne958) * Fix bug with `Worksheet.insert_row()` with `value_input_option` argument (#873 by @elijabesu) * Fix typos in doc and comments (#868 by @cclauss) * Auto cast numeric values from sheet cells to python int or float (#866 by @lavigne958) * Add `Worksheet.get_values()` method (#775 by @burnash) * Allow `gspread.oauth()` to accept a custom filename (#847 by @bastienboutonnet) * Document dictionary credentials auth (#860 by @dmytrostriletskyi) * Add `Worksheet.get_note()` (#855 by @water-ghosts ) * Add steps for creating new keys (#856 by @hanzala-sohrab) * Add `folder_id` argument to `Client.copy()` (#851 by @punnerud) * Fix typos in docstrings (#848 by @dgilman) 3.7.0 (2021-02-18) ------------------ * Add `Worksheet.insert_note()`, `Worksheet.update_note()`, `Worksheet.clear_note()` (#818 by @lavigne958) * Update documentation: oauth2.rst (#836 by @Prometheus3375) * Documentation fixes (#838 by @jayeshmanani) * Documentation fixes (#845 by @creednaylor) * Add `Worksheet.insert_cols()` (#802 by @AlexeyDmitriev) * Documentation fixes (#814 by @hkuffel) * Update README.md (#811 by @tasawar-hussain) * Add `value_render_option` parameter to `Worksheet.get_all_records()` (#776 by @damgad) * Remove `requests` from `install_requires` (#801) * Simplify implementation of `Worksheet.insert_rows()` (#799 by @AlexeyDmitriev) * Add `auth.service_account_from_dict()` (#785 b7 @mahenzon) * Fix `ValueRange.from_json()` (#791 by @erakli) * Update documentation: oauth2.rst (#794 by @elnjensen) * Update documentation: oauth2.rst (#789 by @Takur0) * Allow `auth` to be `None`. Fix #773 (#774 by @lepture) 3.6.0 (2020-04-30) ------------------ * Add `Worksheet.insert_rows()` (#734 by @tr-fi) * Add `Worksheet.copy_to()` (#758 by @JoachimKoenigslieb) * Add ability to create a cell instance using A1 notation (#765 by @tivaliy) * Add `auth.service_account()` (#768) * Add Authlib usage (#552 by @lepture) 3.5.0 (2020-04-23) ------------------ * Simplified OAuth2 flow (#762) * Fix `Worksheet.delete_rows()` index error (#760 by @rafa-guillermo) * Deprecate `Worksheet.delete_row()` (#766) * Scope `Worksheet.find()` to a specific row or a column (#739 by @alfonsocv12) * Add `Worksheet.add_protected_range()` #447 (#720 by @KesterChan01) * Add ability to fetch cell address in A1 notation (#763 by @tivaliy) * Add `Worksheet.delete_columns()` (#761 by @rafa-guillermo) * Ignore numericising specific columns in `get_all_records` (#701 by @benjamindhimes) * Add option ``folder_id`` when creating a spreadsheet (#754 by @Abdellam1994) * Add `insertDataOption` to `Worksheet.append_row()` and `Worksheet.append_rows()` (#719 by @lobatt) 3.4.2 (2020-04-06) ------------------ * Fix Python 2 `SyntaxError` in models.py #751 (#752) 3.4.1 (2020-04-05) ------------------ * Fix `TypeError` when using gspread in google colab (#750) 3.4.0 (2020-04-05) ------------------ * Remove `oauth2client` in favor of `google-auth` #472, #529 (#637 by @BigHeadGeorge) * Convert `oauth2client` credentials to `google-auth` (#711 by @aiguofer) * Remove unnecessary `login()` from `gspread.authorize` * Fix sheet name quoting issue (#554, #636, #716): * Add quotes to worksheet title for get_all_values (#640 by @grlbrwrg, #717 by @zynaxsoft) * Escaping title containing single quotes with double quotes (#730 by @vijay-shanker) * Use `utils.absolute_range_name()` to handle range names (#748) * Fix `numericise()`: add underscores test to work in python2 and Maintainer-email: Alexandre Lavigne , alifeee Requires-Python: >=3.8 Description-Content-Type: text/markdown Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: End Users/Desktop Classifier: Intended Audience :: Science/Research Classifier: Topic :: Office/Business :: Financial :: Spreadsheet Classifier: Topic :: Software Development :: Libraries :: Python Modules Requires-Dist: google-auth>=1.12.0 Requires-Dist: google-auth-oauthlib>=0.4.1 Project-URL: Documentation, https://gspread.readthedocs.io/en/latest/ Project-URL: Source, https://github.com/burnash/gspread # Google Spreadsheets Python API v4 ![main workflow](https://img.shields.io/github/actions/workflow/status/burnash/gspread/main.yaml?logo=github) ![GitHub licence](https://img.shields.io/pypi/l/gspread?logo=github) ![GitHub downloads](https://img.shields.io/github/downloads-pre/burnash/gspread/latest/total?logo=github) ![documentation](https://img.shields.io/readthedocs/gspread?logo=readthedocs) ![PyPi download](https://img.shields.io/pypi/dm/gspread?logo=pypi) ![PyPi version](https://img.shields.io/pypi/v/gspread?logo=pypi) ![python version](https://img.shields.io/pypi/pyversions/gspread?style=pypi) Simple interface for working with Google Sheets. Features: - Open a spreadsheet by **title**, **key** or **URL**. - Read, write, and format cell ranges. - Sharing and access control. - Batching updates. ## Installation ```sh pip install gspread ``` Requirements: Python 3.8+. ## Basic Usage 1. [Create credentials in Google API Console](http://gspread.readthedocs.org/en/latest/oauth2.html) 2. Start using gspread ```python import gspread gc = gspread.service_account() # Open a sheet from a spreadsheet in one go wks = gc.open("Where is the money Lebowski?").sheet1 # Update a range of cells using the top left corner address wks.update([[1, 2], [3, 4]], "A1") # Or update a single cell wks.update_acell("B42", "it's down there somewhere, let me take another look.") # Format the header wks.format('A1:B1', {'textFormat': {'bold': True}}) ``` ## v5.12 to v6.0 Migration Guide ### Upgrade from Python 3.7 Python 3.7 is [end-of-life](https://devguide.python.org/versions/). gspread v6 requires a minimum of Python 3.8. ### Change `Worksheet.update` arguments The first two arguments (`values` & `range_name`) have swapped (to `range_name` & `values`). Either swap them (works in v6 only), or use named arguments (works in v5 & v6). As well, `values` can no longer be a list, and must be a 2D array. ```diff - file.sheet1.update([["new", "values"]]) + file.sheet1.update([["new", "values"]]) # unchanged - file.sheet1.update("B2:C2", [["54", "55"]]) + file.sheet1.update([["54", "55"]], "B2:C2") # or + file.sheet1.update(range_name="B2:C2", values=[["54", "55"]]) ``` ### More
See More Migration Guide ### Change colors from dictionary to text v6 uses hexadecimal color representation. Change all colors to hex. You can use the compatibility function `gspread.utils.convert_colors_to_hex_value()` to convert a dictionary to a hex string. ```diff - tab_color = {"red": 1, "green": 0.5, "blue": 1} + tab_color = "#FF7FFF" file.sheet1.update_tab_color(tab_color) ``` ### Switch lastUpdateTime from property to method ```diff - age = spreadsheet.lastUpdateTime + age = spreadsheet.get_lastUpdateTime() ``` ### Replace method `Worksheet.get_records` In v6 you can now only get *all* sheet records, using `Worksheet.get_all_records()`. The method `Worksheet.get_records()` has been removed. You can get some records using your own fetches and combine them with `gspread.utils.to_records()`. ```diff + from gspread import utils all_records = spreadsheet.get_all_records(head=1) - some_records = spreadsheet.get_all_records(head=1, first_index=6, last_index=9) - some_records = spreadsheet.get_records(head=1, first_index=6, last_index=9) + header = spreadsheet.get("1:1")[0] + cells = spreadsheet.get("6:9") + some_records = utils.to_records(header, cells) ``` ### Silence warnings In version 5 there are many warnings to mark deprecated feature/functions/methods. They can be silenced by setting the `GSPREAD_SILENCE_WARNINGS` environment variable to `1` ### Add more data to `gspread.Worksheet.__init__` ```diff gc = gspread.service_account(filename="google_credentials.json") spreadsheet = gc.open_by_key("{{key}}") properties = spreadsheet.fetch_sheet_metadata()["sheets"][0]["properties"] - worksheet = gspread.Worksheet(spreadsheet, properties) + worksheet = gspread.Worksheet(spreadsheet, properties, spreadsheet.id, gc.http_client) ```
## More Examples ### Opening a Spreadsheet ```python # You can open a spreadsheet by its title as it appears in Google Docs sh = gc.open('My poor gym results') # <-- Look ma, no keys! # If you want to be specific, use a key (which can be extracted from # the spreadsheet's url) sht1 = gc.open_by_key('0BmgG6nO_6dprdS1MN3d3MkdPa142WFRrdnRRUWl1UFE') # Or, if you feel really lazy to extract that key, paste the entire url sht2 = gc.open_by_url('https://docs.google.com/spreadsheet/ccc?key=0Bm...FE&hl') ``` ### Creating a Spreadsheet ```python sh = gc.create('A new spreadsheet') # But that new spreadsheet will be visible only to your script's account. # To be able to access newly created spreadsheet you *must* share it # with your email. Which brings us to… ``` ### Sharing a Spreadsheet ```python sh.share('otto@example.com', perm_type='user', role='writer') ``` ### Selecting a Worksheet ```python # Select worksheet by index. Worksheet indexes start from zero worksheet = sh.get_worksheet(0) # By title worksheet = sh.worksheet("January") # Most common case: Sheet1 worksheet = sh.sheet1 # Get a list of all worksheets worksheet_list = sh.worksheets() ``` ### Creating a Worksheet ```python worksheet = sh.add_worksheet(title="A worksheet", rows="100", cols="20") ``` ### Deleting a Worksheet ```python sh.del_worksheet(worksheet) ``` ### Getting a Cell Value ```python # With label val = worksheet.get('B1').first() # With coords val = worksheet.cell(1, 2).value ``` ### Getting All Values From a Row or a Column ```python # Get all values from the first row values_list = worksheet.row_values(1) # Get all values from the first column values_list = worksheet.col_values(1) ``` ### Getting All Values From a Worksheet as a List of Lists ```python from gspread.utils import GridRangeType list_of_lists = worksheet.get(return_type=GridRangeType.ListOfLists) ``` ### Getting a range of values Receive only the cells with a value in them. ```python >>> worksheet.get("A1:B4") [['A1', 'B1'], ['A2']] ``` Receive a rectangular array around the cells with values in them. ```python >>> worksheet.get("A1:B4", pad_values=True) [['A1', 'B1'], ['A2', '']] ``` Receive an array matching the request size regardless of if values are empty or not. ```python >>> worksheet.get("A1:B4", maintain_size=True) [['A1', 'B1'], ['A2', ''], ['', ''], ['', '']] ``` ### Finding a Cell ```python # Find a cell with exact string value cell = worksheet.find("Dough") print("Found something at R%sC%s" % (cell.row, cell.col)) # Find a cell matching a regular expression amount_re = re.compile(r'(Big|Enormous) dough') cell = worksheet.find(amount_re) ``` ### Finding All Matched Cells ```python # Find all cells with string value cell_list = worksheet.findall("Rug store") # Find all cells with regexp criteria_re = re.compile(r'(Small|Room-tiering) rug') cell_list = worksheet.findall(criteria_re) ``` ### Updating Cells ```python # Update a single cell worksheet.update_acell('B1', 'Bingo!') # Update a range worksheet.update([[1, 2], [3, 4]], 'A1:B2') # Update multiple ranges at once worksheet.batch_update([{ 'range': 'A1:B2', 'values': [['A1', 'B1'], ['A2', 'B2']], }, { 'range': 'J42:K43', 'values': [[1, 2], [3, 4]], }]) ``` ### Get unformatted cell value or formula ```python from gspread.utils import ValueRenderOption # Get formatted cell value as displayed in the UI >>> worksheet.get("A1:B2") [['$12.00']] # Get unformatted value from the same cell range >>> worksheet.get("A1:B2", value_render_option=ValueRenderOption.unformatted) [[12]] # Get formula from a cell >>> worksheet.get("C2:D2", value_render_option=ValueRenderOption.formula) [['=1/1024']] ### Add data validation to a range ```python import gspread from gspread.utils import ValidationConditionType # Restrict the input to greater than 10 in a single cell worksheet.add_validation( 'A1', ValidationConditionType.number_greater, [10], strict=True, inputMessage='Value must be greater than 10', ) # Restrict the input to Yes/No for a specific range with dropdown worksheet.add_validation( 'C2:C7', ValidationConditionType.one_of_list, ['Yes', 'No',] showCustomUi=True ) ``` ## Documentation [Documentation]\: [https://gspread.readthedocs.io/][Documentation] [Documentation]: https://gspread.readthedocs.io/en/latest/ ### Ask Questions The best way to get an answer to a question is to ask on [Stack Overflow with a gspread tag](http://stackoverflow.com/questions/tagged/gspread?sort=votes&pageSize=50). ## Contributors [List of contributors](https://github.com/burnash/gspread/graphs/contributors) ## How to Contribute Please make sure to take a moment and read the [Code of Conduct](https://github.com/burnash/gspread/blob/master/.github/CODE_OF_CONDUCT.md). ### Report Issues Please report bugs and suggest features via the [GitHub Issues](https://github.com/burnash/gspread/issues). Before opening an issue, search the tracker for possible duplicates. If you find a duplicate, please add a comment saying that you encountered the problem as well. ### Improve Documentation [Documentation](https://gspread.readthedocs.io/) is as important as code. If you know how to make it more consistent, readable and clear, please submit a pull request. The documentation files are in [`docs`](https://github.com/burnash/gspread/tree/master/docs) folder, use [reStructuredText](http://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html#rst-index) markup and rendered by [Sphinx](http://www.sphinx-doc.org/). ### Contribute code Please make sure to read the [Contributing Guide](https://github.com/burnash/gspread/blob/master/.github/CONTRIBUTING.md) before making a pull request. python-gspread-6.1.4/README.md000066400000000000000000000230631472155066000157570ustar00rootroot00000000000000# Google Spreadsheets Python API v4 ![main workflow](https://img.shields.io/github/actions/workflow/status/burnash/gspread/main.yaml?logo=github) ![GitHub licence](https://img.shields.io/pypi/l/gspread?logo=github) ![GitHub downloads](https://img.shields.io/github/downloads-pre/burnash/gspread/latest/total?logo=github) ![documentation](https://img.shields.io/readthedocs/gspread?logo=readthedocs) ![PyPi download](https://img.shields.io/pypi/dm/gspread?logo=pypi) ![PyPi version](https://img.shields.io/pypi/v/gspread?logo=pypi) ![python version](https://img.shields.io/pypi/pyversions/gspread?style=pypi) Simple interface for working with Google Sheets. Features: - Open a spreadsheet by **title**, **key** or **URL**. - Read, write, and format cell ranges. - Sharing and access control. - Batching updates. ## Installation ```sh pip install gspread ``` Requirements: Python 3.8+. ## Basic Usage 1. [Create credentials in Google API Console](http://gspread.readthedocs.org/en/latest/oauth2.html) 2. Start using gspread ```python import gspread gc = gspread.service_account() # Open a sheet from a spreadsheet in one go wks = gc.open("Where is the money Lebowski?").sheet1 # Update a range of cells using the top left corner address wks.update([[1, 2], [3, 4]], "A1") # Or update a single cell wks.update_acell("B42", "it's down there somewhere, let me take another look.") # Format the header wks.format('A1:B1', {'textFormat': {'bold': True}}) ``` ## v5.12 to v6.0 Migration Guide ### Upgrade from Python 3.7 Python 3.7 is [end-of-life](https://devguide.python.org/versions/). gspread v6 requires a minimum of Python 3.8. ### Change `Worksheet.update` arguments The first two arguments (`values` & `range_name`) have swapped (to `range_name` & `values`). Either swap them (works in v6 only), or use named arguments (works in v5 & v6). As well, `values` can no longer be a list, and must be a 2D array. ```diff - file.sheet1.update([["new", "values"]]) + file.sheet1.update([["new", "values"]]) # unchanged - file.sheet1.update("B2:C2", [["54", "55"]]) + file.sheet1.update([["54", "55"]], "B2:C2") # or + file.sheet1.update(range_name="B2:C2", values=[["54", "55"]]) ``` ### More
See More Migration Guide ### Change colors from dictionary to text v6 uses hexadecimal color representation. Change all colors to hex. You can use the compatibility function `gspread.utils.convert_colors_to_hex_value()` to convert a dictionary to a hex string. ```diff - tab_color = {"red": 1, "green": 0.5, "blue": 1} + tab_color = "#FF7FFF" file.sheet1.update_tab_color(tab_color) ``` ### Switch lastUpdateTime from property to method ```diff - age = spreadsheet.lastUpdateTime + age = spreadsheet.get_lastUpdateTime() ``` ### Replace method `Worksheet.get_records` In v6 you can now only get *all* sheet records, using `Worksheet.get_all_records()`. The method `Worksheet.get_records()` has been removed. You can get some records using your own fetches and combine them with `gspread.utils.to_records()`. ```diff + from gspread import utils all_records = spreadsheet.get_all_records(head=1) - some_records = spreadsheet.get_all_records(head=1, first_index=6, last_index=9) - some_records = spreadsheet.get_records(head=1, first_index=6, last_index=9) + header = spreadsheet.get("1:1")[0] + cells = spreadsheet.get("6:9") + some_records = utils.to_records(header, cells) ``` ### Silence warnings In version 5 there are many warnings to mark deprecated feature/functions/methods. They can be silenced by setting the `GSPREAD_SILENCE_WARNINGS` environment variable to `1` ### Add more data to `gspread.Worksheet.__init__` ```diff gc = gspread.service_account(filename="google_credentials.json") spreadsheet = gc.open_by_key("{{key}}") properties = spreadsheet.fetch_sheet_metadata()["sheets"][0]["properties"] - worksheet = gspread.Worksheet(spreadsheet, properties) + worksheet = gspread.Worksheet(spreadsheet, properties, spreadsheet.id, gc.http_client) ```
## More Examples ### Opening a Spreadsheet ```python # You can open a spreadsheet by its title as it appears in Google Docs sh = gc.open('My poor gym results') # <-- Look ma, no keys! # If you want to be specific, use a key (which can be extracted from # the spreadsheet's url) sht1 = gc.open_by_key('0BmgG6nO_6dprdS1MN3d3MkdPa142WFRrdnRRUWl1UFE') # Or, if you feel really lazy to extract that key, paste the entire url sht2 = gc.open_by_url('https://docs.google.com/spreadsheet/ccc?key=0Bm...FE&hl') ``` ### Creating a Spreadsheet ```python sh = gc.create('A new spreadsheet') # But that new spreadsheet will be visible only to your script's account. # To be able to access newly created spreadsheet you *must* share it # with your email. Which brings us to… ``` ### Sharing a Spreadsheet ```python sh.share('otto@example.com', perm_type='user', role='writer') ``` ### Selecting a Worksheet ```python # Select worksheet by index. Worksheet indexes start from zero worksheet = sh.get_worksheet(0) # By title worksheet = sh.worksheet("January") # Most common case: Sheet1 worksheet = sh.sheet1 # Get a list of all worksheets worksheet_list = sh.worksheets() ``` ### Creating a Worksheet ```python worksheet = sh.add_worksheet(title="A worksheet", rows="100", cols="20") ``` ### Deleting a Worksheet ```python sh.del_worksheet(worksheet) ``` ### Getting a Cell Value ```python # With label val = worksheet.get('B1').first() # With coords val = worksheet.cell(1, 2).value ``` ### Getting All Values From a Row or a Column ```python # Get all values from the first row values_list = worksheet.row_values(1) # Get all values from the first column values_list = worksheet.col_values(1) ``` ### Getting All Values From a Worksheet as a List of Lists ```python from gspread.utils import GridRangeType list_of_lists = worksheet.get(return_type=GridRangeType.ListOfLists) ``` ### Getting a range of values Receive only the cells with a value in them. ```python >>> worksheet.get("A1:B4") [['A1', 'B1'], ['A2']] ``` Receive a rectangular array around the cells with values in them. ```python >>> worksheet.get("A1:B4", pad_values=True) [['A1', 'B1'], ['A2', '']] ``` Receive an array matching the request size regardless of if values are empty or not. ```python >>> worksheet.get("A1:B4", maintain_size=True) [['A1', 'B1'], ['A2', ''], ['', ''], ['', '']] ``` ### Finding a Cell ```python # Find a cell with exact string value cell = worksheet.find("Dough") print("Found something at R%sC%s" % (cell.row, cell.col)) # Find a cell matching a regular expression amount_re = re.compile(r'(Big|Enormous) dough') cell = worksheet.find(amount_re) ``` ### Finding All Matched Cells ```python # Find all cells with string value cell_list = worksheet.findall("Rug store") # Find all cells with regexp criteria_re = re.compile(r'(Small|Room-tiering) rug') cell_list = worksheet.findall(criteria_re) ``` ### Updating Cells ```python # Update a single cell worksheet.update_acell('B1', 'Bingo!') # Update a range worksheet.update([[1, 2], [3, 4]], 'A1:B2') # Update multiple ranges at once worksheet.batch_update([{ 'range': 'A1:B2', 'values': [['A1', 'B1'], ['A2', 'B2']], }, { 'range': 'J42:K43', 'values': [[1, 2], [3, 4]], }]) ``` ### Get unformatted cell value or formula ```python from gspread.utils import ValueRenderOption # Get formatted cell value as displayed in the UI >>> worksheet.get("A1:B2") [['$12.00']] # Get unformatted value from the same cell range >>> worksheet.get("A1:B2", value_render_option=ValueRenderOption.unformatted) [[12]] # Get formula from a cell >>> worksheet.get("C2:D2", value_render_option=ValueRenderOption.formula) [['=1/1024']] ### Add data validation to a range ```python import gspread from gspread.utils import ValidationConditionType # Restrict the input to greater than 10 in a single cell worksheet.add_validation( 'A1', ValidationConditionType.number_greater, [10], strict=True, inputMessage='Value must be greater than 10', ) # Restrict the input to Yes/No for a specific range with dropdown worksheet.add_validation( 'C2:C7', ValidationConditionType.one_of_list, ['Yes', 'No',] showCustomUi=True ) ``` ## Documentation [Documentation]\: [https://gspread.readthedocs.io/][Documentation] [Documentation]: https://gspread.readthedocs.io/en/latest/ ### Ask Questions The best way to get an answer to a question is to ask on [Stack Overflow with a gspread tag](http://stackoverflow.com/questions/tagged/gspread?sort=votes&pageSize=50). ## Contributors [List of contributors](https://github.com/burnash/gspread/graphs/contributors) ## How to Contribute Please make sure to take a moment and read the [Code of Conduct](https://github.com/burnash/gspread/blob/master/.github/CODE_OF_CONDUCT.md). ### Report Issues Please report bugs and suggest features via the [GitHub Issues](https://github.com/burnash/gspread/issues). Before opening an issue, search the tracker for possible duplicates. If you find a duplicate, please add a comment saying that you encountered the problem as well. ### Improve Documentation [Documentation](https://gspread.readthedocs.io/) is as important as code. If you know how to make it more consistent, readable and clear, please submit a pull request. The documentation files are in [`docs`](https://github.com/burnash/gspread/tree/master/docs) folder, use [reStructuredText](http://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html#rst-index) markup and rendered by [Sphinx](http://www.sphinx-doc.org/). ### Contribute code Please make sure to read the [Contributing Guide](https://github.com/burnash/gspread/blob/master/.github/CONTRIBUTING.md) before making a pull request. python-gspread-6.1.4/docs/000077500000000000000000000000001472155066000154245ustar00rootroot00000000000000python-gspread-6.1.4/docs/_templates/000077500000000000000000000000001472155066000175615ustar00rootroot00000000000000python-gspread-6.1.4/docs/_templates/layout.html000066400000000000000000000024251472155066000217670ustar00rootroot00000000000000{% extends "!layout.html" %} {% block htmltitle %} {{ super() }} {% endblock %} {% block extrabody %} {% endblock %} {% block footer %} {{ super() }} {% endblock %} python-gspread-6.1.4/docs/advanced.rst000066400000000000000000000044061472155066000177270ustar00rootroot00000000000000Advanced Usage ============== Custom Authentication --------------------- Google Colaboratory ~~~~~~~~~~~~~~~~~~~ If you familiar with the Jupyter Notebook, `Google Colaboratory `_ is probably the easiest way to get started using gspread:: from google.colab import auth auth.authenticate_user() import gspread from google.auth import default creds, _ = default() gc = gspread.authorize(creds) See the full example in the `External data: Local Files, Drive, Sheets, and Cloud Storage `_ notebook. Using Authlib ~~~~~~~~~~~~~ Using ``Authlib`` instead of ``google-auth``. Similar to `google.auth.transport.requests.AuthorizedSession `_ Authlib's ``AssertionSession`` can automatically refresh tokens.:: import json from gspread import Client from authlib.integrations.requests_client import AssertionSession def create_assertion_session(conf_file, scopes, subject=None): with open(conf_file, 'r') as f: conf = json.load(f) token_url = conf['token_uri'] issuer = conf['client_email'] key = conf['private_key'] key_id = conf.get('private_key_id') header = {'alg': 'RS256'} if key_id: header['kid'] = key_id # Google puts scope in payload claims = {'scope': ' '.join(scopes)} return AssertionSession( grant_type=AssertionSession.JWT_BEARER_GRANT_TYPE, token_url=token_url, issuer=issuer, audience=token_url, claims=claims, subject=subject, key=key, header=header, ) scopes = [ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive', ] session = create_assertion_session('your-google-conf.json', scopes) gc = Client(None, session) wks = gc.open("Where is the money Lebowski?").sheet1 wks.update_acell('B2', "it's down there somewhere, let me take another look.") # Fetch a cell range cell_list = wks.range('A1:B7') python-gspread-6.1.4/docs/api/000077500000000000000000000000001472155066000161755ustar00rootroot00000000000000python-gspread-6.1.4/docs/api/auth.rst000066400000000000000000000000651472155066000176710ustar00rootroot00000000000000Auth ==== .. automodule:: gspread.auth :members: python-gspread-6.1.4/docs/api/client.rst000066400000000000000000000000721472155066000202040ustar00rootroot00000000000000Client ====== .. autoclass:: gspread.Client :members: python-gspread-6.1.4/docs/api/exceptions.rst000066400000000000000000000007301472155066000211100ustar00rootroot00000000000000Exceptions ========== .. autoexception:: gspread.exceptions.APIError .. autoexception:: gspread.exceptions.GSpreadException .. autoexception:: gspread.exceptions.IncorrectCellLabel .. autoexception:: gspread.exceptions.InvalidInputValue .. autoexception:: gspread.exceptions.NoValidUrlKeyFound .. autoexception:: gspread.exceptions.SpreadsheetNotFound .. autoexception:: gspread.exceptions.UnSupportedExportFormat .. autoexception:: gspread.exceptions.WorksheetNotFound python-gspread-6.1.4/docs/api/http_client.rst000066400000000000000000000004021472155066000212400ustar00rootroot00000000000000HTTP Client =========== .. note:: This class is not intended to be used directly. It is used by all gspread models to interact with the Google API .. autoclass:: gspread.HTTPClient :members: .. autoclass:: gspread.BackOffHTTPClient :members: python-gspread-6.1.4/docs/api/index.rst000066400000000000000000000002201472155066000200300ustar00rootroot00000000000000API Reference ============= .. toctree:: :maxdepth: 2 top-level auth client http_client models/index utils exceptions python-gspread-6.1.4/docs/api/models/000077500000000000000000000000001472155066000174605ustar00rootroot00000000000000python-gspread-6.1.4/docs/api/models/cell.rst000066400000000000000000000000711472155066000211270ustar00rootroot00000000000000Cell ==== .. autoclass:: gspread.cell.Cell :members: python-gspread-6.1.4/docs/api/models/index.rst000066400000000000000000000006601472155066000213230ustar00rootroot00000000000000Models ====== The models represent common spreadsheet entities: :class:`a spreadsheet `, :class:`a worksheet ` and :class:`a cell `. .. note:: The classes described below should not be instantiated by the end-user. Their instances result from calling other objects' methods. .. toctree:: :maxdepth: 2 spreadsheet worksheet cell python-gspread-6.1.4/docs/api/models/spreadsheet.rst000066400000000000000000000001251472155066000225170ustar00rootroot00000000000000Spreadsheet =========== .. autoclass:: gspread.spreadsheet.Spreadsheet :members: python-gspread-6.1.4/docs/api/models/worksheet.rst000066400000000000000000000002631472155066000222260ustar00rootroot00000000000000Worksheet ========= ValueRange ---------- .. autoclass:: gspread.worksheet.ValueRange :members: Worksheet --------- .. autoclass:: gspread.worksheet.Worksheet :members: python-gspread-6.1.4/docs/api/top-level.rst000066400000000000000000000002001472155066000206260ustar00rootroot00000000000000Top level ========= .. module:: gspread .. autofunction:: oauth .. autofunction:: service_account .. autofunction:: authorize python-gspread-6.1.4/docs/api/utils.rst000066400000000000000000000001131472155066000200620ustar00rootroot00000000000000Utils ===== .. automodule:: gspread.utils :members: :undoc-members: python-gspread-6.1.4/docs/conf.py000066400000000000000000000225031472155066000167250ustar00rootroot00000000000000# # gspread documentation build configuration file, created by # sphinx-quickstart on Thu Dec 15 14:44:32 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys from datetime import date # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) from gspread import __version__ # noqa: E402 # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.doctest", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.ifconfig", "sphinx.ext.intersphinx", "sphinx_toolbox.more_autodoc.autonamedtuple", "sphinx.ext.autodoc", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "gspread" copyright = "%s, Anton Burnashev" % date.today().year # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "gspreaddoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ("index", "gspread.tex", "gspread Documentation", "Anton Burnashev", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [("index", "gspread", "gspread Documentation", ["Anton Burnashev"], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "gspread", "gspread Documentation", "Anton Burnashev", "gspread", "Google Spreadsheets Python API.", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. epub_title = "gspread" epub_author = "Anton Burnashev" epub_publisher = "Anton Burnashev" epub_copyright = "%s, Anton Burnashev" % date.today().year # The language of the text. It defaults to the language option # or en if the language is not set. # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. # epub_identifier = '' # A unique identification for the text. # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # epub_cover = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_pre_files = [] # HTML files that should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_post_files = [] # A list of files that should not be packed into the epub file. # epub_exclude_files = [] # The depth of the table of contents in toc.ncx. # epub_tocdepth = 3 # Allow duplicate toc entries. # epub_tocdup = True # Intersphinx configuration intersphinx_mapping = { "python": ("https://docs.python.org/3.6", (None, "python-inv.txt")), } python-gspread-6.1.4/docs/index.rst000066400000000000000000000044021472155066000172650ustar00rootroot00000000000000gspread ======= `gspread`_ is a Python API for Google Sheets. Features: - Google Sheets API v4. - Open a spreadsheet by title, key or url. - Read, write, and format cell ranges. - Sharing and access control. - Batching updates. Installation ------------ .. code:: sh pip install gspread Requirements: Python 3+. Quick Example ------------- .. code:: python import gspread gc = gspread.service_account() # Open a sheet from a spreadsheet in one go wks = gc.open("Where is the money Lebowski?").sheet1 # Update a range of cells using the top left corner address wks.update([[1, 2], [3, 4]], 'A1') # Or update a single cell wks.update_acell('B42', "it's down there somewhere, let me take another look.") # Format the header wks.format('A1:B1', {'textFormat': {'bold': True}}) Getting Started --------------- .. toctree:: :maxdepth: 2 oauth2 Usage ----- .. toctree:: :maxdepth: 2 user-guide Advanced -------- .. toctree:: :maxdepth: 2 advanced API Documentation --------------------------- .. toctree:: :maxdepth: 2 api/index How to Contribute ----------------- Please make sure to take a moment and read the `Code of Conduct`_. Ask Questions ~~~~~~~~~~~~~ The best way to get an answer to a question is to ask on `Stack Overflow with a gspread tag`_. Report Issues ~~~~~~~~~~~~~ Please report bugs and suggest features via the `GitHub Issues`_. Before opening an issue, search the tracker for possible duplicates. If you find a duplicate, please add a comment saying that you encountered the problem as well. Contribute code ~~~~~~~~~~~~~~~ Please make sure to read the `Contributing Guide`_ before making a pull request. Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` .. _gspread: https://github.com/burnash/gspread .. _Obtain OAuth2 credentials from Google Developers Console: oauth2.html .. _Code of Conduct: https://github.com/burnash/gspread/blob/master/.github/CODE_OF_CONDUCT.md .. _Stack Overflow with a gspread tag: http://stackoverflow.com/questions/tagged/gspread?sort=votes&pageSize=50 .. _GitHub Issues: https://github.com/burnash/gspread/issues .. _Contributing Guide: https://github.com/burnash/gspread/blob/master/.github/CONTRIBUTING.md python-gspread-6.1.4/docs/oauth2.rst000066400000000000000000000257041472155066000173700ustar00rootroot00000000000000Authentication ============== To access spreadsheets via Google Sheets API you need to authenticate and authorize your application. * If you plan to access spreadsheets on behalf of a bot account use :ref:`Service Account `. * If you'd like to access spreadsheets on behalf of end users (including yourself) use :ref:`OAuth Client ID `. * If you'd like to **only** open public spreadsheets use :ref:`API key ` .. _enable-api-access: Enable API Access for a Project ------------------------------- 1. Head to `Google Developers Console `_ and create a new project (or select the one you already have). 2. In the box labeled "Search for APIs and Services", search for "Google Drive API" and enable it. 3. In the box labeled "Search for APIs and Services", search for "Google Sheets API" and enable it. .. _service-account: For Bots: Using Service Account ------------------------------- A service account is a special type of Google account intended to represent a non-human user that needs to authenticate and be authorized to access data in Google APIs [sic]. Since it's a separate account, by default it does not have access to any spreadsheet until you share it with this account. Just like any other Google account. Here's how to get one: 1. :ref:`enable-api-access` if you haven't done it yet. 2. Go to "APIs & Services > Credentials" and choose "Create credentials > Service account key". 3. Fill out the form 4. Click "Create" and "Done". 5. Press "Manage service accounts" above Service Accounts. 6. Press on **⋮** near recently created service account and select "Manage keys" and then click on "ADD KEY > Create new key". 7. Select JSON key type and press "Create". You will automatically download a JSON file with credentials. It may look like this: :: { "type": "service_account", "project_id": "api-project-XXX", "private_key_id": "2cd … ba4", "private_key": "-----BEGIN PRIVATE KEY-----\nNrDyLw … jINQh/9\n-----END PRIVATE KEY-----\n", "client_email": "473000000000-yoursisdifferent@developer.gserviceaccount.com", "client_id": "473 … hd.apps.googleusercontent.com", ... } Remember the path to the downloaded credentials file. Also, in the next step you'll need the value of *client_email* from this file. 6. Very important! Go to your spreadsheet and share it with a *client_email* from the step above. Just like you do with any other Google account. If you don't do this, you'll get a ``gspread.exceptions.SpreadsheetNotFound`` exception when trying to access this spreadsheet from your application or a script. 7. Move the downloaded file to ``~/.config/gspread/service_account.json``. Windows users should put this file to ``%APPDATA%\gspread\service_account.json``. 8. Create a new Python file with this code: :: import gspread gc = gspread.service_account() sh = gc.open("Example spreadsheet") print(sh.sheet1.get('A1')) Ta-da! .. NOTE:: If you want to store the credentials file somewhere else, specify the path to `service_account.json` in :meth:`~gspread.service_account`: :: gc = gspread.service_account(filename='path/to/the/downloaded/file.json') Make sure you store the credentials file in a safe place. For the curious, under the hood :meth:`~gspread.service_account` loads your credentials and authorizes gspread. Similarly to the code that has been used for authentication prior to the gspread version 3.6: :: from google.oauth2.service_account import Credentials scopes = [ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive' ] credentials = Credentials.from_service_account_file( 'path/to/the/downloaded/file.json', scopes=scopes ) gc = gspread.authorize(credentials) There is also the option to pass credentials as a dictionary: :: import gspread credentials = { "type": "service_account", "project_id": "api-project-XXX", "private_key_id": "2cd … ba4", "private_key": "-----BEGIN PRIVATE KEY-----\nNrDyLw … jINQh/9\n-----END PRIVATE KEY-----\n", "client_email": "473000000000-yoursisdifferent@developer.gserviceaccount.com", "client_id": "473 … hd.apps.googleusercontent.com", ... } gc = gspread.service_account_from_dict(credentials) sh = gc.open("Example spreadsheet") print(sh.sheet1.get('A1')) .. NOTE:: Older versions of gspread have used `oauth2client `_. Google has `deprecated `_ it in favor of `google-auth`. If you're still using `oauth2client` credentials, the library will convert these to `google-auth` for you, but you can change your code to use the new credentials to make sure nothing breaks in the future. .. _oauth-client-id: For End Users: Using OAuth Client ID ------------------------------------ This is the case where your application or a script is accessing spreadsheets on behalf of an end user. When you use this scenario, your application or a script will ask the end user (or yourself if you're running it) to grant access to the user's data. 1. :ref:`enable-api-access` if you haven't done it yet. #. Go to "APIs & Services > OAuth Consent Screen." Click the button for "Configure Consent Screen". a. In the "1 OAuth consent screen" tab, give your app a name and fill the "User support email" and "Developer contact information". Click "SAVE AND CONTINUE". #. There is no need to fill in anything in the tab "2 Scopes", just click "SAVE AND CONTINUE". #. In the tab "3 Test users", add the Google account email of the end user, typically your own Google email. Click "SAVE AND CONTINUE". #. Double check the "4 Summary" presented and click "BACK TO DASHBOARD". 3. Go to "APIs & Services > Credentials" #. Click "+ Create credentials" at the top, then select "OAuth client ID". #. Select "Desktop app", name the credentials and click "Create". Click "Ok" in the "OAuth client created" popup. #. Download the credentials by clicking the Download JSON button in "OAuth 2.0 Client IDs" section. #. Move the downloaded file to ``~/.config/gspread/credentials.json``. Windows users should put this file to ``%APPDATA%\gspread\credentials.json``. Create a new Python file with this code: :: import gspread gc = gspread.oauth() sh = gc.open("Example spreadsheet") print(sh.sheet1.get('A1')) When you run this code, it launches a browser asking you for authentication. Follow the instruction on the web page. Once finished, gspread stores authorized credentials in the config directory next to `credentials.json`. You only need to do authorization in the browser once, following runs will reuse stored credentials. .. NOTE:: If you want to store the credentials file somewhere else, specify the path to `credentials.json` and `authorized_user.json` in :meth:`~gspread.oauth`: :: gc = gspread.oauth( credentials_filename='path/to/the/credentials.json', authorized_user_filename='path/to/the/authorized_user.json' ) Make sure you store the credentials file in a safe place. There is also the option to pass your credentials directly as a python dict. This way you don't have to store them as files or you can store them in your favorite password manager. :: import gspread credentials = { "installed": { "client_id": "12345678901234567890abcdefghijklmn.apps.googleusercontent.com", "project_id": "my-project1234", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", ... } } gc, authorized_user = gspread.oauth_from_dict(credentials) sh = gc.open("Example spreadsheet") print(sh.sheet1.get('A1')) Once authenticated you must store the returned json string containing your authenticated user information. Provide that details as a python dict as second argument in your next `oauth` request to be directly authenticated and skip the flow. .. NOTE:: The second time if your authorized user has not expired, you can omit the credentials. Be aware, if the authorized user has expired your credentials are required to authenticate again. :: import gspread credentials = { "installed": { "client_id": "12345678901234567890abcdefghijklmn.apps.googleusercontent.com", "project_id": "my-project1234", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", ... } } authorized_user = { "refresh_token": "8//ThisALONGTOkEn....", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "12345678901234567890abcdefghijklmn.apps.googleusercontent.com", "client_secret": "MySecRet....", "scopes": [ "https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/drive" ], "expiry": "1070-01-01T00:00:00.000001Z" } gc, authorized_user = gspread.oauth_from_dict(credentials, authorized_user) sh = gc.open("Example spreadsheet") print(sh.sheet1.get('A1')) .. warning:: Security credentials file and authorized credentials contain sensitive data. **Do not share these files with others** and treat them like private keys. If you are concerned about giving the application access to your spreadsheets and Drive, use Service Accounts. .. NOTE:: The user interface of Google Developers Console may be different when you're reading this. If you find that this document is out of sync with the actual UI, please update it. Improvements to the documentation are always welcome. Click **Edit on GitHub** in the top right corner of the page, make it better and submit a PR. .. _api-key: For public spreadsheets only ---------------------------- An API key is a token that allows an application to open public spreadsheet files. Here's how to get one: 1. :ref:`enable-api-access` if you haven't done it yet. 2. Go to "APIs & Services > Credentials" and choose "Create credentials > API key" 3. A pop-up should display your newly created key. 4. Copy the key. 5. That's it your key is created. .. note:: You can access your key any time later, come back to the "APIs & Services > Credentials" page, you'll be able to see your key again. 6. Create a new Python file with this code: :: import gspread gc = gspread.api_key(""") sh = gc.open_by_key("1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms") print(sh.sheet1.get('A1')) Ta-da ! .. note:: You can only open public keys, this means you can only open spreadsheet files using the methods: ``gc.open_by_key`` and ``gc.open_by_url``. The method ``gc.open()`` searches your private files to find the one with a matching name so it will never work. python-gspread-6.1.4/docs/requirements.txt000066400000000000000000000000561472155066000207110ustar00rootroot00000000000000sphinx==6.2.1 sphinx_rtd_theme sphinx-toolbox python-gspread-6.1.4/docs/user-guide.rst000066400000000000000000000256271472155066000202430ustar00rootroot00000000000000Examples of gspread Usage ========================= If you haven't yet authorized your app, read :doc:`oauth2` first. Opening a Spreadsheet ~~~~~~~~~~~~~~~~~~~~~ You can open a spreadsheet by its title as it appears in Google Docs: .. code:: python sh = gc.open('My poor gym results') .. NOTE:: If you have multiple Google Sheets with the same title, only the latest sheet will be opened by this method without throwing an error. It's recommended to open the sheet using its unique ID instead (see below) If you want to be specific, use a key (which can be extracted from the spreadsheet's url): .. code:: python sht1 = gc.open_by_key('0BmgG6nO_6dprdS1MN3d3MkdPa142WFRrdnRRUWl1UFE') Or, if you feel really lazy to extract that key, paste the entire spreadsheet's url .. code:: python sht2 = gc.open_by_url('https://docs.google.com/spreadsheet/ccc?key=0Bm...FE&hl') Creating a Spreadsheet ~~~~~~~~~~~~~~~~~~~~~~ Use :meth:`~gspread.Client.create` to create a new blank spreadsheet: .. code:: python sh = gc.create('A new spreadsheet') .. NOTE:: If you're using a :ref:`service account `, this new spreadsheet will be visible only to this account. To be able to access newly created spreadsheet from Google Sheets with your own Google account you *must* share it with your email. See how to share a spreadsheet in the section below. Sharing a Spreadsheet ~~~~~~~~~~~~~~~~~~~~~ If your email is *otto@example.com* you can share the newly created spreadsheet with yourself: .. code:: python sh.share('otto@example.com', perm_type='user', role='writer') See :meth:`~gspread.models.Spreadsheet.share` documentation for a full list of accepted parameters. Selecting a Worksheet ~~~~~~~~~~~~~~~~~~~~~ Select worksheet by index. Worksheet indexes start from zero: .. code:: python worksheet = sh.get_worksheet(0) Or by title: .. code:: python worksheet = sh.worksheet("January") Or the most common case: *Sheet1*: .. code:: python worksheet = sh.sheet1 To get a list of all worksheets: .. code:: python worksheet_list = sh.worksheets() Creating a Worksheet ~~~~~~~~~~~~~~~~~~~~ .. code:: python worksheet = sh.add_worksheet(title="A worksheet", rows=100, cols=20) Deleting a Worksheet ~~~~~~~~~~~~~~~~~~~~ .. code:: python sh.del_worksheet(worksheet) Updating a Worksheet's name and color ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: python worksheet.update_title("December Transactions") worksheet.update_tab_color({"red": 1, "green": 0.5, "blue": 0.5}) Getting a Cell Value ~~~~~~~~~~~~~~~~~~~~ Using `A1 notation `_: .. code:: python val = worksheet.acell('B1').value Or row and column coordinates: .. code:: python val = worksheet.cell(1, 2).value If you want to get a cell formula: .. code:: python cell = worksheet.acell('B1', value_render_option='FORMULA').value # or cell = worksheet.cell(1, 2, value_render_option='FORMULA').value Getting Unformatted Cell Value ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get the Unformatted value from a cell. Example: cells formatted as currency will display with the selected currency but they actual value is regular number. Get the formatted (as displayed) value: .. code:: python worksheet.get("A1:B2") Results in: ``[['$12.00']]`` Get the unformatted value: .. code:: python from gspread.utils import ValueRenderOption worksheet.get("A1:B2", value_render_option=ValueRenderOption.unformatted) Results in: ``[[12]]`` Getting Cell formula ~~~~~~~~~~~~~~~~~~~~ Get the formula from a cell instead of the resulting value: .. code:: python from gspread.utils import ValueRenderOption worksheet.get("G6", value_render_option=ValueRenderOption.formula) Resulsts in: ``[['=1/1024']]`` Getting All Values From a Row or a Column ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Get all values from the first row: .. code:: python values_list = worksheet.row_values(1) Get all values from the first column: .. code:: python values_list = worksheet.col_values(1) .. NOTE:: So far we've been fetching a limited amount of data from a sheet. This works great until you need to get values from hundreds of cells or iterating over many rows or columns. Under the hood, gspread uses `Google Sheets API v4 `_. Most of the time when you call a gspread method to fetch or update a sheet gspread produces one HTTP API call. HTTP calls have performance costs. So if you find your app fetching values one by one in a loop or iterating over rows or columns you can improve the performance of the app by fetching data in one go. What's more, Sheets API v4 introduced `Usage Limits `_ (as of this writing, 300 requests per 60 seconds per project, and 60 requests per 60 seconds per user). When your application hits that limit, you get an :exc:`~gspread.exceptions.APIError` `429 RESOURCE_EXHAUSTED`. Here are the methods that may help you to reduce API calls: * :meth:`~gspread.models.Worksheet.get_all_values` fetches values from all of the cells of the sheet. * :meth:`~gspread.models.Worksheet.get` fetches all values from a range of cells. * :meth:`~gspread.models.Worksheet.batch_get` can fetch values from multiple ranges of cells with one API call. * :meth:`~gspread.models.Worksheet.update` lets you update a range of cells with a list of lists. * :meth:`~gspread.models.Worksheet.batch_update` lets you update multiple ranges of cells with one API call. Getting All Values From a Worksheet as a List of Lists ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: python list_of_lists = worksheet.get_all_values() Getting All Values From a Worksheet as a List of Dictionaries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: python list_of_dicts = worksheet.get_all_records() Finding a Cell ~~~~~~~~~~~~~~ Find a cell matching a string: .. code:: python cell = worksheet.find("Dough") print("Found something at R%sC%s" % (cell.row, cell.col)) Find a cell matching a regular expression .. code:: python amount_re = re.compile(r'(Big|Enormous) dough') cell = worksheet.find(amount_re) `find` returns `None` if value is not Found Finding All Matched Cells ~~~~~~~~~~~~~~~~~~~~~~~~~ Find all cells matching a string: .. code:: python cell_list = worksheet.findall("Rug store") Find all cells matching a regexp: .. code:: python criteria_re = re.compile(r'(Small|Room-tiering) rug') cell_list = worksheet.findall(criteria_re) Clear A Worksheet ~~~~~~~~~~~~~~~~~ Clear one or multiple cells ranges at once: .. code:: python worksheet.batch_clear(["A1:B1", "C2:E2", "my_named_range"]) Clear the entire worksheet: .. code:: python worksheet.clear() Cell Object ~~~~~~~~~~~ Each cell has a value and coordinates properties: .. code:: python value = cell.value row_number = cell.row column_number = cell.col Updating Cells ~~~~~~~~~~~~~~ Using `A1 notation `_: .. code:: python worksheet.update_acell('B1', 'Bingo!') Or row and column coordinates: .. code:: python worksheet.update_cell(1, 2, 'Bingo!') Update a range .. code:: python worksheet.update([[1, 2], [3, 4]], 'A1:B2') Adding Data Validation ~~~~~~~~~~~~~~~~~~~~~~ You can add a strict validation to a cell. .. code:: python ws.add_validation( 'A1', ValidationConditionType.number_greater, [10], strict=True, inputMessage='Value must be greater than 10', ) Or add validation with a drop down. .. code:: python worksheet.add_validation( 'C2:C7', ValidationConditionType.one_of_list, ['Yes', 'No',] showCustomUi=True ) Check out the api docs for `DataValidationRule`_ and `CondtionType`_ for more details. .. _CondtionType: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ConditionType .. _DataValidationRule: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#DataValidationRule Formatting ~~~~~~~~~~ Here's an example of basic formatting. Set **A1:B1** text format to bold: .. code:: python worksheet.format('A1:B1', {'textFormat': {'bold': True}}) Color the background of **A2:B2** cell range in black, change horizontal alignment, text color and font size: .. code:: python worksheet.format("A2:B2", { "backgroundColor": { "red": 0.0, "green": 0.0, "blue": 0.0 }, "horizontalAlignment": "CENTER", "textFormat": { "foregroundColor": { "red": 1.0, "green": 1.0, "blue": 1.0 }, "fontSize": 12, "bold": True } }) The second argument to :meth:`~gspread.models.Worksheet.format` is a dictionary containing the fields to update. A full specification of format options is available at `CellFormat `_ in Sheet API Reference. .. Tip:: `gspread-formatting `_ offers extensive functionality to help you when you go beyond basics. Using gspread with pandas ~~~~~~~~~~~~~~~~~~~~~~~~~ `pandas `_ is a popular library for data analysis. The simplest way to get data from a sheet to a pandas DataFrame is with :meth:`~gspread.models.Worksheet.get_all_records`: .. code:: python import pandas as pd dataframe = pd.DataFrame(worksheet.get_all_records()) Here's a basic example for writing a dataframe to a sheet. With :meth:`~gspread.models.Worksheet.update` we put the header of a dataframe into the first row of a sheet followed by the values of a dataframe: .. code:: python import pandas as pd worksheet.update([dataframe.columns.values.tolist()] + dataframe.values.tolist()) For advanced pandas use cases check out these libraries: * `gspread-pandas `_ * `gspread-dataframe `_ Using gspread with NumPy ~~~~~~~~~~~~~~~~~~~~~~~~ `NumPy `_ is a library for scientific computing in Python. It provides tools for working with high performance multi-dimensional arrays. Read contents of a sheet into a NumPy array: .. code:: python import numpy as np array = np.array(worksheet.get_all_values()) The code above assumes that your data starts from the first row of the sheet. If you have a header row in the first row, you need replace ``worksheet.get_all_values()`` with ``worksheet.get_all_values()[1:]``. Write a NumPy array to a sheet: .. code:: python import numpy as np array = np.array([[1, 2, 3], [4, 5, 6]]) # Write the array to worksheet starting from the A2 cell worksheet.update(array.tolist(), 'A2') python-gspread-6.1.4/gspread/000077500000000000000000000000001472155066000161215ustar00rootroot00000000000000python-gspread-6.1.4/gspread/__init__.py000066400000000000000000000010601472155066000202270ustar00rootroot00000000000000"""Google Spreadsheets Python API""" __version__ = "6.1.4" __author__ = "Anton Burnashev" from .auth import ( api_key, authorize, oauth, oauth_from_dict, service_account, service_account_from_dict, ) from .cell import Cell from .client import Client from .exceptions import ( GSpreadException, IncorrectCellLabel, NoValidUrlKeyFound, SpreadsheetNotFound, WorksheetNotFound, ) from .http_client import BackOffHTTPClient, HTTPClient from .spreadsheet import Spreadsheet from .worksheet import ValueRange, Worksheet python-gspread-6.1.4/gspread/auth.py000066400000000000000000000337271472155066000174500ustar00rootroot00000000000000""" gspread.auth ~~~~~~~~~~~~ Simple authentication with OAuth. """ import json import os from pathlib import Path from typing import Any, Dict, Iterable, Mapping, Optional, Protocol, Tuple, Union from google.auth.credentials import Credentials try: from google.auth.api_key import Credentials as APIKeyCredentials GOOGLE_AUTH_API_KEY_AVAILABLE = True except ImportError: GOOGLE_AUTH_API_KEY_AVAILABLE = False from google.oauth2.credentials import Credentials as OAuthCredentials from google.oauth2.service_account import Credentials as SACredentials from google_auth_oauthlib.flow import InstalledAppFlow from requests import Session from .client import Client from .http_client import HTTPClient, HTTPClientType DEFAULT_SCOPES = [ "https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/drive", ] READONLY_SCOPES = [ "https://www.googleapis.com/auth/spreadsheets.readonly", "https://www.googleapis.com/auth/drive.readonly", ] def get_config_dir( config_dir_name: str = "gspread", os_is_windows: bool = os.name == "nt" ) -> Path: r"""Construct a config dir path. By default: * `%APPDATA%\gspread` on Windows * `~/.config/gspread` everywhere else """ if os_is_windows: return Path(os.environ["APPDATA"], config_dir_name) else: return Path(Path.home(), ".config", config_dir_name) DEFAULT_CONFIG_DIR = get_config_dir() DEFAULT_CREDENTIALS_FILENAME = DEFAULT_CONFIG_DIR / "credentials.json" DEFAULT_AUTHORIZED_USER_FILENAME = DEFAULT_CONFIG_DIR / "authorized_user.json" DEFAULT_SERVICE_ACCOUNT_FILENAME = DEFAULT_CONFIG_DIR / "service_account.json" def authorize( credentials: Credentials, http_client: HTTPClientType = HTTPClient, session: Optional[Session] = None, ) -> Client: """Login to Google API using OAuth2 credentials. This is a shortcut/helper function which instantiates a client using `http_client`. By default :class:`gspread.HTTPClient` is used (but could also use :class:`gspread.BackOffHTTPClient` to avoid rate limiting). It can take an additional `requests.Session` object in order to provide you own session object. .. note:: When providing your own `requests.Session` object, use the value `None` as `credentials`. :returns: An instance of the class produced by `http_client`. :rtype: :class:`gspread.client.Client` """ return Client(auth=credentials, session=session, http_client=http_client) class FlowCallable(Protocol): """Protocol for OAuth flow callables.""" def __call__( self, client_config: Mapping[str, Any], scopes: Iterable[str], port: int = 0 ) -> OAuthCredentials: ... def local_server_flow( client_config: Mapping[str, Any], scopes: Iterable[str], port: int = 0 ) -> OAuthCredentials: """Run an OAuth flow using a local server strategy. Creates an OAuth flow and runs `google_auth_oauthlib.flow.InstalledAppFlow.run_local_server `_. This will start a local web server and open the authorization URL in the user's browser. Pass this function to ``flow`` parameter of :meth:`~gspread.oauth` to run a local server flow. """ flow = InstalledAppFlow.from_client_config(client_config, scopes) return flow.run_local_server(port=port) def load_credentials( filename: Path = DEFAULT_AUTHORIZED_USER_FILENAME, ) -> Optional[Credentials]: if filename.exists(): return OAuthCredentials.from_authorized_user_file(filename) return None def store_credentials( creds: OAuthCredentials, filename: Path = DEFAULT_AUTHORIZED_USER_FILENAME, strip: str = "token", ) -> None: filename.parent.mkdir(parents=True, exist_ok=True) with filename.open("w") as f: f.write(creds.to_json(strip)) def oauth( scopes: Iterable[str] = DEFAULT_SCOPES, flow: FlowCallable = local_server_flow, credentials_filename: Union[str, Path] = DEFAULT_CREDENTIALS_FILENAME, authorized_user_filename: Union[str, Path] = DEFAULT_AUTHORIZED_USER_FILENAME, http_client: HTTPClientType = HTTPClient, ) -> Client: r"""Authenticate with OAuth Client ID. By default this function will use the local server strategy and open the authorization URL in the user's browser:: gc = gspread.oauth() Another option is to run a console strategy. This way, the user is instructed to open the authorization URL in their browser. Once the authorization is complete, the user must then copy & paste the authorization code into the application:: gc = gspread.oauth(flow=gspread.auth.console_flow) ``scopes`` parameter defaults to read/write scope available in ``gspread.auth.DEFAULT_SCOPES``. It's read/write for Sheets and Drive API:: DEFAULT_SCOPES =[ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive' ] You can also use ``gspread.auth.READONLY_SCOPES`` for read only access. Obviously any method of ``gspread`` that updates a spreadsheet **will not work** in this case:: gc = gspread.oauth(scopes=gspread.auth.READONLY_SCOPES) sh = gc.open("A spreadsheet") sh.sheet1.update_acell('A1', '42') # <-- this will not work If you're storing your user credentials in a place other than the default, you may provide a path to that file like so:: gc = gspread.oauth( credentials_filename='/alternative/path/credentials.json', authorized_user_filename='/alternative/path/authorized_user.json', ) :param list scopes: The scopes used to obtain authorization. :param function flow: OAuth flow to use for authentication. Defaults to :meth:`~gspread.auth.local_server_flow` :param str credentials_filename: Filepath (including name) pointing to a credentials `.json` file. Defaults to DEFAULT_CREDENTIALS_FILENAME: * `%APPDATA%\gspread\credentials.json` on Windows * `~/.config/gspread/credentials.json` everywhere else :param str authorized_user_filename: Filepath (including name) pointing to an authorized user `.json` file. Defaults to DEFAULT_AUTHORIZED_USER_FILENAME: * `%APPDATA%\gspread\authorized_user.json` on Windows * `~/.config/gspread/authorized_user.json` everywhere else :type http_client: :class:`gspread.http_client.HTTPClient` :param http_client: A factory function that returns a client class. Defaults to :class:`gspread.http_client.HTTPClient` (but could also use :class:`gspread.http_client.BackOffHTTPClient` to avoid rate limiting) :rtype: :class:`gspread.client.Client` """ authorized_user_filename = Path(authorized_user_filename) creds = load_credentials(filename=authorized_user_filename) if not isinstance(creds, Credentials): with open(credentials_filename) as json_file: client_config = json.load(json_file) creds = flow(client_config=client_config, scopes=scopes) store_credentials(creds, filename=authorized_user_filename) return Client(auth=creds, http_client=http_client) def oauth_from_dict( credentials: Optional[Mapping[str, Any]] = None, authorized_user_info: Optional[Mapping[str, Any]] = None, scopes: Iterable[str] = DEFAULT_SCOPES, flow: FlowCallable = local_server_flow, http_client: HTTPClientType = HTTPClient, ) -> Tuple[Client, Dict[str, Any]]: r"""Authenticate with OAuth Client ID. By default this function will use the local server strategy and open the authorization URL in the user's browser:: gc = gspread.oauth_from_dict() Another option is to run a console strategy. This way, the user is instructed to open the authorization URL in their browser. Once the authorization is complete, the user must then copy & paste the authorization code into the application:: gc = gspread.oauth_from_dict(flow=gspread.auth.console_flow) ``scopes`` parameter defaults to read/write scope available in ``gspread.auth.DEFAULT_SCOPES``. It's read/write for Sheets and Drive API:: DEFAULT_SCOPES =[ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive' ] You can also use ``gspread.auth.READONLY_SCOPES`` for read only access. Obviously any method of ``gspread`` that updates a spreadsheet **will not work** in this case:: gc = gspread.oauth_from_dict(scopes=gspread.auth.READONLY_SCOPES) sh = gc.open("A spreadsheet") sh.sheet1.update_acell('A1', '42') # <-- this will not work This function requires you to pass the credentials directly as a python dict. After the first authentication the function returns the authenticated user info, this can be passed again to authenticate the user without the need to run the flow again. .. code block below must be explicitly announced using code-block .. code-block:: python gc = gspread.oauth_from_dict( credentials=my_creds, authorized_user_info=my_auth_user ) :param dict credentials: The credentials from google cloud platform :param dict authorized_user_info: The authenticated user if already authenticated. :param list scopes: The scopes used to obtain authorization. :param function flow: OAuth flow to use for authentication. Defaults to :meth:`~gspread.auth.local_server_flow` :type http_client: :class:`gspread.http_client.HTTPClient` :param http_client: A factory function that returns a client class. Defaults to :class:`gspread.http_client.HTTPClient` (but could also use :class:`gspread.http_client.BackOffHTTPClient` to avoid rate limiting) :rtype: (:class:`gspread.client.Client`, str) """ if authorized_user_info is not None: creds = OAuthCredentials.from_authorized_user_info(authorized_user_info, scopes) if not creds and credentials is not None: creds = flow(client_config=credentials, scopes=scopes) client = Client(auth=creds, http_client=http_client) # must return the creds to the user # must strip the token an use the dedicated method from Credentials # to return a dict "safe to store". return (client, creds.to_json("token")) def service_account( filename: Union[Path, str] = DEFAULT_SERVICE_ACCOUNT_FILENAME, scopes: Iterable[str] = DEFAULT_SCOPES, http_client: HTTPClientType = HTTPClient, ) -> Client: """Authenticate using a service account. ``scopes`` parameter defaults to read/write scope available in ``gspread.auth.DEFAULT_SCOPES``. It's read/write for Sheets and Drive API:: DEFAULT_SCOPES =[ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive' ] You can also use ``gspread.auth.READONLY_SCOPES`` for read only access. Obviously any method of ``gspread`` that updates a spreadsheet **will not work** in this case. :param str filename: The path to the service account json file. :param list scopes: The scopes used to obtain authorization. :type http_client: :class:`gspread.http_client.HTTPClient` :param http_client: A factory function that returns a client class. Defaults to :class:`gspread.HTTPClient` (but could also use :class:`gspread.BackOffHTTPClient` to avoid rate limiting) :rtype: :class:`gspread.client.Client` """ creds = SACredentials.from_service_account_file(filename, scopes=scopes) return Client(auth=creds, http_client=http_client) def service_account_from_dict( info: Mapping[str, Any], scopes: Iterable[str] = DEFAULT_SCOPES, http_client: HTTPClientType = HTTPClient, ) -> Client: """Authenticate using a service account (json). ``scopes`` parameter defaults to read/write scope available in ``gspread.auth.DEFAULT_SCOPES``. It's read/write for Sheets and Drive API:: DEFAULT_SCOPES =[ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive' ] You can also use ``gspread.auth.READONLY_SCOPES`` for read only access. Obviously any method of ``gspread`` that updates a spreadsheet **will not work** in this case. :param info (Mapping[str, str]): The service account info in Google format :param list scopes: The scopes used to obtain authorization. :type http_client: :class:`gspread.http_client.HTTPClient` :param http_client: A factory function that returns a client class. Defaults to :class:`gspread.http_client.HTTPClient` (but could also use :class:`gspread.http_client.BackOffHTTPClient` to avoid rate limiting) :rtype: :class:`gspread.client.Client` """ creds = SACredentials.from_service_account_info( info=info, scopes=scopes, ) return Client(auth=creds, http_client=http_client) def api_key(token: str, http_client: HTTPClientType = HTTPClient) -> Client: """Authenticate using an API key. Allows you to open public spreadsheet files. .. warning:: This method only allows you to open public spreadsheet files. It does not work for private spreadsheet files. :param token str: The actual API key to use :type http_client: :class:`gspread.http_client.HTTPClient` :param http_client: A factory function that returns a client class. Defaults to :class:`gspread.http_client.HTTPClient` (but could also use :class:`gspread.http_client.BackOffHTTPClient` to avoid rate limiting) :rtype: :class:`gspread.client.Client` """ if GOOGLE_AUTH_API_KEY_AVAILABLE is False: raise NotImplementedError( "api_key is only available with package google.auth>=2.4.0." 'Install it with "pip install google-auth>=2.4.0".' ) creds = APIKeyCredentials(token) return Client(auth=creds, http_client=http_client) python-gspread-6.1.4/gspread/cell.py000066400000000000000000000045471472155066000174240ustar00rootroot00000000000000""" gspread.cell ~~~~~~~~~~~~ This module contains common cells' models. """ from typing import Optional, Union from .utils import a1_to_rowcol, numericise, rowcol_to_a1 class Cell: """An instance of this class represents a single cell in a :class:`~gspread.worksheet.Worksheet`. """ def __init__(self, row: int, col: int, value: Optional[str] = "") -> None: self._row: int = row self._col: int = col #: Value of the cell. self.value: Optional[str] = value @classmethod def from_address(cls, label: str, value: str = "") -> "Cell": """Instantiate a new :class:`~gspread.cell.Cell` from an A1 notation address and a value :param string label: the A1 label of the returned cell :param string value: the value for the returned cell :rtype: Cell """ row, col = a1_to_rowcol(label) return cls(row, col, value) def __repr__(self) -> str: return "<{} R{}C{} {}>".format( self.__class__.__name__, self.row, self.col, repr(self.value), ) def __eq__(self, other: object) -> bool: if not isinstance(other, Cell): return False same_row = self.row == other.row same_col = self.col == other.col same_value = self.value == other.value return same_row and same_col and same_value @property def row(self) -> int: """Row number of the cell. :type: int """ return self._row @property def col(self) -> int: """Column number of the cell. :type: int """ return self._col @property def numeric_value(self) -> Optional[Union[int, float]]: """Numeric value of this cell. Will try to numericise this cell value, upon success will return its numeric value with the appropriate type. :type: int or float """ numeric_value = numericise(self.value, default_blank=None) # if could not convert, return None if isinstance(numeric_value, int) or isinstance(numeric_value, float): return numeric_value else: return None @property def address(self) -> str: """Cell address in A1 notation. :type: str """ return rowcol_to_a1(self.row, self.col) python-gspread-6.1.4/gspread/client.py000066400000000000000000000373711472155066000177640ustar00rootroot00000000000000""" gspread.client ~~~~~~~~~~~~~~ This module contains Client class responsible for managing spreadsheet files """ from http import HTTPStatus from typing import Any, Dict, List, Optional, Tuple, Union from google.auth.credentials import Credentials from requests import Response, Session from .exceptions import APIError, SpreadsheetNotFound from .http_client import HTTPClient, HTTPClientType, ParamsType from .spreadsheet import Spreadsheet from .urls import DRIVE_FILES_API_V3_COMMENTS_URL, DRIVE_FILES_API_V3_URL from .utils import ExportFormat, MimeType, extract_id_from_url, finditem class Client: """An instance of this class Manages Spreadsheet files It is used to: - open/create/list/delete spreadsheets - create/delete/list spreadsheet permission - etc It is the gspread entry point. It will handle creating necessary :class:`~gspread.models.Spreadsheet` instances. """ def __init__( self, auth: Credentials, session: Optional[Session] = None, http_client: HTTPClientType = HTTPClient, ) -> None: self.http_client = http_client(auth, session) def set_timeout( self, timeout: Optional[Union[float, Tuple[float, float]]] = None ) -> None: """How long to wait for the server to send data before giving up, as a float, or a ``(connect timeout, read timeout)`` tuple. Use value ``None`` to restore default timeout Value for ``timeout`` is in seconds (s). """ self.http_client.set_timeout(timeout) def get_file_drive_metadata(self, id: str) -> Any: """Get the metadata from the Drive API for a specific file This method is mainly here to retrieve the create/update time of a file (these metadata are only accessible from the Drive API). """ return self.http_client.get_file_drive_metadata(id) def list_spreadsheet_files( self, title: Optional[str] = None, folder_id: Optional[str] = None ) -> List[Dict[str, Any]]: """List all the spreadsheet files Will list all spreadsheet files owned by/shared with this user account. :param str title: Filter only spreadsheet files with this title :param str folder_id: Only look for spreadsheet files in this folder The parameter ``folder_id`` can be obtained from the URL when looking at a folder in a web browser as follow: ``https://drive.google.com/drive/u/0/folders/`` :returns: a list of dicts containing the keys id, name, createdTime and modifiedTime. """ files, _ = self._list_spreadsheet_files(title=title, folder_id=folder_id) return files def _list_spreadsheet_files( self, title: Optional[str] = None, folder_id: Optional[str] = None ) -> Tuple[List[Dict[str, Any]], Response]: files = [] page_token = "" url = DRIVE_FILES_API_V3_URL query = f'mimeType="{MimeType.google_sheets}"' if title: query += f' and name = "{title}"' if folder_id: query += f' and parents in "{folder_id}"' params: ParamsType = { "q": query, "pageSize": 1000, "supportsAllDrives": True, "includeItemsFromAllDrives": True, "fields": "kind,nextPageToken,files(id,name,createdTime,modifiedTime)", } while True: if page_token: params["pageToken"] = page_token response = self.http_client.request("get", url, params=params) response_json = response.json() files.extend(response_json["files"]) page_token = response_json.get("nextPageToken", None) if page_token is None: break return files, response def open(self, title: str, folder_id: Optional[str] = None) -> Spreadsheet: """Opens a spreadsheet. :param str title: A title of a spreadsheet. :param str folder_id: (optional) If specified can be used to filter spreadsheets by parent folder ID. :returns: a :class:`~gspread.models.Spreadsheet` instance. If there's more than one spreadsheet with same title the first one will be opened. :raises gspread.SpreadsheetNotFound: if no spreadsheet with specified `title` is found. >>> gc.open('My fancy spreadsheet') """ spreadsheet_files, response = self._list_spreadsheet_files(title, folder_id) try: properties = finditem( lambda x: x["name"] == title, spreadsheet_files, ) except StopIteration as ex: raise SpreadsheetNotFound(response) from ex # Drive uses different terminology properties["title"] = properties["name"] return Spreadsheet(self.http_client, properties) def open_by_key(self, key: str) -> Spreadsheet: """Opens a spreadsheet specified by `key` (a.k.a Spreadsheet ID). :param str key: A key of a spreadsheet as it appears in a URL in a browser. :returns: a :class:`~gspread.models.Spreadsheet` instance. >>> gc.open_by_key('0BmgG6nO_6dprdS1MN3d3MkdPa142WFRrdnRRUWl1UFE') """ try: spreadsheet = Spreadsheet(self.http_client, {"id": key}) except APIError as ex: if ex.response.status_code == HTTPStatus.NOT_FOUND: raise SpreadsheetNotFound(ex.response) from ex if ex.response.status_code == HTTPStatus.FORBIDDEN: raise PermissionError from ex raise ex return spreadsheet def open_by_url(self, url: str) -> Spreadsheet: """Opens a spreadsheet specified by `url`. :param str url: URL of a spreadsheet as it appears in a browser. :returns: a :class:`~gspread.models.Spreadsheet` instance. :raises gspread.SpreadsheetNotFound: if no spreadsheet with specified `url` is found. >>> gc.open_by_url('https://docs.google.com/spreadsheet/ccc?key=0Bm...FE&hl') """ return self.open_by_key(extract_id_from_url(url)) def openall(self, title: Optional[str] = None) -> List[Spreadsheet]: """Opens all available spreadsheets. :param str title: (optional) If specified can be used to filter spreadsheets by title. :returns: a list of :class:`~gspread.models.Spreadsheet` instances. """ spreadsheet_files = self.list_spreadsheet_files(title) if title: spreadsheet_files = [ spread for spread in spreadsheet_files if title == spread["name"] ] return [ Spreadsheet(self.http_client, dict(title=x["name"], **x)) for x in spreadsheet_files ] def create(self, title: str, folder_id: Optional[str] = None) -> Spreadsheet: """Creates a new spreadsheet. :param str title: A title of a new spreadsheet. :param str folder_id: Id of the folder where we want to save the spreadsheet. :returns: a :class:`~gspread.models.Spreadsheet` instance. """ payload: Dict[str, Any] = { "name": title, "mimeType": MimeType.google_sheets, } params: ParamsType = { "supportsAllDrives": True, } if folder_id is not None: payload["parents"] = [folder_id] r = self.http_client.request( "post", DRIVE_FILES_API_V3_URL, json=payload, params=params ) spreadsheet_id = r.json()["id"] return self.open_by_key(spreadsheet_id) def export(self, file_id: str, format: str = ExportFormat.PDF) -> bytes: """Export the spreadsheet in the given format. :param str file_id: The key of the spreadsheet to export :param str format: The format of the resulting file. Possible values are: * ``ExportFormat.PDF`` * ``ExportFormat.EXCEL`` * ``ExportFormat.CSV`` * ``ExportFormat.OPEN_OFFICE_SHEET`` * ``ExportFormat.TSV`` * ``ExportFormat.ZIPPED_HTML`` See `ExportFormat`_ in the Drive API. :type format: :class:`~gspread.utils.ExportFormat` :returns bytes: The content of the exported file. .. _ExportFormat: https://developers.google.com/drive/api/guides/ref-export-formats """ return self.http_client.export(file_id=file_id, format=format) def copy( self, file_id: str, title: Optional[str] = None, copy_permissions: bool = False, folder_id: Optional[str] = None, copy_comments: bool = True, ) -> Spreadsheet: """Copies a spreadsheet. :param str file_id: A key of a spreadsheet to copy. :param str title: (optional) A title for the new spreadsheet. :param bool copy_permissions: (optional) If True, copy permissions from the original spreadsheet to the new spreadsheet. :param str folder_id: Id of the folder where we want to save the spreadsheet. :param bool copy_comments: (optional) If True, copy the comments from the original spreadsheet to the new spreadsheet. :returns: a :class:`~gspread.models.Spreadsheet` instance. .. versionadded:: 3.1.0 .. note:: If you're using custom credentials without the Drive scope, you need to add ``https://www.googleapis.com/auth/drive`` to your OAuth scope in order to use this method. Example:: scope = [ 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive' ] Otherwise, you will get an ``Insufficient Permission`` error when you try to copy a spreadsheet. """ url = "{}/{}/copy".format(DRIVE_FILES_API_V3_URL, file_id) payload: Dict[str, Any] = { "name": title, "mimeType": MimeType.google_sheets, } if folder_id is not None: payload["parents"] = [folder_id] params: ParamsType = {"supportsAllDrives": True} r = self.http_client.request("post", url, json=payload, params=params) spreadsheet_id = r.json()["id"] new_spreadsheet = self.open_by_key(spreadsheet_id) if copy_permissions is True: original = self.open_by_key(file_id) permissions = original.list_permissions() for p in permissions: if p.get("deleted"): continue # In case of domain type the domain extract the domain # In case of user/group extract the emailAddress # Otherwise use None for type 'Anyone' email_or_domain = "" if str(p["type"]) == "domain": email_or_domain = str(p["domain"]) elif str(p["type"]) in ("user", "group"): email_or_domain = str(p["emailAddress"]) new_spreadsheet.share( email_address=email_or_domain, perm_type=str(p["type"]), role=str(p["role"]), notify=False, ) if copy_comments is True: source_url = DRIVE_FILES_API_V3_COMMENTS_URL % (file_id) page_token = "" comments = [] params = { "fields": "comments/content,comments/anchor,nextPageToken", "includeDeleted": False, "pageSize": 100, # API limit to maximum 100 } while page_token is not None: params["pageToken"] = page_token res = self.http_client.request("get", source_url, params=params).json() comments.extend(res["comments"]) page_token = res.get("nextPageToken", None) destination_url = DRIVE_FILES_API_V3_COMMENTS_URL % (new_spreadsheet.id) # requesting some fields in the response is mandatory from the API. # choose 'id' randomly out of all the fields, but no need to use it for now. params = {"fields": "id"} for comment in comments: self.http_client.request( "post", destination_url, json=comment, params=params ) return new_spreadsheet def del_spreadsheet(self, file_id: str) -> None: """Deletes a spreadsheet. :param str file_id: a spreadsheet ID (a.k.a file ID). """ url = "{}/{}".format(DRIVE_FILES_API_V3_URL, file_id) params: ParamsType = {"supportsAllDrives": True} self.http_client.request("delete", url, params=params) def import_csv(self, file_id: str, data: Union[str, bytes]) -> Any: """Imports data into the first page of the spreadsheet. :param str file_id: :param str data: A CSV string of data. Example: .. code:: # Read CSV file contents content = open('file_to_import.csv', 'r').read() gc.import_csv(spreadsheet.id, content) .. note:: This method removes all other worksheets and then entirely replaces the contents of the first worksheet. """ return self.http_client.import_csv(file_id, data) def list_permissions(self, file_id: str) -> List[Dict[str, Union[str, bool]]]: """Retrieve a list of permissions for a file. :param str file_id: a spreadsheet ID (aka file ID). """ return self.http_client.list_permissions(file_id) def insert_permission( self, file_id: str, value: Optional[str] = None, perm_type: Optional[str] = None, role: Optional[str] = None, notify: bool = True, email_message: Optional[str] = None, with_link: bool = False, ) -> Response: """Creates a new permission for a file. :param str file_id: a spreadsheet ID (aka file ID). :param value: user or group e-mail address, domain name or None for 'anyone' type. :type value: str, None :param str perm_type: (optional) The account type. Allowed values are: ``user``, ``group``, ``domain``, ``anyone`` :param str role: (optional) The primary role for this user. Allowed values are: ``owner``, ``writer``, ``reader`` :param bool notify: (optional) Whether to send an email to the target user/domain. :param str email_message: (optional) An email message to be sent if ``notify=True``. :param bool with_link: (optional) Whether the link is required for this permission to be active. :returns dict: the newly created permission Examples:: # Give write permissions to otto@example.com gc.insert_permission( '0BmgG6nO_6dprnRRUWl1UFE', 'otto@example.org', perm_type='user', role='writer' ) # Make the spreadsheet publicly readable gc.insert_permission( '0BmgG6nO_6dprnRRUWl1UFE', None, perm_type='anyone', role='reader' ) """ return self.http_client.insert_permission( file_id, value, perm_type, role, notify, email_message, with_link ) def remove_permission(self, file_id: str, permission_id: str) -> None: """Deletes a permission from a file. :param str file_id: a spreadsheet ID (aka file ID.) :param str permission_id: an ID for the permission. """ self.http_client.remove_permission(file_id, permission_id) python-gspread-6.1.4/gspread/exceptions.py000066400000000000000000000035461472155066000206640ustar00rootroot00000000000000""" gspread.exceptions ~~~~~~~~~~~~~~~~~~ Exceptions used in gspread. """ from typing import Any, Mapping from requests import Response class UnSupportedExportFormat(Exception): """Raised when export format is not supported.""" class GSpreadException(Exception): """A base class for gspread's exceptions.""" class WorksheetNotFound(GSpreadException): """Trying to open non-existent or inaccessible worksheet.""" class NoValidUrlKeyFound(GSpreadException): """No valid key found in URL.""" class IncorrectCellLabel(GSpreadException): """The cell label is incorrect.""" class InvalidInputValue(GSpreadException): """The provided values is incorrect.""" class APIError(GSpreadException): """Errors coming from the API itself, such as when we attempt to retrieve things that don't exist.""" def __init__(self, response: Response): try: error = response.json()["error"] except Exception as e: # in case we failed to parse the error from the API # build an empty error object to notify the caller # and keep the exception raise flow running error = { "code": -1, "message": response.text, "status": "invalid JSON: '{}'".format(e), } super().__init__(error) self.response: Response = response self.error: Mapping[str, Any] = error self.code: int = self.error["code"] def __str__(self) -> str: return "{}: [{}]: {}".format( self.__class__.__name__, self.code, self.error["message"] ) def __repr__(self) -> str: return self.__str__() def __reduce__(self) -> tuple: return self.__class__, (self.response,) class SpreadsheetNotFound(GSpreadException): """Trying to open non-existent or inaccessible spreadsheet.""" python-gspread-6.1.4/gspread/http_client.py000066400000000000000000000541751472155066000210240ustar00rootroot00000000000000""" gspread.http_client ~~~~~~~~~~~~~~ This module contains HTTPClient class responsible for communicating with Google API. """ import time from http import HTTPStatus from typing import ( IO, Any, Dict, List, Mapping, MutableMapping, Optional, Tuple, Type, Union, ) from google.auth.credentials import Credentials from google.auth.transport.requests import AuthorizedSession from requests import Response, Session from .exceptions import APIError, UnSupportedExportFormat from .urls import ( DRIVE_FILES_API_V3_URL, DRIVE_FILES_UPLOAD_API_V2_URL, SPREADSHEET_BATCH_UPDATE_URL, SPREADSHEET_SHEETS_COPY_TO_URL, SPREADSHEET_URL, SPREADSHEET_VALUES_APPEND_URL, SPREADSHEET_VALUES_BATCH_CLEAR_URL, SPREADSHEET_VALUES_BATCH_UPDATE_URL, SPREADSHEET_VALUES_BATCH_URL, SPREADSHEET_VALUES_CLEAR_URL, SPREADSHEET_VALUES_URL, ) from .utils import ExportFormat, convert_credentials, quote ParamsType = MutableMapping[str, Optional[Union[str, int, bool, float, List[str]]]] FileType = Optional[ Union[ MutableMapping[str, IO[Any]], MutableMapping[str, Tuple[str, IO[Any]]], MutableMapping[str, Tuple[str, IO[Any], str]], MutableMapping[str, Tuple[str, IO[Any], str, MutableMapping[str, str]]], ] ] class HTTPClient: """An instance of this class communicates with Google API. :param Credentials auth: An instance of google.auth.Credentials used to authenticate requests created by either: * gspread.auth.oauth() * gspread.auth.oauth_from_dict() * gspread.auth.service_account() * gspread.auth.service_account_from_dict() :param Session session: (Optional) An OAuth2 credential object. Credential objects created by `google-auth `_. You can pass you own Session object, simply pass ``auth=None`` and ``session=my_custom_session``. This class is not intended to be created manually. It will be created by the gspread.Client class. """ def __init__(self, auth: Credentials, session: Optional[Session] = None) -> None: if session is not None: self.session = session else: self.auth: Credentials = convert_credentials(auth) self.session = AuthorizedSession(self.auth) self.timeout: Optional[Union[float, Tuple[float, float]]] = None def login(self) -> None: from google.auth.transport.requests import Request self.auth.refresh(Request(self.session)) self.session.headers.update({"Authorization": "Bearer %s" % self.auth.token}) def set_timeout(self, timeout: Optional[Union[float, Tuple[float, float]]]) -> None: """How long to wait for the server to send data before giving up, as a float, or a ``(connect timeout, read timeout)`` tuple. Use value ``None`` to restore default timeout Value for ``timeout`` is in seconds (s). """ self.timeout = timeout def request( self, method: str, endpoint: str, params: Optional[ParamsType] = None, data: Optional[bytes] = None, json: Optional[Mapping[str, Any]] = None, files: FileType = None, headers: Optional[MutableMapping[str, str]] = None, ) -> Response: response = self.session.request( method=method, url=endpoint, json=json, params=params, data=data, files=files, headers=headers, timeout=self.timeout, ) if response.ok: return response else: raise APIError(response) def batch_update(self, id: str, body: Optional[Mapping[str, Any]]) -> Any: """Lower-level method that directly calls `spreadsheets/:batchUpdate `_. :param dict body: `Batch Update Request body `_. :returns: `Batch Update Response body `_. :rtype: dict .. versionadded:: 3.0 """ r = self.request("post", SPREADSHEET_BATCH_UPDATE_URL % id, json=body) return r.json() def values_update( self, id: str, range: str, params: Optional[ParamsType] = None, body: Optional[Mapping[str, Any]] = None, ) -> Any: """Lower-level method that directly calls `PUT spreadsheets//values/ `_. :param str range: The `A1 notation `_ of the values to update. :param dict params: (optional) `Values Update Query parameters `_. :param dict body: (optional) `Values Update Request body `_. :returns: `Values Update Response body `_. :rtype: dict Example:: sh.values_update( 'Sheet1!A2', params={ 'valueInputOption': 'USER_ENTERED' }, body={ 'values': [[1, 2, 3]] } ) .. versionadded:: 3.0 """ url = SPREADSHEET_VALUES_URL % (id, quote(range)) r = self.request("put", url, params=params, json=body) return r.json() def values_append( self, id: str, range: str, params: ParamsType, body: Optional[Mapping[str, Any]] ) -> Any: """Lower-level method that directly calls `spreadsheets//values:append `_. :param str range: The `A1 notation `_ of a range to search for a logical table of data. Values will be appended after the last row of the table. :param dict params: `Values Append Query parameters `_. :param dict body: `Values Append Request body `_. :returns: `Values Append Response body `_. :rtype: dict .. versionadded:: 3.0 """ url = SPREADSHEET_VALUES_APPEND_URL % (id, quote(range)) r = self.request("post", url, params=params, json=body) return r.json() def values_clear(self, id: str, range: str) -> Any: """Lower-level method that directly calls `spreadsheets//values:clear `_. :param str range: The `A1 notation `_ of the values to clear. :returns: `Values Clear Response body `_. :rtype: dict .. versionadded:: 3.0 """ url = SPREADSHEET_VALUES_CLEAR_URL % (id, quote(range)) r = self.request("post", url) return r.json() def values_batch_clear( self, id: str, params: Optional[ParamsType] = None, body: Optional[Mapping[str, Any]] = None, ) -> Any: """Lower-level method that directly calls `spreadsheets//values:batchClear` :param dict params: (optional) `Values Batch Clear Query parameters `_. :param dict body: (optional) `Values Batch Clear request body `_. :rtype: dict """ url = SPREADSHEET_VALUES_BATCH_CLEAR_URL % id r = self.request("post", url, params=params, json=body) return r.json() def values_get( self, id: str, range: str, params: Optional[ParamsType] = None ) -> Any: """Lower-level method that directly calls `GET spreadsheets//values/ `_. :param str range: The `A1 notation `_ of the values to retrieve. :param dict params: (optional) `Values Get Query parameters `_. :returns: `Values Get Response body `_. :rtype: dict .. versionadded:: 3.0 """ url = SPREADSHEET_VALUES_URL % (id, quote(range)) r = self.request("get", url, params=params) return r.json() def values_batch_get( self, id: str, ranges: List[str], params: Optional[ParamsType] = None ) -> Any: """Lower-level method that directly calls `spreadsheets//values:batchGet `_. :param list ranges: List of ranges in the `A1 notation `_ of the values to retrieve. :param dict params: (optional) `Values Batch Get Query parameters `_. :returns: `Values Batch Get Response body `_. :rtype: dict """ if params is None: params = {} params["ranges"] = ranges url = SPREADSHEET_VALUES_BATCH_URL % id r = self.request("get", url, params=params) return r.json() def values_batch_update( self, id: str, body: Optional[Mapping[str, Any]] = None ) -> Any: """Lower-level method that directly calls `spreadsheets//values:batchUpdate `_. :param dict body: (optional) `Values Batch Update Request body `_. :returns: `Values Batch Update Response body `_. :rtype: dict """ url = SPREADSHEET_VALUES_BATCH_UPDATE_URL % id r = self.request("post", url, json=body) return r.json() def spreadsheets_get(self, id: str, params: Optional[ParamsType] = None) -> Any: """A method stub that directly calls `spreadsheets.get `_.""" url = SPREADSHEET_URL % id r = self.request("get", url, params=params) return r.json() def spreadsheets_sheets_copy_to( self, id: str, sheet_id: int, destination_spreadsheet_id: str ) -> Any: """Lower-level method that directly calls `spreadsheets.sheets.copyTo `_.""" url = SPREADSHEET_SHEETS_COPY_TO_URL % (id, sheet_id) body = {"destinationSpreadsheetId": destination_spreadsheet_id} r = self.request("post", url, json=body) return r.json() def fetch_sheet_metadata( self, id: str, params: Optional[ParamsType] = None ) -> Mapping[str, Any]: """Similar to :method spreadsheets_get:`gspread.http_client.spreadsheets_get`, get the spreadsheet form the API but by default **does not get the cells data**. It only retrieve the the metadata from the spreadsheet. :param str id: the spreadsheet ID key :param dict params: (optional) the HTTP params for the GET request. By default sets the parameter ``includeGridData`` to ``false``. :returns: The raw spreadsheet :rtype: dict """ if params is None: params = {"includeGridData": "false"} url = SPREADSHEET_URL % id r = self.request("get", url, params=params) return r.json() def get_file_drive_metadata(self, id: str) -> Any: """Get the metadata from the Drive API for a specific file This method is mainly here to retrieve the create/update time of a file (these metadata are only accessible from the Drive API). """ url = DRIVE_FILES_API_V3_URL + "/{}".format(id) params: ParamsType = { "supportsAllDrives": True, "includeItemsFromAllDrives": True, "fields": "id,name,createdTime,modifiedTime", } res = self.request("get", url, params=params) return res.json() def export(self, file_id: str, format: str = ExportFormat.PDF) -> bytes: """Export the spreadsheet in the given format. :param str file_id: The key of the spreadsheet to export :param str format: The format of the resulting file. Possible values are: * ``ExportFormat.PDF`` * ``ExportFormat.EXCEL`` * ``ExportFormat.CSV`` * ``ExportFormat.OPEN_OFFICE_SHEET`` * ``ExportFormat.TSV`` * ``ExportFormat.ZIPPED_HTML`` See `ExportFormat`_ in the Drive API. :type format: :class:`~gspread.utils.ExportFormat` :returns bytes: The content of the exported file. .. _ExportFormat: https://developers.google.com/drive/api/guides/ref-export-formats """ if format not in ExportFormat: raise UnSupportedExportFormat url = "{}/{}/export".format(DRIVE_FILES_API_V3_URL, file_id) params: ParamsType = {"mimeType": format} r = self.request("get", url, params=params) return r.content def insert_permission( self, file_id: str, email_address: Optional[str], perm_type: Optional[str], role: Optional[str], notify: bool = True, email_message: Optional[str] = None, with_link: bool = False, ) -> Response: """Creates a new permission for a file. :param str file_id: a spreadsheet ID (aka file ID). :param email_address: user or group e-mail address, domain name or None for 'anyone' type. :type email_address: str, None :param str perm_type: (optional) The account type. Allowed values are: ``user``, ``group``, ``domain``, ``anyone`` :param str role: (optional) The primary role for this user. Allowed values are: ``owner``, ``writer``, ``reader`` :param bool notify: Whether to send an email to the target user/domain. Default ``True``. :param str email_message: (optional) An email message to be sent if ``notify=True``. :param bool with_link: Whether the link is required for this permission to be active. Default ``False``. :returns dict: the newly created permission Examples:: # Give write permissions to otto@example.com gc.insert_permission( '0BmgG6nO_6dprnRRUWl1UFE', 'otto@example.org', perm_type='user', role='writer' ) # Make the spreadsheet publicly readable gc.insert_permission( '0BmgG6nO_6dprnRRUWl1UFE', None, perm_type='anyone', role='reader' ) """ url = "{}/{}/permissions".format(DRIVE_FILES_API_V3_URL, file_id) payload = { "type": perm_type, "role": role, "withLink": with_link, } params: ParamsType = { "supportsAllDrives": "true", } if perm_type == "domain": payload["domain"] = email_address elif perm_type in {"user", "group"}: payload["emailAddress"] = email_address params["sendNotificationEmail"] = notify params["emailMessage"] = email_message elif perm_type == "anyone": pass else: raise ValueError("Invalid permission type: {}".format(perm_type)) return self.request("post", url, json=payload, params=params) def list_permissions(self, file_id: str) -> List[Dict[str, Union[str, bool]]]: """Retrieve a list of permissions for a file. :param str file_id: a spreadsheet ID (aka file ID). """ url = "{}/{}/permissions".format(DRIVE_FILES_API_V3_URL, file_id) params: ParamsType = { "supportsAllDrives": True, "fields": "nextPageToken,permissions", } token = "" permissions = [] while token is not None: if token: params["pageToken"] = token r = self.request("get", url, params=params).json() permissions.extend(r["permissions"]) token = r.get("nextPageToken", None) return permissions def remove_permission(self, file_id: str, permission_id: str) -> None: """Deletes a permission from a file. :param str file_id: a spreadsheet ID (aka file ID.) :param str permission_id: an ID for the permission. """ url = "{}/{}/permissions/{}".format( DRIVE_FILES_API_V3_URL, file_id, permission_id ) params: ParamsType = {"supportsAllDrives": True} self.request("delete", url, params=params) def import_csv(self, file_id: str, data: Union[str, bytes]) -> Any: """Imports data into the first page of the spreadsheet. :param str data: A CSV string of data. Example: .. code:: # Read CSV file contents content = open('file_to_import.csv', 'r').read() gc.import_csv(spreadsheet.id, content) .. note:: This method removes all other worksheets and then entirely replaces the contents of the first worksheet. """ # Make sure we send utf-8 if isinstance(data, str): data = data.encode("utf-8") headers = {"Content-Type": "text/csv"} url = "{}/{}".format(DRIVE_FILES_UPLOAD_API_V2_URL, file_id) res = self.request( "put", url, data=data, params={ "uploadType": "media", "convert": True, "supportsAllDrives": True, }, headers=headers, ) return res.json() class BackOffHTTPClient(HTTPClient): """BackoffClient is a gspread client with exponential backoff retries. In case a request fails due to some API rate limits, it will wait for some time, then retry the request. This can help by trying the request after some time and prevent the application from failing (by raising an APIError exception). .. Warning:: This Client is not production ready yet. Use it at your own risk ! .. note:: To use with the `auth` module, make sure to pass this backoff client factory using the ``client_factory`` parameter of the method used. .. note:: Currently known issues are: * will retry exponentially even when the error should raise instantly. Due to the Drive API that raises 403 (Forbidden) errors for forbidden access and for api rate limit exceeded.""" _HTTP_ERROR_CODES: List[HTTPStatus] = [ HTTPStatus.REQUEST_TIMEOUT, # in case of a timeout HTTPStatus.TOO_MANY_REQUESTS, # sheet API usage rate limit exceeded ] _NR_BACKOFF: int = 0 _MAX_BACKOFF: int = 128 # arbitrary maximum backoff def request(self, *args: Any, **kwargs: Any) -> Response: # Check if we should retry the request def _should_retry( code: int, error: Mapping[str, Any], wait: int, ) -> bool: # Drive API return a dict object 'errors', the sheet API does not if "errors" in error: # Drive API returns a code 403 when reaching quotas/usage limits if ( code == HTTPStatus.FORBIDDEN and error["errors"][0]["domain"] == "usageLimits" ): return True # We retry if: # - the return code is one of: # - 429: too many requests # - 408: request timeout # - >= 500: some server error # - AND we did not reach the max retry limit return ( code in self._HTTP_ERROR_CODES or code >= HTTPStatus.INTERNAL_SERVER_ERROR ) and wait <= self._MAX_BACKOFF try: return super().request(*args, **kwargs) except APIError as err: code = err.code error = err.error self._NR_BACKOFF += 1 wait = min(2**self._NR_BACKOFF, self._MAX_BACKOFF) # check if error should retry if _should_retry(code, error, wait) is True: time.sleep(wait) # make the request again response = self.request(*args, **kwargs) # reset counters for next time self._NR_BACKOFF = 0 return response # failed too many times, raise APIEerror raise err HTTPClientType = Type[HTTPClient] python-gspread-6.1.4/gspread/py.typed000066400000000000000000000000001472155066000176060ustar00rootroot00000000000000python-gspread-6.1.4/gspread/spreadsheet.py000066400000000000000000000706021472155066000210070ustar00rootroot00000000000000""" gspread.spreadsheet ~~~~~~~~~~~~~~ This module contains common spreadsheets' models. """ import warnings from typing import Any, Dict, Generator, Iterable, List, Mapping, Optional, Union from requests import Response from .cell import Cell from .exceptions import WorksheetNotFound from .http_client import HTTPClient, ParamsType from .urls import DRIVE_FILES_API_V3_URL, SPREADSHEET_DRIVE_URL from .utils import ExportFormat, finditem from .worksheet import Worksheet class Spreadsheet: """The class that represents a spreadsheet.""" def __init__(self, http_client: HTTPClient, properties: Dict[str, Union[str, Any]]): self.client = http_client self._properties = properties metadata = self.fetch_sheet_metadata() self._properties.update(metadata["properties"]) @property def id(self) -> str: """Spreadsheet ID.""" return self._properties["id"] @property def title(self) -> str: """Spreadsheet title.""" return self._properties["title"] @property def url(self) -> str: """Spreadsheet URL.""" return SPREADSHEET_DRIVE_URL % self.id @property def creationTime(self) -> str: """Spreadsheet Creation time.""" if "createdTime" not in self._properties: self.update_drive_metadata() return self._properties["createdTime"] @property def lastUpdateTime(self) -> str: """Spreadsheet last updated time. Only updated on initialisation. For actual last updated time, use get_lastUpdateTime().""" warnings.warn( "worksheet.lastUpdateTime is deprecated, please use worksheet.get_lastUpdateTime()", category=DeprecationWarning, ) if "modifiedTime" not in self._properties: self.update_drive_metadata() return self._properties["modifiedTime"] @property def timezone(self) -> str: """Spreadsheet timeZone""" return self._properties["timeZone"] @property def locale(self) -> str: """Spreadsheet locale""" return self._properties["locale"] @property def sheet1(self) -> Worksheet: """Shortcut property for getting the first worksheet.""" return self.get_worksheet(0) def __iter__(self) -> Generator[Worksheet, None, None]: yield from self.worksheets() def __repr__(self) -> str: return "<{} {} id:{}>".format( self.__class__.__name__, repr(self.title), self.id, ) def batch_update(self, body: Mapping[str, Any]) -> Any: """Lower-level method that directly calls `spreadsheets/:batchUpdate `_. :param dict body: `Batch Update Request body `_. :returns: `Batch Update Response body `_. :rtype: dict .. versionadded:: 3.0 """ return self.client.batch_update(self.id, body) def values_append( self, range: str, params: ParamsType, body: Mapping[str, Any] ) -> Any: """Lower-level method that directly calls `spreadsheets//values:append `_. :param str range: The `A1 notation `_ of a range to search for a logical table of data. Values will be appended after the last row of the table. :param dict params: `Values Append Query parameters `_. :param dict body: `Values Append Request body `_. :returns: `Values Append Response body `_. :rtype: dict .. versionadded:: 3.0 """ return self.client.values_append(self.id, range, params, body) def values_clear(self, range: str) -> Any: """Lower-level method that directly calls `spreadsheets//values:clear `_. :param str range: The `A1 notation `_ of the values to clear. :returns: `Values Clear Response body `_. :rtype: dict .. versionadded:: 3.0 """ return self.client.values_clear(self.id, range) def values_batch_clear( self, params: Optional[ParamsType] = None, body: Optional[Mapping[str, Any]] = None, ) -> Any: """Lower-level method that directly calls `spreadsheets//values:batchClear` :param dict params: (optional) `Values Batch Clear Query parameters `_. :param dict body: (optional) `Values Batch Clear request body `_. :rtype: dict """ return self.client.values_batch_clear(self.id, params, body) def values_get(self, range: str, params: Optional[ParamsType] = None) -> Any: """Lower-level method that directly calls `GET spreadsheets//values/ `_. :param str range: The `A1 notation `_ of the values to retrieve. :param dict params: (optional) `Values Get Query parameters `_. :returns: `Values Get Response body `_. :rtype: dict .. versionadded:: 3.0 """ return self.client.values_get(self.id, range, params=params) def values_batch_get( self, ranges: List[str], params: Optional[ParamsType] = None ) -> Any: """Lower-level method that directly calls `spreadsheets//values:batchGet `_. :param list ranges: List of ranges in the `A1 notation `_ of the values to retrieve. :param dict params: (optional) `Values Batch Get Query parameters `_. :returns: `Values Batch Get Response body `_. :rtype: dict """ return self.client.values_batch_get(self.id, ranges, params=params) def values_update( self, range: str, params: Optional[ParamsType] = None, body: Optional[Mapping[str, Any]] = None, ) -> Any: """Lower-level method that directly calls `PUT spreadsheets//values/ `_. :param str range: The `A1 notation `_ of the values to update. :param dict params: (optional) `Values Update Query parameters `_. :param dict body: (optional) `Values Update Request body `_. :returns: `Values Update Response body `_. :rtype: dict Example:: sh.values_update( 'Sheet1!A2', params={ 'valueInputOption': 'USER_ENTERED' }, body={ 'values': [[1, 2, 3]] } ) .. versionadded:: 3.0 """ return self.client.values_update(self.id, range, params=params, body=body) def values_batch_update(self, body: Optional[Mapping[str, Any]] = None) -> Any: """Lower-level method that directly calls `spreadsheets//values:batchUpdate `_. :param dict body: (optional) `Values Batch Update Request body `_. :returns: `Values Batch Update Response body `_. :rtype: dict """ return self.client.values_batch_update(self.id, body=body) def _spreadsheets_get(self, params: Optional[ParamsType] = None) -> Any: """A method stub that directly calls `spreadsheets.get `_.""" return self.client.spreadsheets_get(self.id, params=params) def _spreadsheets_sheets_copy_to( self, sheet_id: int, destination_spreadsheet_id: str ) -> Any: """Lower-level method that directly calls `spreadsheets.sheets.copyTo `_.""" return self.client.spreadsheets_sheets_copy_to( self.id, sheet_id, destination_spreadsheet_id ) def fetch_sheet_metadata( self, params: Optional[ParamsType] = None ) -> Mapping[str, Any]: """Similar to :method spreadsheets_get:`gspread.http_client.spreadsheets_get`, get the spreadsheet form the API but by default **does not get the cells data**. It only retrieve the the metadata from the spreadsheet. :param dict params: (optional) the HTTP params for the GET request. By default sets the parameter ``includeGridData`` to ``false``. :returns: The raw spreadsheet :rtype: dict """ return self.client.fetch_sheet_metadata(self.id, params=params) def get_worksheet(self, index: int) -> Worksheet: """Returns a worksheet with specified `index`. :param index: An index of a worksheet. Indexes start from zero. :type index: int :returns: an instance of :class:`gspread.worksheet.Worksheet`. :raises: :class:`~gspread.exceptions.WorksheetNotFound`: if can't find the worksheet Example. To get third worksheet of a spreadsheet: >>> sht = client.open('My fancy spreadsheet') >>> worksheet = sht.get_worksheet(2) """ sheet_data = self.fetch_sheet_metadata() try: properties = sheet_data["sheets"][index]["properties"] return Worksheet(self, properties, self.id, self.client) except (KeyError, IndexError): raise WorksheetNotFound("index {} not found".format(index)) def get_worksheet_by_id(self, id: Union[str, int]) -> Worksheet: """Returns a worksheet with specified `worksheet id`. :param id: The id of a worksheet. it can be seen in the url as the value of the parameter 'gid'. :type id: str | int :returns: an instance of :class:`gspread.worksheet.Worksheet`. :raises: :class:`~gspread.exceptions.WorksheetNotFound`: if can't find the worksheet Example. To get the worksheet 123456 of a spreadsheet: >>> sht = client.open('My fancy spreadsheet') >>> worksheet = sht.get_worksheet_by_id(123456) """ sheet_data = self.fetch_sheet_metadata() try: worksheet_id_int = int(id) except ValueError as ex: raise ValueError("id should be int") from ex try: item = finditem( lambda x: x["properties"]["sheetId"] == worksheet_id_int, sheet_data["sheets"], ) return Worksheet(self, item["properties"], self.id, self.client) except (StopIteration, KeyError): raise WorksheetNotFound("id {} not found".format(worksheet_id_int)) def worksheets(self, exclude_hidden: bool = False) -> List[Worksheet]: """Returns a list of all :class:`worksheets ` in a spreadsheet. :param exclude_hidden: (optional) If set to ``True`` will only return visible worksheets. Default is ``False``. :type exclude_hidden: bool :returns: a list of :class:`worksheets `. :rtype: list """ sheet_data = self.fetch_sheet_metadata() worksheets = [ Worksheet(self, s["properties"], self.id, self.client) for s in sheet_data["sheets"] ] if exclude_hidden: worksheets = [w for w in worksheets if not w.isSheetHidden] return worksheets def worksheet(self, title: str) -> Worksheet: """Returns a worksheet with specified `title`. :param title: A title of a worksheet. If there're multiple worksheets with the same title, first one will be returned. :type title: str :returns: an instance of :class:`gspread.worksheet.Worksheet`. :raises: WorksheetNotFound: if can't find the worksheet Example. Getting worksheet named 'Annual bonuses' >>> sht = client.open('Sample one') >>> worksheet = sht.worksheet('Annual bonuses') """ sheet_data = self.fetch_sheet_metadata() try: item = finditem( lambda x: x["properties"]["title"] == title, sheet_data["sheets"], ) return Worksheet(self, item["properties"], self.id, self.client) except (StopIteration, KeyError): raise WorksheetNotFound(title) def add_worksheet( self, title: str, rows: int, cols: int, index: Optional[int] = None ) -> Worksheet: """Adds a new worksheet to a spreadsheet. :param title: A title of a new worksheet. :type title: str :param rows: Number of rows. :type rows: int :param cols: Number of columns. :type cols: int :param index: Position of the sheet. :type index: int :returns: a newly created :class:`worksheets `. """ body: Dict[ str, List[Dict[str, Dict[str, Dict[str, Union[str, int, Dict[str, int]]]]]] ] = { "requests": [ { "addSheet": { "properties": { "title": title, "sheetType": "GRID", "gridProperties": { "rowCount": rows, "columnCount": cols, }, } } } ] } if index is not None: body["requests"][0]["addSheet"]["properties"]["index"] = index data = self.client.batch_update(self.id, body) properties = data["replies"][0]["addSheet"]["properties"] return Worksheet(self, properties, self.id, self.client) def duplicate_sheet( self, source_sheet_id: int, insert_sheet_index: Optional[int] = None, new_sheet_id: Optional[int] = None, new_sheet_name: Optional[str] = None, ) -> Worksheet: """Duplicates the contents of a sheet. :param int source_sheet_id: The sheet ID to duplicate. :param int insert_sheet_index: (optional) The zero-based index where the new sheet should be inserted. The index of all sheets after this are incremented. :param int new_sheet_id: (optional) The ID of the new sheet. If not set, an ID is chosen. If set, the ID must not conflict with any existing sheet ID. If set, it must be non-negative. :param str new_sheet_name: (optional) The name of the new sheet. If empty, a new name is chosen for you. :returns: a newly created :class:`gspread.worksheet.Worksheet` .. versionadded:: 3.1 """ return Worksheet._duplicate( self.client, self.id, source_sheet_id, self, insert_sheet_index=insert_sheet_index, new_sheet_id=new_sheet_id, new_sheet_name=new_sheet_name, ) def del_worksheet(self, worksheet: Worksheet) -> Any: """Deletes a worksheet from a spreadsheet. :param worksheet: The worksheet to be deleted. :type worksheet: :class:`~gspread.worksheet.Worksheet` """ body = {"requests": [{"deleteSheet": {"sheetId": worksheet.id}}]} return self.client.batch_update(self.id, body) def del_worksheet_by_id(self, worksheet_id: Union[str, int]) -> Any: """ Deletes a Worksheet by id """ try: worksheet_id_int = int(worksheet_id) except ValueError as ex: raise ValueError("id should be int") from ex body = {"requests": [{"deleteSheet": {"sheetId": worksheet_id_int}}]} return self.client.batch_update(self.id, body) def reorder_worksheets( self, worksheets_in_desired_order: Iterable[Worksheet] ) -> Any: """Updates the ``index`` property of each Worksheet to reflect its index in the provided sequence of Worksheets. :param worksheets_in_desired_order: Iterable of Worksheet objects in desired order. Note: If you omit some of the Spreadsheet's existing Worksheet objects from the provided sequence, those Worksheets will be appended to the end of the sequence in the order that they appear in the list returned by :meth:`gspread.spreadsheet.Spreadsheet.worksheets`. .. versionadded:: 3.4 """ idx_map = {} for idx, w in enumerate(worksheets_in_desired_order): idx_map[w.id] = idx for w in self.worksheets(): if w.id in idx_map: continue idx += 1 idx_map[w.id] = idx body = { "requests": [ { "updateSheetProperties": { "properties": {"sheetId": key, "index": val}, "fields": "index", } } for key, val in idx_map.items() ] } return self.client.batch_update(self.id, body) def share( self, email_address: str, perm_type: str, role: str, notify: bool = True, email_message: Optional[str] = None, with_link: bool = False, ) -> Response: """Share the spreadsheet with other accounts. :param email_address: user or group e-mail address, domain name or None for 'anyone' type. :type email_address: str, None :param perm_type: The account type. Allowed values are: ``user``, ``group``, ``domain``, ``anyone``. :type perm_type: str :param role: The primary role for this user. Allowed values are: ``owner``, ``writer``, ``reader``. :type role: str :param notify: (optional) Whether to send an email to the target user/domain. :type notify: bool :param email_message: (optional) The email to be sent if notify=True :type email_message: str :param with_link: (optional) Whether the link is required for this permission :type with_link: bool Example:: # Give Otto a write permission on this spreadsheet sh.share('otto@example.com', perm_type='user', role='writer') # Give Otto's family a read permission on this spreadsheet sh.share('otto-familly@example.com', perm_type='group', role='reader') """ return self.client.insert_permission( self.id, email_address=email_address, perm_type=perm_type, role=role, notify=notify, email_message=email_message, with_link=with_link, ) def export(self, format: ExportFormat = ExportFormat.PDF) -> bytes: """Export the spreadsheet in the given format. :param str file_id: A key of a spreadsheet to export :param format: The format of the resulting file. Possible values are: ``ExportFormat.PDF``, ``ExportFormat.EXCEL``, ``ExportFormat.CSV``, ``ExportFormat.OPEN_OFFICE_SHEET``, ``ExportFormat.TSV``, and ``ExportFormat.ZIPPED_HTML``. See `ExportFormat`_ in the Drive API. Default value is ``ExportFormat.PDF``. :type format: :class:`~gspread.utils.ExportFormat` :returns bytes: The content of the exported file. .. _ExportFormat: https://developers.google.com/drive/api/guides/ref-export-formats """ return self.client.export(self.id, format) def list_permissions(self) -> List[Dict[str, Union[str, bool]]]: """Lists the spreadsheet's permissions.""" return self.client.list_permissions(self.id) def remove_permissions(self, value: str, role: str = "any") -> List[str]: """Remove permissions from a user or domain. :param value: User or domain to remove permissions from :type value: str :param role: (optional) Permission to remove. Defaults to all permissions. :type role: str Example:: # Remove Otto's write permission for this spreadsheet sh.remove_permissions('otto@example.com', role='writer') # Remove all Otto's permissions for this spreadsheet sh.remove_permissions('otto@example.com') """ permission_list = self.client.list_permissions(self.id) key = "emailAddress" if "@" in value else "domain" filtered_id_list: List[str] = [ str(p["id"]) for p in permission_list if p.get(key) == value and (p["role"] == role or role == "any") ] for permission_id in filtered_id_list: self.client.remove_permission(self.id, permission_id) return filtered_id_list def transfer_ownership(self, permission_id: str) -> Response: """Transfer the ownership of this file to a new user. It is necessary to first create the permission with the new owner's email address, get the permission ID then use this method to transfer the ownership. .. note:: You can list all permissions using :meth:`gspread.spreadsheet.Spreadsheet.list_permissions`. .. warning:: You can only transfer ownership to a new user, you cannot transfer ownership to a group or a domain email address. """ url = "{}/{}/permissions/{}".format( DRIVE_FILES_API_V3_URL, self.id, permission_id ) payload = { # new owner must be writer in order to accept ownership by editing permissions "role": "writer", "pendingOwner": True, } return self.client.request("patch", url, json=payload) def accept_ownership(self, permission_id: str) -> Response: """Accept the pending ownership request on that file. It is necessary to edit the permission with the pending ownership. .. note:: You can only accept ownership transfer for the user currently being used. """ url = "{}/{}/permissions/{}".format( DRIVE_FILES_API_V3_URL, self.id, permission_id, ) payload = { "role": "owner", } params: ParamsType = { "transferOwnership": True, } return self.client.request("patch", url, json=payload, params=params) def named_range(self, named_range: str) -> List[Cell]: """return a list of :class:`gspread.cell.Cell` objects from the specified named range. :param named_range: A string with a named range value to fetch. :type named_range: str """ # the function `range` does all necessary actions to get a named range. # This is only here to provide better user experience. return self.sheet1.range(named_range) def list_named_ranges(self) -> List[Any]: """Lists the spreadsheet's named ranges.""" return self.fetch_sheet_metadata(params={"fields": "namedRanges"}).get( "namedRanges", [] ) def update_title(self, title: str) -> Any: """Renames the spreadsheet. :param str title: A new title. """ body = { "requests": [ { "updateSpreadsheetProperties": { "properties": {"title": title}, "fields": "title", } } ] } res = self.batch_update(body) self._properties["title"] = title return res def update_timezone(self, timezone: str) -> Any: """Updates the current spreadsheet timezone. Can be any timezone in CLDR format such as "America/New_York" or a custom time zone such as GMT-07:00. """ body = { "requests": [ { "updateSpreadsheetProperties": { "properties": {"timeZone": timezone}, "fields": "timeZone", }, }, ] } res = self.batch_update(body) self._properties["timeZone"] = timezone return res def update_locale(self, locale: str) -> Any: """Update the locale of the spreadsheet. Can be any of the ISO 639-1 language codes, such as: de, fr, en, ... Or an ISO 639-2 if no ISO 639-1 exists. Or a combination of the ISO language code and country code, such as en_US, de_CH, fr_FR, ... .. note:: Note: when updating this field, not all locales/languages are supported. """ body = { "requests": [ { "updateSpreadsheetProperties": { "properties": {"locale": locale}, "fields": "locale", }, }, ] } res = self.batch_update(body) self._properties["locale"] = locale return res def list_protected_ranges(self, sheetid: int) -> List[Any]: """Lists the spreadsheet's protected named ranges""" sheets: List[Mapping[str, Any]] = self.fetch_sheet_metadata( params={"fields": "sheets.properties,sheets.protectedRanges"} )["sheets"] try: sheet = finditem( lambda sheet: sheet["properties"]["sheetId"] == sheetid, sheets ) except StopIteration: raise WorksheetNotFound("worksheet id {} not found".format(sheetid)) return sheet.get("protectedRanges", []) def get_lastUpdateTime(self) -> str: """Get the lastUpdateTime metadata from the Drive API.""" metadata = self.client.get_file_drive_metadata(self.id) return metadata["modifiedTime"] def update_drive_metadata(self) -> None: """Fetches the drive metadata from the Drive API and updates the cached values in _properties dict.""" drive_metadata = self.client.get_file_drive_metadata(self._properties["id"]) self._properties.update(drive_metadata) python-gspread-6.1.4/gspread/urls.py000066400000000000000000000024071472155066000174630ustar00rootroot00000000000000""" gspread.urls ~~~~~~~~~~~~ Google API urls. """ SPREADSHEETS_API_V4_BASE_URL: str = "https://sheets.googleapis.com/v4/spreadsheets" SPREADSHEET_URL: str = SPREADSHEETS_API_V4_BASE_URL + "/%s" SPREADSHEET_BATCH_UPDATE_URL: str = SPREADSHEETS_API_V4_BASE_URL + "/%s:batchUpdate" SPREADSHEET_VALUES_URL: str = SPREADSHEETS_API_V4_BASE_URL + "/%s/values/%s" SPREADSHEET_VALUES_BATCH_URL: str = SPREADSHEETS_API_V4_BASE_URL + "/%s/values:batchGet" SPREADSHEET_VALUES_BATCH_UPDATE_URL: str = ( SPREADSHEETS_API_V4_BASE_URL + "/%s/values:batchUpdate" ) SPREADSHEET_VALUES_BATCH_CLEAR_URL: str = ( SPREADSHEETS_API_V4_BASE_URL + "/%s/values:batchClear" ) SPREADSHEET_VALUES_APPEND_URL: str = SPREADSHEET_VALUES_URL + ":append" SPREADSHEET_VALUES_CLEAR_URL: str = SPREADSHEET_VALUES_URL + ":clear" SPREADSHEET_SHEETS_COPY_TO_URL: str = SPREADSHEET_URL + "/sheets/%s:copyTo" DRIVE_FILES_API_V3_URL: str = "https://www.googleapis.com/drive/v3/files" DRIVE_FILES_UPLOAD_API_V2_URL: str = ( "https://www.googleapis.com" "/upload/drive/v2/files" ) DRIVE_FILES_API_V3_COMMENTS_URL: str = ( "https://www.googleapis.com/drive/v3/files/%s/comments" ) SPREADSHEET_DRIVE_URL: str = "https://docs.google.com/spreadsheets/d/%s" WORKSHEET_DRIVE_URL = SPREADSHEET_DRIVE_URL + "#gid=%s" python-gspread-6.1.4/gspread/utils.py000066400000000000000000000675171472155066000176530ustar00rootroot00000000000000""" gspread.utils ~~~~~~~~~~~~~ This module contains utility functions. """ import enum import re from collections import defaultdict from collections.abc import Sequence from functools import wraps from itertools import chain from typing import ( TYPE_CHECKING, Any, AnyStr, Callable, Dict, Iterable, List, Mapping, Optional, Tuple, TypeVar, Union, ) from urllib.parse import quote as uquote from google.auth.credentials import Credentials as Credentials from google.oauth2.credentials import Credentials as UserCredentials from google.oauth2.service_account import Credentials as ServiceAccountCredentials from .exceptions import IncorrectCellLabel, InvalidInputValue, NoValidUrlKeyFound if TYPE_CHECKING: from .cell import Cell MAGIC_NUMBER = 64 CELL_ADDR_RE = re.compile(r"([A-Za-z]+)([1-9]\d*)") A1_ADDR_ROW_COL_RE = re.compile(r"([A-Za-z]+)?([1-9]\d*)?$") A1_ADDR_FULL_RE = re.compile(r"[A-Za-z]+\d+:[A-Za-z]+\d+") # e.g. A1:B2 not A1:B URL_KEY_V1_RE = re.compile(r"key=([^&#]+)") URL_KEY_V2_RE = re.compile(r"/spreadsheets/d/([a-zA-Z0-9-_]+)") class StrEnum(str, enum.Enum): def __new__(cls, value, *args, **kwargs): if not isinstance(value, (str, enum.auto)): raise TypeError( f"Values of StrEnums must be strings: {value!r} is a {type(value)}" ) return super().__new__(cls, value, *args, **kwargs) def __str__(self): return str(self.value) def _generate_next_value_(name, *_): return name class Dimension(StrEnum): rows = "ROWS" cols = "COLUMNS" class MergeType(StrEnum): merge_all = "MERGE_ALL" merge_columns = "MERGE_COLUMNS" merge_rows = "MERGE_ROWS" class ValueRenderOption(StrEnum): formatted = "FORMATTED_VALUE" unformatted = "UNFORMATTED_VALUE" formula = "FORMULA" class ValueInputOption(StrEnum): raw = "RAW" user_entered = "USER_ENTERED" class InsertDataOption(StrEnum): overwrite = "OVERWRITE" insert_rows = "INSERT_ROWS" class DateTimeOption(StrEnum): serial_number = "SERIAL_NUMBER" formatted_string = "FORMATTED_STRING" class MimeType(StrEnum): google_sheets = "application/vnd.google-apps.spreadsheet" pdf = "application/pdf" excel = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" csv = "text/csv" open_office_sheet = "application/vnd.oasis.opendocument.spreadsheet" tsv = "text/tab-separated-values" zip = "application/zip" class ExportFormat(StrEnum): PDF = MimeType.pdf EXCEL = MimeType.excel CSV = MimeType.csv OPEN_OFFICE_SHEET = MimeType.open_office_sheet TSV = MimeType.tsv ZIPPED_HTML = MimeType.zip class PasteType(StrEnum): normal = "PASTE_NORMAL" values = "PASTE_VALUES" format = "PASTE_FORMAT" # type: ignore no_borders = "PASTE_NO_BORDERS" formula = "PASTE_NO_BORDERS" data_validation = "PASTE_DATA_VALIDATION" conditional_formating = "PASTE_CONDITIONAL_FORMATTING" class PasteOrientation(StrEnum): normal = "NORMAL" transpose = "TRANSPOSE" class GridRangeType(StrEnum): ValueRange = "ValueRange" ListOfLists = "ListOfLists" class ValidationConditionType(StrEnum): number_greater = "NUMBER_GREATER" number_greater_than_eq = "NUMBER_GREATER_THAN_EQ" number_less = "NUMBER_LESS" number_less_than_eq = "NUMBER_LESS_THAN_EQ" number_eq = "NUMBER_EQ" number_not_eq = "NUMBER_NOT_EQ" number_between = "NUMBER_BETWEEN" number_not_between = "NUMBER_NOT_BETWEEN" text_contains = "TEXT_CONTAINS" text_not_contains = "TEXT_NOT_CONTAINS" text_starts_with = "TEXT_STARTS_WITH" text_ends_with = "TEXT_ENDS_WITH" text_eq = "TEXT_EQ" text_is_email = "TEXT_IS_EMAIL" text_is_url = "TEXT_IS_URL" date_eq = "DATE_EQ" date_before = "DATE_BEFORE" date_after = "DATE_AFTER" date_on_or_before = "DATE_ON_OR_BEFORE" date_on_or_after = "DATE_ON_OR_AFTER" date_between = "DATE_BETWEEN" date_not_between = "DATE_NOT_BETWEEN" date_is_valid = "DATE_IS_VALID" one_of_range = "ONE_OF_RANGE" one_of_list = "ONE_OF_LIST" blank = "BLANK" not_blank = "NOT_BLANK" custom_formula = "CUSTOM_FORMULA" boolean = "BOOLEAN" text_not_eq = "TEXT_NOT_EQ" date_not_eq = "DATE_NOT_EQ" filter_expression = "FILTER_EXPRESSION" def convert_credentials(credentials: Credentials) -> Credentials: module = credentials.__module__ cls = credentials.__class__.__name__ if "oauth2client" in module and cls == "ServiceAccountCredentials": return _convert_service_account(credentials) elif "oauth2client" in module and cls in ( "OAuth2Credentials", "AccessTokenCredentials", "GoogleCredentials", ): return _convert_oauth(credentials) elif isinstance(credentials, Credentials): return credentials raise TypeError( "Credentials need to be from either oauth2client or from google-auth." ) def _convert_oauth(credentials: Any) -> Credentials: return UserCredentials( credentials.access_token, credentials.refresh_token, credentials.id_token, credentials.token_uri, credentials.client_id, credentials.client_secret, credentials.scopes, ) def _convert_service_account(credentials: Any) -> Credentials: data = credentials.serialization_data data["token_uri"] = credentials.token_uri scopes = credentials._scopes.split() or [ "https://www.googleapis.com/auth/drive", "https://spreadsheets.google.com/feeds", ] return ServiceAccountCredentials.from_service_account_info(data, scopes=scopes) T = TypeVar("T") def finditem(func: Callable[[T], bool], seq: Iterable[T]) -> T: """Finds and returns first item in iterable for which func(item) is True.""" return next(item for item in seq if func(item)) def numericise( value: Optional[AnyStr], empty2zero: bool = False, default_blank: Any = "", allow_underscores_in_numeric_literals: bool = False, ) -> Optional[Union[int, float, AnyStr]]: """Returns a value that depends on the input: - Float if input is a string that can be converted to Float - Integer if input is a string that can be converted to integer - Zero if the input is a string that is empty and empty2zero flag is set - The unmodified input value, otherwise. Examples:: >>> numericise("faa") 'faa' >>> numericise("3") 3 >>> numericise("3_2", allow_underscores_in_numeric_literals=False) '3_2' >>> numericise("3_2", allow_underscores_in_numeric_literals=True) 32 >>> numericise("3.1") 3.1 >>> numericise("2,000.1") 2000.1 >>> numericise("", empty2zero=True) 0 >>> numericise("", empty2zero=False) '' >>> numericise("", default_blank=None) >>> >>> numericise("", default_blank="foo") 'foo' >>> numericise("") '' >>> numericise(None) >>> """ numericised: Optional[Union[int, float, AnyStr]] = value if isinstance(value, str): if "_" in value: if not allow_underscores_in_numeric_literals: return value value = value.replace("_", "") # replace comma separating thousands to match python format cleaned_value = value.replace(",", "") try: numericised = int(cleaned_value) except ValueError: try: numericised = float(cleaned_value) except ValueError: if value == "": if empty2zero: numericised = 0 else: numericised = default_blank return numericised def numericise_all( values: List[AnyStr], empty2zero: bool = False, default_blank: Any = "", allow_underscores_in_numeric_literals: bool = False, ignore: List[int] = [], ) -> List[Optional[Union[int, float, AnyStr]]]: """Returns a list of numericised values from strings except those from the row specified as ignore. :param list values: Input row :param bool empty2zero: (optional) Whether or not to return empty cells as 0 (zero). Defaults to ``False``. :param str default_blank: Which value to use for blank cells, defaults to empty string. :param bool allow_underscores_in_numeric_literals: Whether or not to allow visual underscores in numeric literals :param list ignore: List of ints of indices of the row (index 1) to ignore numericising. """ # in case someone explicitly passes `None` as ignored list ignore = ignore or [] numericised_list = [ ( values[index] if index + 1 in ignore else numericise( values[index], empty2zero=empty2zero, default_blank=default_blank, allow_underscores_in_numeric_literals=allow_underscores_in_numeric_literals, ) ) for index in range(len(values)) ] return numericised_list def rowcol_to_a1(row: int, col: int) -> str: """Translates a row and column cell address to A1 notation. :param row: The row of the cell to be converted. Rows start at index 1. :type row: int, str :param col: The column of the cell to be converted. Columns start at index 1. :type row: int, str :returns: a string containing the cell's coordinates in A1 notation. Example: >>> rowcol_to_a1(1, 1) A1 """ if row < 1 or col < 1: raise IncorrectCellLabel("({}, {})".format(row, col)) div = col column_label = "" while div: (div, mod) = divmod(div, 26) if mod == 0: mod = 26 div -= 1 column_label = chr(mod + MAGIC_NUMBER) + column_label label = "{}{}".format(column_label, row) return label def a1_to_rowcol(label: str) -> Tuple[int, int]: """Translates a cell's address in A1 notation to a tuple of integers. :param str label: A cell label in A1 notation, e.g. 'B1'. Letter case is ignored. :returns: a tuple containing `row` and `column` numbers. Both indexed from 1 (one). :rtype: tuple Example: >>> a1_to_rowcol('A1') (1, 1) """ m = CELL_ADDR_RE.match(label) if m: column_label = m.group(1).upper() row = int(m.group(2)) col = 0 for i, c in enumerate(reversed(column_label)): col += (ord(c) - MAGIC_NUMBER) * (26**i) else: raise IncorrectCellLabel(label) return (row, col) IntOrInf = Union[int, float] def _a1_to_rowcol_unbounded(label: str) -> Tuple[IntOrInf, IntOrInf]: """Translates a cell's address in A1 notation to a tuple of integers. Same as `a1_to_rowcol()` but allows for missing row or column part (e.g. "A" for the first column) :returns: a tuple containing `row` and `column` numbers. Both indexed from 1 (one). :rtype: tuple Example: >>> _a1_to_rowcol_unbounded('A1') (1, 1) >>> _a1_to_rowcol_unbounded('A') (inf, 1) >>> _a1_to_rowcol_unbounded('1') (1, inf) >>> _a1_to_rowcol_unbounded('ABC123') (123, 731) >>> _a1_to_rowcol_unbounded('ABC') (inf, 731) >>> _a1_to_rowcol_unbounded('123') (123, inf) >>> _a1_to_rowcol_unbounded('1A') Traceback (most recent call last): ... gspread.exceptions.IncorrectCellLabel: 1A >>> _a1_to_rowcol_unbounded('') (inf, inf) """ m = A1_ADDR_ROW_COL_RE.match(label) if m: column_label, row = m.groups() col: IntOrInf if column_label: col = 0 for i, c in enumerate(reversed(column_label.upper())): col += (ord(c) - MAGIC_NUMBER) * (26**i) else: col = float("inf") if row: row = int(row) else: row = float("inf") else: raise IncorrectCellLabel(label) return (row, col) def a1_range_to_grid_range(name: str, sheet_id: Optional[int] = None) -> Dict[str, int]: """Converts a range defined in A1 notation to a dict representing a `GridRange`_. All indexes are zero-based. Indexes are half open, e.g the start index is inclusive and the end index is exclusive: [startIndex, endIndex). Missing indexes indicate the range is unbounded on that side. .. _GridRange: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#GridRange Examples:: >>> a1_range_to_grid_range('A1:A1') {'startRowIndex': 0, 'endRowIndex': 1, 'startColumnIndex': 0, 'endColumnIndex': 1} >>> a1_range_to_grid_range('A3:B4') {'startRowIndex': 2, 'endRowIndex': 4, 'startColumnIndex': 0, 'endColumnIndex': 2} >>> a1_range_to_grid_range('A:B') {'startColumnIndex': 0, 'endColumnIndex': 2} >>> a1_range_to_grid_range('A5:B') {'startRowIndex': 4, 'startColumnIndex': 0, 'endColumnIndex': 2} >>> a1_range_to_grid_range('A1') {'startRowIndex': 0, 'endRowIndex': 1, 'startColumnIndex': 0, 'endColumnIndex': 1} >>> a1_range_to_grid_range('A') {'startColumnIndex': 0, 'endColumnIndex': 1} >>> a1_range_to_grid_range('1') {'startRowIndex': 0, 'endRowIndex': 1} >>> a1_range_to_grid_range('A1', sheet_id=0) {'sheetId': 0, 'startRowIndex': 0, 'endRowIndex': 1, 'startColumnIndex': 0, 'endColumnIndex': 1} """ start_label, _, end_label = name.partition(":") start_row_index, start_column_index = _a1_to_rowcol_unbounded(start_label) end_row_index, end_column_index = _a1_to_rowcol_unbounded(end_label or start_label) if start_row_index > end_row_index: start_row_index, end_row_index = end_row_index, start_row_index if start_column_index > end_column_index: start_column_index, end_column_index = end_column_index, start_column_index grid_range = { "startRowIndex": start_row_index - 1, "endRowIndex": end_row_index, "startColumnIndex": start_column_index - 1, "endColumnIndex": end_column_index, } filtered_grid_range: Dict[str, int] = { key: value for (key, value) in grid_range.items() if isinstance(value, int) } if sheet_id is not None: filtered_grid_range["sheetId"] = sheet_id return filtered_grid_range def column_letter_to_index(column: str) -> int: """Converts a column letter to its numerical index. This is useful when using the method :meth:`gspread.worksheet.Worksheet.col_values`. Which requires a column index. This function is case-insensitive. Raises :exc:`gspread.exceptions.InvalidInputValue` in case of invalid input. Examples:: >>> column_letter_to_index("a") 1 >>> column_letter_to_index("A") 1 >>> column_letter_to_index("AZ") 52 >>> column_letter_to_index("!@#$%^&") ... gspread.exceptions.InvalidInputValue: invalid value: !@#$%^&, must be a column letter """ try: (_, index) = _a1_to_rowcol_unbounded(column) except IncorrectCellLabel: # make it coherent and raise the same exception in case of any error # from user input value raise InvalidInputValue( "invalid value: {}, must be a column letter".format(column) ) if not isinstance(index, int): raise InvalidInputValue( "invalid value: {}, must be a column letter".format(column) ) return index def cast_to_a1_notation(method: Callable[..., T]) -> Callable[..., T]: """Decorator function casts wrapped arguments to A1 notation in range method calls. """ def contains_row_cols(args: Tuple[Any, ...]) -> bool: return ( isinstance(args[0], int) and isinstance(args[1], int) and isinstance(args[2], int) and isinstance(args[3], int) ) @wraps(method) def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: try: if len(args) >= 4 and contains_row_cols(args): # Convert to A1 notation # Assuming rowcol_to_a1 has appropriate typing range_start = rowcol_to_a1(*args[:2]) # Assuming rowcol_to_a1 has appropriate typing range_end = rowcol_to_a1(*args[2:4]) range_name = ":".join((range_start, range_end)) args = (range_name,) + args[4:] except ValueError: pass return method(self, *args, **kwargs) return wrapper def extract_id_from_url(url: str) -> str: m2 = URL_KEY_V2_RE.search(url) if m2: return m2.group(1) m1 = URL_KEY_V1_RE.search(url) if m1: return m1.group(1) raise NoValidUrlKeyFound def wid_to_gid(wid: str) -> str: """Calculate gid of a worksheet from its wid.""" widval = wid[1:] if len(wid) > 3 else wid xorval = 474 if len(wid) > 3 else 31578 return str(int(widval, 36) ^ xorval) def rightpad(row: List[Any], max_len: int, padding_value: Any = "") -> List[Any]: pad_len = max_len - len(row) return row + ([padding_value] * pad_len) if pad_len != 0 else row def fill_gaps( L: List[List[Any]], rows: Optional[int] = None, cols: Optional[int] = None, padding_value: Any = "", ) -> List[List[Any]]: """Fill gaps in a list of lists. e.g.,:: >>> L = [ ... [1, 2, 3], ... ] >>> fill_gaps(L, 2, 4) [ [1, 2, 3, ""], ["", "", "", ""] ] :param L: List of lists to fill gaps in. :param rows: Number of rows to fill. :param cols: Number of columns to fill. :param padding_value: Default value to fill gaps with. :type L: list[list[T]] :type rows: int :type cols: int :type padding_value: T :return: List of lists with gaps filled. :rtype: list[list[T]]: """ try: max_cols = max(len(row) for row in L) if cols is None else cols max_rows = len(L) if rows is None else rows pad_rows = max_rows - len(L) if pad_rows: L = L + ([[]] * pad_rows) return [rightpad(row, max_cols, padding_value=padding_value) for row in L] except ValueError: return [[]] def cell_list_to_rect(cell_list: List["Cell"]) -> List[List[Optional[str]]]: if not cell_list: return [] rows: Dict[int, Dict[int, Optional[str]]] = defaultdict(dict) row_offset = min(c.row for c in cell_list) col_offset = min(c.col for c in cell_list) for cell in cell_list: row = rows.setdefault(int(cell.row) - row_offset, {}) row[cell.col - col_offset] = cell.value if not rows: return [] all_row_keys = chain.from_iterable(row.keys() for row in rows.values()) rect_cols = range(max(all_row_keys) + 1) rect_rows = range(max(rows.keys()) + 1) # Return the values of the cells as a list of lists where each sublist # contains all of the values for one row. The Google API requires a rectangle # of updates, so if a cell isn't present in the input cell_list, then the # value will be None and will not be updated. return [[rows[i].get(j) for j in rect_cols] for i in rect_rows] def quote(value: str, safe: str = "", encoding: str = "utf-8") -> str: return uquote(value.encode(encoding), safe) def absolute_range_name(sheet_name: str, range_name: Optional[str] = None) -> str: """Return an absolutized path of a range. >>> absolute_range_name("Sheet1", "A1:B1") "'Sheet1'!A1:B1" >>> absolute_range_name("Sheet1", "A1") "'Sheet1'!A1" >>> absolute_range_name("Sheet1") "'Sheet1'" >>> absolute_range_name("Sheet'1") "'Sheet''1'" >>> absolute_range_name("Sheet''1") "'Sheet''''1'" >>> absolute_range_name("''sheet12''", "A1:B2") "'''''sheet12'''''!A1:B2" """ sheet_name = "'{}'".format(sheet_name.replace("'", "''")) if range_name: return "{}!{}".format(sheet_name, range_name) else: return sheet_name def is_scalar(x: Any) -> bool: """Return True if the value is scalar. A scalar is not a sequence but can be a string. >>> is_scalar([]) False >>> is_scalar([1, 2]) False >>> is_scalar(42) True >>> is_scalar('nice string') True >>> is_scalar({}) True >>> is_scalar(set()) True """ return isinstance(x, str) or not isinstance(x, Sequence) def combined_merge_values( worksheet_metadata: Mapping[str, Any], values: List[List[Any]], start_row_index: int, start_col_index: int, ) -> List[List[Any]]: """For each merged region, replace all values with the value of the top-left cell of the region. e.g., replaces [ [1, None, None], [None, None, None], ] with [ [1, 1, None], [1, 1, None], ] if the top-left four cells are merged. :param worksheet_metadata: The metadata returned by the Google API for the worksheet. Should have a "merges" key. :param values: The values returned by the Google API for the worksheet. 2D array. :param start_row_index: The index of the first row of the values in the worksheet. e.g., if the values are in rows 3-5, this should be 2. :param start_col_index: The index of the first column of the values in the worksheet. e.g., if the values are in columns C-E, this should be 2. :returns: matrix of values with merged coordinates filled according to top-left value :rtype: list(list(any)) """ merges = worksheet_metadata.get("merges", []) # each merge has "startRowIndex", "endRowIndex", "startColumnIndex", "endColumnIndex new_values = [list(row) for row in values] # max row and column indices max_row_index = len(values) - 1 max_col_index = len(values[0]) - 1 for merge in merges: merge_start_row, merge_end_row = merge["startRowIndex"], merge["endRowIndex"] merge_start_col, merge_end_col = ( merge["startColumnIndex"], merge["endColumnIndex"], ) # subtract offset merge_start_row -= start_row_index merge_end_row -= start_row_index merge_start_col -= start_col_index merge_end_col -= start_col_index # if out of bounds, ignore if merge_start_row > max_row_index or merge_start_col > max_col_index: continue if merge_start_row < 0 or merge_start_col < 0: continue top_left_value = values[merge_start_row][merge_start_col] row_indices = range(merge_start_row, merge_end_row) col_indices = range(merge_start_col, merge_end_col) for row_index in row_indices: for col_index in col_indices: # if out of bounds, ignore if row_index > max_row_index or col_index > max_col_index: continue new_values[row_index][col_index] = top_left_value return new_values def convert_hex_to_colors_dict(hex_color: str) -> Mapping[str, float]: """Convert a hex color code to RGB color values. :param str hex_color: Hex color code in the format "#RRGGBB". :returns: Dict containing the color's red, green and blue values between 0 and 1. :rtype: dict :raises: ValueError: If the input hex string is not in the correct format or length. Examples: >>> convert_hex_to_colors_dict("#3300CC") {'red': 0.2, 'green': 0.0, 'blue': 0.8} >>> convert_hex_to_colors_dict("#30C") {'red': 0.2, 'green': 0.0, 'blue': 0.8} """ hex_color = hex_color.lstrip("#") # Google API ColorStyle Reference: # "The alpha value in the Color object isn't generally supported." # https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#colorstyle if len(hex_color) == 8: hex_color = hex_color[:-2] # Expand 3 character hex. if len(hex_color) == 3: hex_color = "".join([char * 2 for char in hex_color]) if len(hex_color) != 6: raise ValueError("Hex color code must be in the format '#RRGGBB'.") try: rgb_color = { "red": int(hex_color[0:2], 16) / 255, "green": int(hex_color[2:4], 16) / 255, "blue": int(hex_color[4:6], 16) / 255, } return rgb_color except ValueError as ex: raise ValueError(f"Invalid character in hex color string: #{hex_color}") from ex def convert_colors_to_hex_value( red: float = 0.0, green: float = 0.0, blue: float = 0.0 ) -> str: """Convert RGB color values to a hex color code. :param float red: Red color value (0-1). :param float green: Green color value (0-1). :param float blue: Blue color value (0-1). :returns: Hex color code in the format "#RRGGBB". :rtype: str :raises: ValueError: If any color value is out of the accepted range (0-1). Example: >>> convert_colors_to_hex_value(0.2, 0, 0.8) '#3300CC' >>> convert_colors_to_hex_value(green=0.5) '#008000' """ def to_hex(value: float) -> str: """ Convert an integer to a 2-digit uppercase hex string. """ hex_value = hex(round(value * 255))[2:] return hex_value.upper().zfill(2) if any(value < 0 or value > 1 for value in (red, green, blue)): raise ValueError("Color value out of accepted range 0-1.") return f"#{to_hex(red)}{to_hex(green)}{to_hex(blue)}" def is_full_a1_notation(range_name: str) -> bool: """Check if the range name is a full A1 notation. "A1:B2", "Sheet1!A1:B2" are full A1 notations "A1:B", "A1" are not Args: range_name (str): The range name to check. Returns: bool: True if the range name is a full A1 notation, False otherwise. Examples: >>> is_full_a1_notation("A1:B2") True >>> is_full_a1_notation("A1:B") False """ return A1_ADDR_FULL_RE.search(range_name) is not None def get_a1_from_absolute_range(range_name: str) -> str: """Get the A1 notation from an absolute range name. "Sheet1!A1:B2" -> "A1:B2" "A1:B2" -> "A1:B2" Args: range_name (str): The range name to check. Returns: str: The A1 notation of the range name stripped of the sheet. """ if "!" in range_name: return range_name.split("!")[1] return range_name def to_records( headers: Iterable[Any] = [], values: Iterable[Iterable[Any]] = [[]] ) -> List[Dict[str, Union[str, int, float]]]: """Builds the list of dictionaries, all of them have the headers sequence as keys set, each key is associated to the corresponding value for the same index in each list from the matrix ``values``. There are as many dictionaries as they are entry in the list of given values. :param list: headers the key set for all dictionaries :param list: values a matrix of values Examples:: >>> to_records(["name", "City"], [["Spiderman", "NY"], ["Batman", "Gotham"]]) [ { "Name": "Spiderman", "City": "NY", }, { "Name": "Batman", "City": "Gotham", }, ] """ return [dict(zip(headers, row)) for row in values] # SHOULD NOT BE NEEDED UNTIL NEXT MAJOR VERSION # DEPRECATION_WARNING_TEMPLATE = ( # "[Deprecated][in version {v_deprecated}]: {msg_deprecated}" # ) # SILENCE_WARNINGS_ENV_KEY = "GSPREAD_SILENCE_WARNINGS" # def deprecation_warning(version: str, msg: str) -> None: # """Emit a deprecation warning. # ..note:: # This warning can be silenced by setting the environment variable: # GSPREAD_SILENCE_WARNINGS=1 # """ # # do not emit warning if env variable is set specifically to 1 # if os.getenv(SILENCE_WARNINGS_ENV_KEY, "0") == "1": # return # warnings.warn( # DEPRECATION_WARNING_TEMPLATE.format(v_deprecated=version, msg_deprecated=msg), # DeprecationWarning, # 4, # showd the 4th stack: [1]:current->[2]:deprecation_warning->[3]:->[4]: # ) if __name__ == "__main__": import doctest doctest.testmod() python-gspread-6.1.4/gspread/worksheet.py000066400000000000000000003524031472155066000205150ustar00rootroot00000000000000""" gspread.worksheet ~~~~~~~~~~~~~~~~~ This module contains common worksheets' models. """ import re import warnings from typing import ( TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Literal, Mapping, MutableMapping, Optional, Sequence, Tuple, Type, TypedDict, TypeVar, Union, ) from .cell import Cell from .exceptions import GSpreadException from .http_client import HTTPClient, ParamsType from .urls import WORKSHEET_DRIVE_URL from .utils import ( DateTimeOption, Dimension, GridRangeType, InsertDataOption, MergeType, PasteOrientation, PasteType, T, ValidationConditionType, ValueInputOption, ValueRenderOption, a1_range_to_grid_range, a1_to_rowcol, absolute_range_name, cast_to_a1_notation, cell_list_to_rect, combined_merge_values, convert_colors_to_hex_value, convert_hex_to_colors_dict, fill_gaps, finditem, get_a1_from_absolute_range, is_full_a1_notation, numericise_all, rowcol_to_a1, to_records, ) if TYPE_CHECKING is True: from .spreadsheet import Spreadsheet CellFormat = TypedDict( "CellFormat", { "range": str, "format": Mapping[str, Any], }, ) BatchData = TypedDict("BatchData", {"range": str, "values": List[List[Any]]}) JSONResponse = MutableMapping[str, Any] ValueRangeType = TypeVar("ValueRangeType", bound="ValueRange") class ValueRange(list): """The class holds the returned values. This class inherit the :const:`list` object type. It behaves exactly like a list. The values are stored in a matrix. The property :meth:`gspread.worksheet.ValueRange.major_dimension` holds the major dimension of the first list level. The inner lists will contain the actual values. Examples:: >>> worksheet.get("A1:B2") [ [ "A1 value", "B1 values", ], [ "A2 value", "B2 value", ] ] >>> worksheet.get("A1:B2").major_dimension ROW .. note:: This class should never be instantiated manually. It will be instantiated using the response from the sheet API. """ _json: MutableMapping[str, str] = {} @classmethod def from_json(cls: Type[ValueRangeType], json: Mapping[str, Any]) -> ValueRangeType: values = json.get("values", []) new_obj = cls(values) new_obj._json = { "range": json["range"], "majorDimension": json["majorDimension"], } return new_obj @property def range(self) -> str: """The range of the values""" return self._json["range"] @property def major_dimension(self) -> str: """The major dimension of this range Can be one of: * ``ROW``: the first list level holds rows of values * ``COLUMNS``: the first list level holds columns of values """ return self._json["majorDimension"] def first(self, default: Optional[str] = None) -> Optional[str]: """Returns the value of a first cell in a range. If the range is empty, return the default value. """ try: return self[0][0] except IndexError: return default class Worksheet: """The class that represents a single sheet in a spreadsheet (aka "worksheet"). """ def __init__( self, spreadsheet: "Spreadsheet", properties: MutableMapping[str, Any], spreadsheet_id: Optional[str] = None, client: Optional[HTTPClient] = None, ): # This object is not intended to be created manually # only using gspread code like: spreadsheet.get_worksheet(0) # keep it backward compatible signarure but raise with explicit message # in case of missing new attributes if spreadsheet_id is None or "": raise RuntimeError( """Missing spreadsheet_id parameter, it must be provided with a valid spreadsheet ID. Please allocate new Worksheet object using method like: spreadsheet.get_worksheet(0) """ ) if client is None or not isinstance(client, HTTPClient): raise RuntimeError( """Missing HTTP Client, it must be provided with a valid instance of type gspread.http_client.HTTPClient . Please allocate new Worksheet object using method like: spreadsheet.get_worksheet(0) """ ) self.spreadsheet_id = spreadsheet_id self.client = client self._properties = properties # kept for backward compatibility - publicly available # do not use if possible. self._spreadsheet = spreadsheet def __repr__(self) -> str: return "<{} {} id:{}>".format( self.__class__.__name__, repr(self.title), self.id, ) @property def id(self) -> int: """Worksheet ID.""" return self._properties["sheetId"] @property def spreadsheet(self) -> "Spreadsheet": """Parent spreadsheet""" return self._spreadsheet @property def title(self) -> str: """Worksheet title.""" return self._properties["title"] @property def url(self) -> str: """Worksheet URL.""" return WORKSHEET_DRIVE_URL % (self.spreadsheet_id, self.id) @property def index(self) -> int: """Worksheet index.""" return self._properties["index"] @property def isSheetHidden(self) -> bool: """Worksheet hidden status.""" # if the property is not set then hidden=False return self._properties.get("hidden", False) @property def row_count(self) -> int: """Number of rows.""" return self._properties["gridProperties"]["rowCount"] @property def col_count(self) -> int: """Number of columns. .. warning:: This value is fetched when opening the worksheet. This is not dynamically updated when adding columns, yet. """ return self._properties["gridProperties"]["columnCount"] @property def column_count(self) -> int: """Number of columns""" return self.col_count @property def frozen_row_count(self) -> int: """Number of frozen rows.""" return self._properties["gridProperties"].get("frozenRowCount", 0) @property def frozen_col_count(self) -> int: """Number of frozen columns.""" return self._properties["gridProperties"].get("frozenColumnCount", 0) @property def is_gridlines_hidden(self) -> bool: """Whether or not gridlines hidden. Boolean. True if hidden. False if shown. """ return self._properties["gridProperties"].get("hideGridlines", False) @property def tab_color(self) -> Optional[str]: """Tab color style. Hex with RGB color values.""" return self.get_tab_color() def get_tab_color(self) -> Optional[str]: """Tab color style in hex format. String.""" tab_color = self._properties.get("tabColorStyle", {}).get("rgbColor", None) if tab_color is None: return None return convert_colors_to_hex_value(**tab_color) def _get_sheet_property(self, property: str, default_value: Optional[T]) -> T: """return a property of this worksheet or default value if not found""" meta = self.client.fetch_sheet_metadata(self.spreadsheet_id) sheet = finditem( lambda x: x["properties"]["sheetId"] == self.id, meta["sheets"] ) return sheet.get(property, default_value) def acell( self, label: str, value_render_option: ValueRenderOption = ValueRenderOption.formatted, ) -> Cell: """Returns an instance of a :class:`gspread.cell.Cell`. :param label: Cell label in A1 notation Letter case is ignored. :type label: str :param value_render_option: (optional) Determines how values should be rendered in the output. See `ValueRenderOption`_ in the Sheets API. :type value_render_option: :class:`~gspread.utils.ValueRenderOption` .. _ValueRenderOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption Example: >>> worksheet.acell('A1') """ return self.cell( *(a1_to_rowcol(label)), value_render_option=value_render_option ) def cell( self, row: int, col: int, value_render_option: ValueRenderOption = ValueRenderOption.formatted, ) -> Cell: """Returns an instance of a :class:`gspread.cell.Cell` located at `row` and `col` column. :param row: Row number. :type row: int :param col: Column number. :type col: int :param value_render_option: (optional) Determines how values should be rendered in the output. See `ValueRenderOption`_ in the Sheets API. :type value_render_option: :class:`~gspread.utils.ValueRenderOption` .. _ValueRenderOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption Example: >>> worksheet.cell(1, 1) :rtype: :class:`gspread.cell.Cell` """ try: data = self.get( rowcol_to_a1(row, col), value_render_option=value_render_option, return_type=GridRangeType.ValueRange, ) # we force a return type to GridRangeType.ValueRange # help typing tool to see it too :-) if isinstance(data, ValueRange): value = data.first() else: raise RuntimeError("returned data must be of type ValueRange") except KeyError: value = "" return Cell(row, col, value) @cast_to_a1_notation def range(self, name: str = "") -> List[Cell]: """Returns a list of :class:`gspread.cell.Cell` objects from a specified range. :param name: A string with range value in A1 notation (e.g. 'A1:A5') or the named range to fetch. :type name: str Alternatively, you may specify numeric boundaries. All values index from 1 (one): :param int first_row: First row number :param int first_col: First column number :param int last_row: Last row number :param int last_col: Last column number :rtype: list Example:: >>> # Using A1 notation >>> worksheet.range('A1:B7') [, ...] >>> # Same with numeric boundaries >>> worksheet.range(1, 1, 7, 2) [, ...] >>> # Named ranges work as well >>> worksheet.range('NamedRange') [, ...] >>> # All values in a single API call >>> worksheet.range() [, ...] """ range_label = absolute_range_name(self.title, name) data = self.client.values_get(self.spreadsheet_id, range_label) if ":" not in name: name = data.get("range", "") if "!" in name: name = name.split("!")[1] grid_range = a1_range_to_grid_range(name) values = data.get("values", []) row_offset = grid_range.get("startRowIndex", 0) column_offset = grid_range.get("startColumnIndex", 0) last_row = grid_range.get("endRowIndex", self.row_count) last_column = grid_range.get("endColumnIndex", self.col_count) if last_row is not None: last_row -= row_offset if last_column is not None: last_column -= column_offset rect_values = fill_gaps( values, rows=last_row, cols=last_column, ) return [ Cell(row=i + row_offset + 1, col=j + column_offset + 1, value=value) for i, row in enumerate(rect_values) for j, value in enumerate(row) ] def get_values( self, range_name: Optional[str] = None, major_dimension: Optional[Dimension] = None, value_render_option: Optional[ValueRenderOption] = None, date_time_render_option: Optional[DateTimeOption] = None, combine_merged_cells: bool = False, maintain_size: bool = False, pad_values: bool = True, return_type: GridRangeType = GridRangeType.ListOfLists, ) -> Union[ValueRange, List[List[Any]]]: """Alias for :meth:`~gspread.worksheet.Worksheet.get`... with ``return_type`` set to ``List[List[Any]]`` and ``pad_values`` set to ``True`` (legacy method) """ return self.get( range_name=range_name, major_dimension=major_dimension, value_render_option=value_render_option, date_time_render_option=date_time_render_option, combine_merged_cells=combine_merged_cells, maintain_size=maintain_size, pad_values=pad_values, return_type=return_type, ) def get_all_values( self, range_name: Optional[str] = None, major_dimension: Optional[Dimension] = None, value_render_option: Optional[ValueRenderOption] = None, date_time_render_option: Optional[DateTimeOption] = None, combine_merged_cells: bool = False, maintain_size: bool = False, pad_values: bool = True, return_type: GridRangeType = GridRangeType.ListOfLists, ) -> Union[ValueRange, List[List[Any]]]: """Alias to :meth:`~gspread.worksheet.Worksheet.get_values`""" return self.get_values( range_name=range_name, major_dimension=major_dimension, value_render_option=value_render_option, date_time_render_option=date_time_render_option, combine_merged_cells=combine_merged_cells, maintain_size=maintain_size, pad_values=pad_values, return_type=return_type, ) def get_all_records( self, head: int = 1, expected_headers: Optional[List[str]] = None, value_render_option: Optional[ValueRenderOption] = None, default_blank: str = "", numericise_ignore: Iterable[Union[str, int]] = [], allow_underscores_in_numeric_literals: bool = False, empty2zero: bool = False, ) -> List[Dict[str, Union[int, float, str]]]: """Returns a list of dictionaries, all of them having the contents of the spreadsheet with the head row as keys and each of these dictionaries holding the contents of subsequent rows of cells as values. This method uses the function :func:`gspread.utils.to_records` to build the resulting records. It mainly wraps around the function and handle the simplest use case using a header row (default = 1) and the the reste of the entire sheet. .. note:: for any particular use-case, please get your dataset, your headers then use the function :func:`gspread.utils.to_records` to build the records. Cell values are numericised (strings that can be read as ints or floats are converted), unless specified in numericise_ignore :param int head: (optional) Determines which row to use as keys, starting from 1 following the numeration of the spreadsheet. :param list expected_headers: (optional) List of expected headers, they must be unique. .. note:: returned dictionaries will contain all headers even if not included in this list :param value_render_option: (optional) Determines how values should be rendered in the output. See `ValueRenderOption`_ in the Sheets API. :type value_render_option: :class:`~gspread.utils.ValueRenderOption` :param str default_blank: (optional) Determines which value to use for blank cells, defaults to empty string. :param list numericise_ignore: (optional) List of ints of indices of the columns (starting at 1) to ignore numericising, special use of ['all'] to ignore numericising on all columns. :param bool allow_underscores_in_numeric_literals: (optional) Allow underscores in numeric literals, as introduced in PEP 515 :param bool empty2zero: (optional) Determines whether empty cells are converted to zeros when numericised, defaults to False. Examples:: # Sheet data: # A B C # # 1 A1 B2 C3 # 2 A6 B7 C8 # 3 A11 B12 C13 # Read all rows from the sheet >>> worksheet.get_all_records() [ {"A1": "A6", "B2": "B7", "C3": "C8"}, {"A1": "A11", "B2": "B12", "C3": "C13"} ] """ entire_sheet = self.get( value_render_option=value_render_option, pad_values=True, ) if entire_sheet == [[]]: # see test_get_all_records_with_all_values_blank # we don't know the length of the sheet so we return [] return [] keys = entire_sheet[head - 1] values = entire_sheet[head:] if expected_headers is None: # all headers must be unique header_row_is_unique = len(keys) == len(set(keys)) if not header_row_is_unique: raise GSpreadException( "the header row in the worksheet is not unique, " "try passing 'expected_headers' to get_all_records" ) else: # all expected headers must be unique expected_headers_are_unique = len(expected_headers) == len( set(expected_headers) ) if not expected_headers_are_unique: raise GSpreadException("the given 'expected_headers' are not uniques") # expected headers must be a subset of the actual headers if not all(header in keys for header in expected_headers): raise GSpreadException( "the given 'expected_headers' contains unknown headers: " f"{set(expected_headers) - set(keys)}" ) if numericise_ignore == ["all"]: pass else: values = [ numericise_all( row, empty2zero, default_blank, allow_underscores_in_numeric_literals, numericise_ignore, # type: ignore ) for row in values ] return to_records(keys, values) def get_all_cells(self) -> List[Cell]: """Returns a list of all `Cell` of the current sheet.""" return self.range() def row_values( self, row: int, major_dimension: Optional[Dimension] = None, value_render_option: Optional[ValueRenderOption] = None, date_time_render_option: Optional[DateTimeOption] = None, ) -> List[str]: """Returns a list of all values in a `row`. Empty cells in this list will be rendered as :const:`None`. :param int row: Row number (one-based). :param str major_dimension: (optional) The major dimension of the values. `Dimension.rows` ("ROWS") or `Dimension.cols` ("COLUMNS"). Defaults to Dimension.rows :type major_dimension: :class:`~gspread.utils.Dimension` :param value_render_option: (optional) Determines how values should be rendered in the output. See `ValueRenderOption`_ in the Sheets API. Possible values are: ``ValueRenderOption.formatted`` (default) Values will be calculated and formatted according to the cell's formatting. Formatting is based on the spreadsheet's locale, not the requesting user's locale. ``ValueRenderOption.unformatted`` Values will be calculated, but not formatted in the reply. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return the number 1.23. ``ValueRenderOption.formula`` Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1". .. _ValueRenderOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption :type value_render_option: :class:`~gspread.utils.ValueRenderOption` :param date_time_render_option: (optional) How dates, times, and durations should be represented in the output. Possible values are: ``DateTimeOption.serial_number`` (default) Instructs date, time, datetime, and duration fields to be output as doubles in "serial number" format, as popularized by Lotus 1-2-3. ``DateTimeOption.formatted_string`` Instructs date, time, datetime, and duration fields to be output as strings in their given number format (which depends on the spreadsheet locale). .. note:: This is ignored if ``value_render_option`` is ``ValueRenderOption.formatted``. The default ``date_time_render_option`` is ``DateTimeOption.serial_number``. :type date_time_render_option: :class:`~gspread.utils.DateTimeOption` """ try: data = self.get( "A{}:{}".format(row, row), major_dimension, value_render_option, date_time_render_option, ) return data[0] if data else [] except KeyError: return [] def col_values( self, col: int, value_render_option: ValueRenderOption = ValueRenderOption.formatted, ) -> List[Optional[Union[int, float, str]]]: """Returns a list of all values in column `col`. Empty cells in this list will be rendered as :const:`None`. :param int col: Column number (one-based). :param str value_render_option: (optional) Determines how values should be rendered in the output. See `ValueRenderOption`_ in the Sheets API. :type value_render_option: :class:`~gspread.utils.ValueRenderOption` .. _ValueRenderOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption """ start_label = rowcol_to_a1(1, col) range_label = "{}:{}".format(start_label, start_label[:-1]) range_name = absolute_range_name(self.title, range_label) data = self.client.values_get( self.spreadsheet_id, range_name, params={ "valueRenderOption": value_render_option, "majorDimension": Dimension.cols, }, ) try: return data["values"][0] except KeyError: return [] def update_acell(self, label: str, value: Union[int, float, str]) -> JSONResponse: """Updates the value of a cell. :param str label: Cell label in A1 notation. :param value: New value. Example:: worksheet.update_acell('A1', '42') """ return self.update_cell(*(a1_to_rowcol(label)), value=value) def update_cell( self, row: int, col: int, value: Union[int, float, str] ) -> JSONResponse: """Updates the value of a cell. :param int row: Row number. :param int col: Column number. :param value: New value. Example:: worksheet.update_cell(1, 1, '42') """ range_name = absolute_range_name(self.title, rowcol_to_a1(row, col)) data = self.client.values_update( self.spreadsheet_id, range_name, params={"valueInputOption": ValueInputOption.user_entered}, body={"values": [[value]]}, ) return data def update_cells( self, cell_list: List[Cell], value_input_option: ValueInputOption = ValueInputOption.raw, ) -> Mapping[str, Any]: """Updates many cells at once. :param list cell_list: List of :class:`gspread.cell.Cell` objects to update. :param value_input_option: (optional) How the input data should be interpreted. Possible values are: ``ValueInputOption.raw`` (default) The values the user has entered will not be parsed and will be stored as-is. ``ValueInputOption.user_entered`` The values will be parsed as if the user typed them into the UI. Numbers will stay as numbers, but strings may be converted to numbers, dates, etc. following the same rules that are applied when entering text into a cell via the Google Sheets UI. See `ValueInputOption`_ in the Sheets API. :type value_input_option: :namedtuple:`~gspread.utils.ValueInputOption` .. _ValueInputOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption Example:: # Select a range cell_list = worksheet.range('A1:C7') for cell in cell_list: cell.value = 'O_o' # Update in batch worksheet.update_cells(cell_list) """ values_rect = cell_list_to_rect(cell_list) start = rowcol_to_a1( min(c.row for c in cell_list), min(c.col for c in cell_list) ) end = rowcol_to_a1(max(c.row for c in cell_list), max(c.col for c in cell_list)) range_name = absolute_range_name(self.title, "{}:{}".format(start, end)) data = self.client.values_update( self.spreadsheet_id, range_name, params={"valueInputOption": value_input_option}, body={"values": values_rect}, ) return data def get( self, range_name: Optional[str] = None, major_dimension: Optional[Dimension] = None, value_render_option: Optional[ValueRenderOption] = None, date_time_render_option: Optional[DateTimeOption] = None, combine_merged_cells: bool = False, maintain_size: bool = False, pad_values: bool = False, return_type: GridRangeType = GridRangeType.ValueRange, ) -> Union[ValueRange, List[List[str]]]: """Reads values of a single range or a cell of a sheet. Returns a ValueRange (list of lists) containing all values from a specified range or cell By default values are returned as strings. See ``value_render_option`` to change the default format. :param str range_name: (optional) Cell range in the A1 notation or a named range. If not specified the method returns values from all non empty cells. :param str major_dimension: (optional) The major dimension of the values. `Dimension.rows` ("ROWS") or `Dimension.cols` ("COLUMNS"). Defaults to Dimension.rows :type major_dimension: :class:`~gspread.utils.Dimension` :param value_render_option: (optional) Determines how values should be rendered in the output. See `ValueRenderOption`_ in the Sheets API. Possible values are: ``ValueRenderOption.formatted`` (default) Values will be calculated and formatted according to the cell's formatting. Formatting is based on the spreadsheet's locale, not the requesting user's locale. ``ValueRenderOption.unformatted`` Values will be calculated, but not formatted in the reply. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return the number 1.23. ``ValueRenderOption.formula`` Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1". .. _ValueRenderOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption :type value_render_option: :class:`~gspread.utils.ValueRenderOption` :param str date_time_render_option: (optional) How dates, times, and durations should be represented in the output. Possible values are: ``DateTimeOption.serial_number`` (default) Instructs date, time, datetime, and duration fields to be output as doubles in "serial number" format, as popularized by Lotus 1-2-3. ``DateTimeOption.formatted_string`` Instructs date, time, datetime, and duration fields to be output as strings in their given number format (which depends on the spreadsheet locale). .. note:: This is ignored if ``value_render_option`` is ``ValueRenderOption.formatted``. The default ``date_time_render_option`` is ``DateTimeOption.serial_number``. :type date_time_render_option: :class:`~gspread.utils.DateTimeOption` :param bool combine_merged_cells: (optional) If True, then all cells that are part of a merged cell will have the same value as the top-left cell of the merged cell. Defaults to False. .. warning:: Setting this to True will cause an additional API request to be made to retrieve the values of all merged cells. :param bool maintain_size: (optional) If True, then the returned values will have the same size as the requested range_name. Defaults to False. :param bool pad_values: (optional) If True, then empty cells will be filled with empty strings. Defaults to False. .. warning:: The returned array will not be rectangular unless this is set to True. If this is a problem, see also `maintain_size`. :param GridRangeType return_type: (optional) The type of object to return. Defaults to :class:`gspread.utils.GridRangeType.ValueRange`. The other option is `gspread.utils.GridRangeType.ListOfLists`. :rtype: :class:`gspread.worksheet.ValueRange` .. versionadded:: 3.3 Examples:: # Return all values from the sheet worksheet.get() # Return value of 'A1' cell worksheet.get('A1') # Return values of 'A1:B2' range worksheet.get('A1:B2') # Return all values from columns "A" and "B" worksheet.get('A:B') # Return values of 'my_range' named range worksheet.get('my_range') # Return unformatted values (e.g. numbers as numbers) worksheet.get('A2:B4', value_render_option=ValueRenderOption.unformatted) # Return cell values without calculating formulas worksheet.get('A2:B4', value_render_option=ValueRenderOption.formula) """ # do not override the given range name with the build up range name for the actual request get_range_name = absolute_range_name(self.title, range_name) params: ParamsType = { "majorDimension": major_dimension, "valueRenderOption": value_render_option, "dateTimeRenderOption": date_time_render_option, } response = self.client.values_get( self.spreadsheet_id, get_range_name, params=params ) values = response.get("values", [[]]) if pad_values is True: try: values = fill_gaps(values) except KeyError: values = [[]] if combine_merged_cells is True: spreadsheet_meta = self.client.fetch_sheet_metadata(self.spreadsheet_id) worksheet_meta = finditem( lambda x: x["properties"]["title"] == self.title, spreadsheet_meta["sheets"], ) # deal with named ranges named_ranges = spreadsheet_meta.get("namedRanges", []) # if there is a named range with the name range_name if any( range_name == ss_namedRange["name"] for ss_namedRange in named_ranges if ss_namedRange.get("name") ): ss_named_range = finditem( lambda x: x["name"] == range_name, named_ranges ) grid_range = ss_named_range.get("range", {}) # norrmal range_name, i.e., A1:B2 elif range_name is not None: a1 = get_a1_from_absolute_range(range_name) grid_range = a1_range_to_grid_range(a1) # no range_name, i.e., all values else: grid_range = worksheet_meta.get("basicFilter", {}).get("range", {}) values = combined_merge_values( worksheet_metadata=worksheet_meta, values=values, start_row_index=grid_range.get("startRowIndex", 0), start_col_index=grid_range.get("startColumnIndex", 0), ) # In case range_name is None range_name = range_name or "" # range_name must be a full grid range so that we can guarantee # startRowIndex and endRowIndex properties if maintain_size is True and is_full_a1_notation(range_name): a1_range = get_a1_from_absolute_range(range_name) grid_range = a1_range_to_grid_range(a1_range) rows = grid_range["endRowIndex"] - grid_range["startRowIndex"] cols = grid_range["endColumnIndex"] - grid_range["startColumnIndex"] values = fill_gaps(values, rows=rows, cols=cols) if return_type is GridRangeType.ValueRange: response["values"] = values return ValueRange.from_json(response) if return_type is GridRangeType.ListOfLists: return values raise ValueError("return_type must be either ValueRange or ListOfLists") def batch_get( self, ranges: Iterable[str], major_dimension: Optional[Dimension] = None, value_render_option: Optional[ValueRenderOption] = None, date_time_render_option: Optional[DateTimeOption] = None, ) -> List[ValueRange]: """Returns one or more ranges of values from the sheet. :param list ranges: List of cell ranges in the A1 notation or named ranges. :param str major_dimension: (optional) The major dimension of the values. `Dimension.rows` ("ROWS") or `Dimension.cols` ("COLUMNS"). Defaults to Dimension.rows :type major_dimension: :class:`~gspread.utils.Dimension` :param value_render_option: (optional) Determines how values should be rendered in the output. See `ValueRenderOption`_ in the Sheets API. Possible values are: ``ValueRenderOption.formatted`` (default) Values will be calculated and formatted according to the cell's formatting. Formatting is based on the spreadsheet's locale, not the requesting user's locale. ``ValueRenderOption.unformatted`` Values will be calculated, but not formatted in the reply. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return the number 1.23. ``ValueRenderOption.formula`` Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1". .. _ValueRenderOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption :type value_render_option: :class:`~gspread.utils.ValueRenderOption` :param str date_time_render_option: (optional) How dates, times, and durations should be represented in the output. Possible values are: ``DateTimeOption.serial_number`` (default) Instructs date, time, datetime, and duration fields to be output as doubles in "serial number" format, as popularized by Lotus 1-2-3. ``DateTimeOption.formatted_string`` Instructs date, time, datetime, and duration fields to be output as strings in their given number format (which depends on the spreadsheet locale). .. note:: This is ignored if ``value_render_option`` is ``ValueRenderOption.formatted``. The default ``date_time_render_option`` is ``DateTimeOption.serial_number``. :type date_time_render_option: :class:`~gspread.utils.DateTimeOption` .. versionadded:: 3.3 Examples:: # Read values from 'A1:B2' range and 'F12' cell worksheet.batch_get(['A1:B2', 'F12']) """ ranges = [absolute_range_name(self.title, r) for r in ranges if r] params: ParamsType = { "majorDimension": major_dimension, "valueRenderOption": value_render_option, "dateTimeRenderOption": date_time_render_option, } response = self.client.values_batch_get( self.spreadsheet_id, ranges=ranges, params=params ) return [ValueRange.from_json(x) for x in response["valueRanges"]] def update( self, values: Iterable[Iterable[Any]], range_name: Optional[str] = None, raw: bool = True, major_dimension: Optional[Dimension] = None, value_input_option: Optional[ValueInputOption] = None, include_values_in_response: Optional[bool] = None, response_value_render_option: Optional[ValueRenderOption] = None, response_date_time_render_option: Optional[DateTimeOption] = None, ) -> JSONResponse: """Sets values in a cell range of the sheet. :param list values: The data to be written in a matrix format. :param str range_name: (optional) The A1 notation of the values to update. :param bool raw: The values will not be parsed by Sheets API and will be stored as-is. For example, formulas will be rendered as plain strings. Defaults to ``True``. This is a shortcut for the ``value_input_option`` parameter. :param str major_dimension: (optional) The major dimension of the values. `Dimension.rows` ("ROWS") or `Dimension.cols` ("COLUMNS"). Defaults to Dimension.rows :type major_dimension: :class:`~gspread.utils.Dimension` :param str value_input_option: (optional) How the input data should be interpreted. Possible values are: ``ValueInputOption.raw`` (default) The values the user has entered will not be parsed and will be stored as-is. ``ValueInputOption.user_entered`` The values will be parsed as if the user typed them into the UI. Numbers will stay as numbers, but strings may be converted to numbers, dates, etc. following the same rules that are applied when entering text into a cell via the Google Sheets UI. :type value_input_option: :class:`~gspread.utils.ValueInputOption` :param response_value_render_option: (optional) Determines how values should be rendered in the output. See `ValueRenderOption`_ in the Sheets API. Possible values are: ``ValueRenderOption.formatted`` (default) Values will be calculated and formatted according to the cell's formatting. Formatting is based on the spreadsheet's locale, not the requesting user's locale. ``ValueRenderOption.unformatted`` Values will be calculated, but not formatted in the reply. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return the number 1.23. ``ValueRenderOption.formula`` Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1". .. _ValueRenderOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption :type response_value_render_option: :class:`~gspread.utils.ValueRenderOption` :param str response_date_time_render_option: (optional) How dates, times, and durations should be represented in the output. Possible values are: ``DateTimeOption.serial_number`` (default) Instructs date, time, datetime, and duration fields to be output as doubles in "serial number" format, as popularized by Lotus 1-2-3. ``DateTimeOption.formatted_string`` Instructs date, time, datetime, and duration fields to be output as strings in their given number format (which depends on the spreadsheet locale). .. note:: This is ignored if ``value_render_option`` is ``ValueRenderOption.formatted``. The default ``date_time_render_option`` is ``DateTimeOption.serial_number``. :type date_time_render_option: :class:`~gspread.utils.DateTimeOption` Examples:: # Sets 'Hello world' in 'A2' cell worksheet.update([['Hello world']], 'A2') # Updates cells A1, B1, C1 with values 42, 43, 44 respectively worksheet.update([[42, 43, 44]]) # Updates A2 and A3 with values 42 and 43 # Note that update range can be bigger than values array worksheet.update([[42], [43]], 'A2:B4') # Add a formula worksheet.update([['=SUM(A1:A4)']], 'A5', raw=False) # Update 'my_range' named range with values 42 and 43 worksheet.update([[42], [43]], 'my_range') # Note: named ranges are defined in the scope of # a spreadsheet, so even if `my_range` does not belong to # this sheet it is still updated .. versionadded:: 3.3 """ if isinstance(range_name, (list, tuple)) and isinstance(values, str): warnings.warn( "The order of arguments in worksheet.update() has changed. " "Please pass values first and range_name second" "or used named arguments (range_name=, values=)", DeprecationWarning, stacklevel=2, ) range_name, values = values, range_name full_range_name = absolute_range_name(self.title, range_name) if not value_input_option: value_input_option = ( ValueInputOption.raw if raw is True else ValueInputOption.user_entered ) params: ParamsType = { "valueInputOption": value_input_option, "includeValuesInResponse": include_values_in_response, "responseValueRenderOption": response_value_render_option, "responseDateTimeRenderOption": response_date_time_render_option, } response = self.client.values_update( self.spreadsheet_id, full_range_name, params=params, body={"values": values, "majorDimension": major_dimension}, ) return response def batch_update( self, data: Iterable[MutableMapping[str, Any]], raw: bool = True, value_input_option: Optional[ValueInputOption] = None, include_values_in_response: Optional[bool] = None, response_value_render_option: Optional[ValueRenderOption] = None, response_date_time_render_option: Optional[DateTimeOption] = None, ) -> JSONResponse: """Sets values in one or more cell ranges of the sheet at once. :param list data: List of dictionaries in the form of `{'range': '...', 'values': [[.., ..], ...]}` where `range` is a target range to update in A1 notation or a named range, and `values` is a list of lists containing new values. :param str value_input_option: (optional) How the input data should be interpreted. Possible values are: * ``ValueInputOption.raw`` The values the user has entered will not be parsed and will be stored as-is. * ``ValueInputOption.user_entered`` The values will be parsed as if the user typed them into the UI. Numbers will stay as numbers, but strings may be converted to numbers, dates, etc. following the same rules that are applied when entering text into a cell via the Google Sheets UI. :type value_input_option: :class:`~gspread.utils.ValueInputOption` :param response_value_render_option: (optional) Determines how values should be rendered in the output. See `ValueRenderOption`_ in the Sheets API. Possible values are: ``ValueRenderOption.formatted`` (default) Values will be calculated and formatted according to the cell's formatting. Formatting is based on the spreadsheet's locale, not the requesting user's locale. ``ValueRenderOption.unformatted`` Values will be calculated, but not formatted in the reply. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return the number 1.23. ``ValueRenderOption.formula`` Values will not be calculated. The reply will include the formulas. For example, if A1 is 1.23 and A2 is =A1 and formatted as currency, then A2 would return "=A1". .. _ValueRenderOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueRenderOption :type response_value_render_option: :class:`~gspread.utils.ValueRenderOption` :param str response_date_time_render_option: (optional) How dates, times, and durations should be represented in the output. Possible values are: ``DateTimeOption.serial_number`` (default) Instructs date, time, datetime, and duration fields to be output as doubles in "serial number" format, as popularized by Lotus 1-2-3. ``DateTimeOption.formatted_string`` Instructs date, time, datetime, and duration fields to be output as strings in their given number format (which depends on the spreadsheet locale). .. note:: This is ignored if ``value_render_option`` is ``ValueRenderOption.formatted``. The default ``date_time_render_option`` is ``DateTimeOption.serial_number``. :type date_time_render_option: :class:`~gspread.utils.DateTimeOption` Examples:: worksheet.batch_update([{ 'range': 'A1:B1', 'values': [['42', '43']], }, { 'range': 'my_range', 'values': [['44', '45']], }]) # Note: named ranges are defined in the scope of # a spreadsheet, so even if `my_range` does not belong to # this sheet it is still updated .. versionadded:: 3.3 """ if not value_input_option: value_input_option = ( ValueInputOption.raw if raw is True else ValueInputOption.user_entered ) for values in data: values["range"] = absolute_range_name(self.title, values["range"]) body: MutableMapping[str, Any] = { "valueInputOption": value_input_option, "includeValuesInResponse": include_values_in_response, "responseValueRenderOption": response_value_render_option, "responseDateTimeRenderOption": response_date_time_render_option, "data": data, } response = self.client.values_batch_update(self.spreadsheet_id, body=body) return response def batch_format(self, formats: List[CellFormat]) -> JSONResponse: """Formats cells in batch. :param list formats: List of ranges to format and the new format to apply to each range. The list is composed of dict objects with the following keys/values: * range : A1 range notation * format : a valid dict object with the format to apply for that range see `CellFormat`_ in the Sheets API for available fields. .. _CellFormat: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#cellformat Examples:: # Format the range ``A1:C1`` with bold text # and format the range ``A2:C2`` a font size of 16 formats = [ { "range": "A1:C1", "format": { "textFormat": { "bold": True, }, }, }, { "range": "A2:C2", "format": { "textFormat": { "fontSize": 16, }, }, }, ] worksheet.batch_format(formats) .. versionadded:: 5.4 """ # No need to type more than that it's only internal to that method body: Dict[str, Any] = { "requests": [], } for format in formats: range_name = format["range"] cell_format = format["format"] grid_range = a1_range_to_grid_range(range_name, self.id) fields = "userEnteredFormat(%s)" % ",".join(cell_format.keys()) body["requests"].append( { "repeatCell": { "range": grid_range, "cell": {"userEnteredFormat": cell_format}, "fields": fields, } } ) return self.client.batch_update(self.spreadsheet_id, body) def format( self, ranges: Union[List[str], str], format: JSONResponse ) -> JSONResponse: """Format a list of ranges with the given format. :param str|list ranges: Target ranges in the A1 notation. :param dict format: Dictionary containing the fields to update. See `CellFormat`_ in the Sheets API for available fields. Examples:: # Set 'A4' cell's text format to bold worksheet.format("A4", {"textFormat": {"bold": True}}) # Set 'A1:D4' and 'A10:D10' cells's text format to bold worksheet.format(["A1:D4", "A10:D10"], {"textFormat": {"bold": True}}) # Color the background of 'A2:B2' cell range in black, # change horizontal alignment, text color and font size worksheet.format("A2:B2", { "backgroundColor": { "red": 0.0, "green": 0.0, "blue": 0.0 }, "horizontalAlignment": "CENTER", "textFormat": { "foregroundColor": { "red": 1.0, "green": 1.0, "blue": 1.0 }, "fontSize": 12, "bold": True } }) .. versionadded:: 3.3 """ if isinstance(ranges, list): range_list = ranges else: range_list = [ranges] formats = [CellFormat(range=range, format=format) for range in range_list] return self.batch_format(formats) def resize( self, rows: Optional[int] = None, cols: Optional[int] = None ) -> JSONResponse: """Resizes the worksheet. Specify one of ``rows`` or ``cols``. :param int rows: (optional) New number of rows. :param int cols: (optional) New number columns. """ grid_properties = {} if rows is not None: grid_properties["rowCount"] = rows if cols is not None: grid_properties["columnCount"] = cols if not grid_properties: raise TypeError("Either 'rows' or 'cols' should be specified.") fields = ",".join("gridProperties/%s" % p for p in grid_properties.keys()) body = { "requests": [ { "updateSheetProperties": { "properties": { "sheetId": self.id, "gridProperties": grid_properties, }, "fields": fields, } } ] } res = self.client.batch_update(self.spreadsheet_id, body) if rows is not None: self._properties["gridProperties"]["rowCount"] = rows if cols is not None: self._properties["gridProperties"]["columnCount"] = cols return res def sort( self, *specs: Tuple[int, Literal["asc", "des"]], range: Optional[str] = None ) -> JSONResponse: """Sorts worksheet using given sort orders. :param list specs: The sort order per column. Each sort order represented by a tuple where the first element is a column index and the second element is the order itself: 'asc' or 'des'. :param str range: The range to sort in A1 notation. By default sorts the whole sheet excluding frozen rows. Example:: # Sort sheet A -> Z by column 'B' wks.sort((2, 'asc')) # Sort range A2:G8 basing on column 'G' A -> Z # and column 'B' Z -> A wks.sort((7, 'asc'), (2, 'des'), range='A2:G8') .. versionadded:: 3.4 """ if range: start_a1, end_a1 = range.split(":") start_row, start_col = a1_to_rowcol(start_a1) end_row, end_col = a1_to_rowcol(end_a1) else: start_row = self._properties["gridProperties"].get("frozenRowCount", 0) + 1 start_col = 1 end_row = self.row_count end_col = self.col_count request_range = { "sheetId": self.id, "startRowIndex": start_row - 1, "endRowIndex": end_row, "startColumnIndex": start_col - 1, "endColumnIndex": end_col, } request_sort_specs = list() for col, order in specs: if order == "asc": request_order = "ASCENDING" elif order == "des": request_order = "DESCENDING" else: raise ValueError( "Either 'asc' or 'des' should be specified as sort order." ) request_sort_spec = { "dimensionIndex": col - 1, "sortOrder": request_order, } request_sort_specs.append(request_sort_spec) body = { "requests": [ { "sortRange": { "range": request_range, "sortSpecs": request_sort_specs, } } ] } response = self.client.batch_update(self.spreadsheet_id, body) return response def update_title(self, title: str) -> JSONResponse: """Renames the worksheet. :param str title: A new title. """ body = { "requests": [ { "updateSheetProperties": { "properties": {"sheetId": self.id, "title": title}, "fields": "title", } } ] } response = self.client.batch_update(self.spreadsheet_id, body) self._properties["title"] = title return response def update_tab_color(self, color: str) -> JSONResponse: """Changes the worksheet's tab color. Use clear_tab_color() to remove the color. :param str color: Hex color value. """ color_dict = convert_hex_to_colors_dict(color) body = { "requests": [ { "updateSheetProperties": { "properties": { "sheetId": self.id, "tabColorStyle": { "rgbColor": color_dict, }, }, "fields": "tabColorStyle", } } ] } response = self.client.batch_update(self.spreadsheet_id, body) self._properties["tabColorStyle"] = {"rgbColor": color_dict} return response def clear_tab_color(self) -> JSONResponse: """Clears the worksheet's tab color. Use update_tab_color() to set the color. """ body = { "requests": [ { "updateSheetProperties": { "properties": { "sheetId": self.id, "tabColorStyle": { "rgbColor": None, }, }, "fields": "tabColorStyle", }, }, ], } response = self.client.batch_update(self.spreadsheet_id, body) self._properties.pop("tabColorStyle") return response def update_index(self, index: int) -> JSONResponse: """Updates the ``index`` property for the worksheet. See the `Sheets API documentation `_ for information on how updating the index property affects the order of worksheets in a spreadsheet. To reorder all worksheets in a spreadsheet, see `Spreadsheet.reorder_worksheets`. .. versionadded:: 3.4 """ body = { "requests": [ { "updateSheetProperties": { "properties": {"sheetId": self.id, "index": index}, "fields": "index", } } ] } res = self.client.batch_update(self.spreadsheet_id, body) self._properties["index"] = index return res def _auto_resize( self, start_index: int, end_index: int, dimension: Dimension ) -> JSONResponse: """Updates the size of rows or columns in the worksheet. Index start from 0 :param start_index: The index (inclusive) to begin resizing :param end_index: The index (exclusive) to finish resizing :param dimension: Specifies whether to resize the row or column :type major_dimension: :class:`~gspread.utils.Dimension` .. versionadded:: 5.3.3 """ body = { "requests": [ { "autoResizeDimensions": { "dimensions": { "sheetId": self.id, "dimension": dimension, "startIndex": start_index, "endIndex": end_index, } } } ] } return self.client.batch_update(self.spreadsheet_id, body) def columns_auto_resize( self, start_column_index: int, end_column_index: int ) -> JSONResponse: """Updates the size of rows or columns in the worksheet. Index start from 0 :param start_column_index: The index (inclusive) to begin resizing :param end_column_index: The index (exclusive) to finish resizing .. versionadded:: 3.4 .. versionchanged:: 5.3.3 """ return self._auto_resize(start_column_index, end_column_index, Dimension.cols) def rows_auto_resize( self, start_row_index: int, end_row_index: int ) -> JSONResponse: """Updates the size of rows or columns in the worksheet. Index start from 0 :param start_row_index: The index (inclusive) to begin resizing :param end_row_index: The index (exclusive) to finish resizing .. versionadded:: 5.3.3 """ return self._auto_resize(start_row_index, end_row_index, Dimension.rows) def add_rows(self, rows: int) -> None: """Adds rows to worksheet. :param rows: Number of new rows to add. :type rows: int """ self.resize(rows=self.row_count + rows) def add_cols(self, cols: int) -> None: """Adds columns to worksheet. :param cols: Number of new columns to add. :type cols: int """ self.resize(cols=self.col_count + cols) def append_row( self, values: Sequence[Union[str, int, float]], value_input_option: ValueInputOption = ValueInputOption.raw, insert_data_option: Optional[InsertDataOption] = None, table_range: Optional[str] = None, include_values_in_response: bool = False, ) -> JSONResponse: """Adds a row to the worksheet and populates it with values. Widens the worksheet if there are more values than columns. :param list values: List of values for the new row. :param value_input_option: (optional) Determines how the input data should be interpreted. See `ValueInputOption`_ in the Sheets API reference. :type value_input_option: :class:`~gspread.utils.ValueInputOption` :param str insert_data_option: (optional) Determines how the input data should be inserted. See `InsertDataOption`_ in the Sheets API reference. :param str table_range: (optional) The A1 notation of a range to search for a logical table of data. Values are appended after the last row of the table. Examples: ``A1`` or ``B2:D4`` :param bool include_values_in_response: (optional) Determines if the update response should include the values of the cells that were appended. By default, responses do not include the updated values. .. _ValueInputOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption .. _InsertDataOption: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append#InsertDataOption """ return self.append_rows( [values], value_input_option=value_input_option, insert_data_option=insert_data_option, table_range=table_range, include_values_in_response=include_values_in_response, ) def append_rows( self, values: Sequence[Sequence[Union[str, int, float]]], value_input_option: ValueInputOption = ValueInputOption.raw, insert_data_option: Optional[InsertDataOption] = None, table_range: Optional[str] = None, include_values_in_response: Optional[bool] = None, ) -> JSONResponse: """Adds multiple rows to the worksheet and populates them with values. Widens the worksheet if there are more values than columns. :param list values: List of rows each row is List of values for the new row. :param value_input_option: (optional) Determines how input data should be interpreted. Possible values are ``ValueInputOption.raw`` or ``ValueInputOption.user_entered``. See `ValueInputOption`_ in the Sheets API. :type value_input_option: :class:`~gspread.utils.ValueInputOption` :param str insert_data_option: (optional) Determines how the input data should be inserted. See `InsertDataOption`_ in the Sheets API reference. :param str table_range: (optional) The A1 notation of a range to search for a logical table of data. Values are appended after the last row of the table. Examples: ``A1`` or ``B2:D4`` :param bool include_values_in_response: (optional) Determines if the update response should include the values of the cells that were appended. By default, responses do not include the updated values. .. _ValueInputOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption .. _InsertDataOption: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/append#InsertDataOption """ range_label = absolute_range_name(self.title, table_range) params: ParamsType = { "valueInputOption": value_input_option, "insertDataOption": insert_data_option, "includeValuesInResponse": include_values_in_response, } body = {"values": values} res = self.client.values_append(self.spreadsheet_id, range_label, params, body) num_new_rows = len(values) self._properties["gridProperties"]["rowCount"] += num_new_rows return res def insert_row( self, values: Sequence[Union[str, int, float]], index: int = 1, value_input_option: ValueInputOption = ValueInputOption.raw, inherit_from_before: bool = False, ) -> JSONResponse: """Adds a row to the worksheet at the specified index and populates it with values. Widens the worksheet if there are more values than columns. :param list values: List of values for the new row. :param int index: (optional) Offset for the newly inserted row. :param str value_input_option: (optional) Determines how input data should be interpreted. Possible values are ``ValueInputOption.raw`` or ``ValueInputOption.user_entered``. See `ValueInputOption`_ in the Sheets API. :type value_input_option: :class:`~gspread.utils.ValueInputOption` :param bool inherit_from_before: (optional) If True, the new row will inherit its properties from the previous row. Defaults to False, meaning that the new row acquires the properties of the row immediately after it. .. warning:: `inherit_from_before` must be False when adding a row to the top of a spreadsheet (`index=1`), and must be True when adding to the bottom of the spreadsheet. .. _ValueInputOption: https://developers.google.com/sheets/api/reference/rest/v4/ValueInputOption """ return self.insert_rows( [values], index, value_input_option=value_input_option, inherit_from_before=inherit_from_before, ) def insert_rows( self, values: Sequence[Sequence[Union[str, int, float]]], row: int = 1, value_input_option: ValueInputOption = ValueInputOption.raw, inherit_from_before: bool = False, ) -> JSONResponse: """Adds multiple rows to the worksheet at the specified index and populates them with values. :param list values: List of row lists. a list of lists, with the lists each containing one row's values. Widens the worksheet if there are more values than columns. :param int row: Start row to update (one-based). Defaults to 1 (one). :param str value_input_option: (optional) Determines how input data should be interpreted. Possible values are ``ValueInputOption.raw`` or ``ValueInputOption.user_entered``. See `ValueInputOption`_ in the Sheets API. :type value_input_option: :class:`~gspread.utils.ValueInputOption` :param bool inherit_from_before: (optional) If true, new rows will inherit their properties from the previous row. Defaults to False, meaning that new rows acquire the properties of the row immediately after them. .. warning:: `inherit_from_before` must be False when adding rows to the top of a spreadsheet (`row=1`), and must be True when adding to the bottom of the spreadsheet. """ # can't insert row on sheet with colon ':' # in its name, see issue: https://issuetracker.google.com/issues/36761154 if ":" in self.title: raise GSpreadException( "can't insert row in worksheet with colon ':' in its name. See issue: https://issuetracker.google.com/issues/36761154" ) if inherit_from_before and row == 1: raise GSpreadException( "inherit_from_before cannot be used when inserting row(s) at the top of a spreadsheet" ) insert_dimension_body = { "requests": [ { "insertDimension": { "range": { "sheetId": self.id, "dimension": Dimension.rows, "startIndex": row - 1, "endIndex": len(values) + row - 1, }, "inheritFromBefore": inherit_from_before, } } ] } self.client.batch_update(self.spreadsheet_id, insert_dimension_body) range_label = absolute_range_name(self.title, "A%s" % row) params: ParamsType = {"valueInputOption": value_input_option} body = {"majorDimension": Dimension.rows, "values": values} res = self.client.values_append(self.spreadsheet_id, range_label, params, body) num_new_rows = len(values) self._properties["gridProperties"]["rowCount"] += num_new_rows return res def insert_cols( self, values: Sequence[Sequence[Union[str, int, float]]], col: int = 1, value_input_option: ValueInputOption = ValueInputOption.raw, inherit_from_before: bool = False, ) -> JSONResponse: """Adds multiple new cols to the worksheet at specified index and populates them with values. :param list values: List of col lists. a list of lists, with the lists each containing one col's values. Increases the number of rows if there are more values than columns. :param int col: Start col to update (one-based). Defaults to 1 (one). :param str value_input_option: (optional) Determines how input data should be interpreted. Possible values are ``ValueInputOption.raw`` or ``ValueInputOption.user_entered``. See `ValueInputOption`_ in the Sheets API. :type value_input_option: :class:`~gspread.utils.ValueInputOption` :param bool inherit_from_before: (optional) If True, new columns will inherit their properties from the previous column. Defaults to False, meaning that new columns acquire the properties of the column immediately after them. .. warning:: `inherit_from_before` must be False if adding at the left edge of a spreadsheet (`col=1`), and must be True if adding at the right edge of the spreadsheet. """ if inherit_from_before and col == 1: raise GSpreadException( "inherit_from_before cannot be used when inserting column(s) at the left edge of a spreadsheet" ) insert_dimension_body = { "requests": [ { "insertDimension": { "range": { "sheetId": self.id, "dimension": Dimension.cols, "startIndex": col - 1, "endIndex": len(values) + col - 1, }, "inheritFromBefore": inherit_from_before, } } ] } self.client.batch_update(self.spreadsheet_id, insert_dimension_body) range_label = absolute_range_name(self.title, rowcol_to_a1(1, col)) params: ParamsType = {"valueInputOption": value_input_option} body = {"majorDimension": Dimension.cols, "values": values} res = self.client.values_append(self.spreadsheet_id, range_label, params, body) num_new_cols = len(values) self._properties["gridProperties"]["columnCount"] += num_new_cols return res @cast_to_a1_notation def add_protected_range( self, name: str, editor_users_emails: Sequence[str] = [], editor_groups_emails: Sequence[str] = [], description: Optional[str] = None, warning_only: bool = False, requesting_user_can_edit: bool = False, ) -> JSONResponse: """Add protected range to the sheet. Only the editors can edit the protected range. Google API will automatically add the owner of this SpreadSheet. The list ``editor_users_emails`` must at least contain the e-mail address used to open that SpreadSheet. ``editor_users_emails`` must only contain e-mail addresses who already have a write access to the spreadsheet. :param str name: A string with range value in A1 notation, e.g. 'A1:A5'. Alternatively, you may specify numeric boundaries. All values index from 1 (one): :param int first_row: First row number :param int first_col: First column number :param int last_row: Last row number :param int last_col: Last column number For both A1 and numeric notation: :param list editor_users_emails: The email addresses of users with edit access to the protected range. This must include your e-mail address at least. :param list editor_groups_emails: (optional) The email addresses of groups with edit access to the protected range. :param str description: (optional) Description for the protected ranges. :param boolean warning_only: (optional) When true this protected range will show a warning when editing. Defaults to ``False``. :param boolean requesting_user_can_edit: (optional) True if the user who requested this protected range can edit the protected cells. Defaults to ``False``. """ grid_range = a1_range_to_grid_range(name, self.id) body = { "requests": [ { "addProtectedRange": { "protectedRange": { "range": grid_range, "description": description, "warningOnly": warning_only, "requestingUserCanEdit": requesting_user_can_edit, "editors": ( None if warning_only else { "users": editor_users_emails, "groups": editor_groups_emails, } ), } } } ] } return self.client.batch_update(self.spreadsheet_id, body) def delete_protected_range(self, id: str) -> JSONResponse: """Delete protected range identified by the ID ``id``. To retrieve the ID of a protected range use the following method to list them all: :func:`~gspread.Spreadsheet.list_protected_ranges` """ body = { "requests": [ { "deleteProtectedRange": { "protectedRangeId": id, } } ] } return self.client.batch_update(self.spreadsheet_id, body) def delete_dimension( self, dimension: Dimension, start_index: int, end_index: Optional[int] = None ) -> JSONResponse: """Deletes multi rows from the worksheet at the specified index. :param dimension: A dimension to delete. ``Dimension.rows`` or ``Dimension.cols``. :type dimension: :class:`~gspread.utils.Dimension` :param int start_index: Index of a first row for deletion. :param int end_index: Index of a last row for deletion. When ``end_index`` is not specified this method only deletes a single row at ``start_index``. """ if end_index is None: end_index = start_index body = { "requests": [ { "deleteDimension": { "range": { "sheetId": self.id, "dimension": dimension, "startIndex": start_index - 1, "endIndex": end_index, } } } ] } res = self.client.batch_update(self.spreadsheet_id, body) if end_index is None: end_index = start_index num_deleted = end_index - start_index + 1 if dimension == Dimension.rows: self._properties["gridProperties"]["rowCount"] -= num_deleted elif dimension == Dimension.cols: self._properties["gridProperties"]["columnCount"] -= num_deleted return res def delete_rows( self, start_index: int, end_index: Optional[int] = None ) -> JSONResponse: """Deletes multiple rows from the worksheet at the specified index. :param int start_index: Index of a first row for deletion. :param int end_index: Index of a last row for deletion. When end_index is not specified this method only deletes a single row at ``start_index``. Example:: # Delete rows 5 to 10 (inclusive) worksheet.delete_rows(5, 10) # Delete only the second row worksheet.delete_rows(2) """ return self.delete_dimension(Dimension.rows, start_index, end_index) def delete_columns( self, start_index: int, end_index: Optional[int] = None ) -> JSONResponse: """Deletes multiple columns from the worksheet at the specified index. :param int start_index: Index of a first column for deletion. :param int end_index: Index of a last column for deletion. When end_index is not specified this method only deletes a single column at ``start_index``. """ return self.delete_dimension(Dimension.cols, start_index, end_index) def clear(self) -> JSONResponse: """Clears all cells in the worksheet.""" return self.client.values_clear( self.spreadsheet_id, absolute_range_name(self.title) ) def batch_clear(self, ranges: Sequence[str]) -> JSONResponse: """Clears multiple ranges of cells with 1 API call. `Batch Clear`_ .. _Batch Clear: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchClear Examples:: worksheet.batch_clear(['A1:B1','my_range']) # Note: named ranges are defined in the scope of # a spreadsheet, so even if `my_range` does not belong to # this sheet it is still updated .. versionadded:: 3.8.0 """ ranges = [absolute_range_name(self.title, rng) for rng in ranges] body = {"ranges": ranges} response = self.client.values_batch_clear(self.spreadsheet_id, body=body) return response def _finder( self, func: Callable[[Callable[[Cell], bool], Iterable[Cell]], Iterator[Cell]], query: Union[str, re.Pattern], case_sensitive: bool, in_row: Optional[int] = None, in_column: Optional[int] = None, ) -> Iterator[Cell]: data = self.client.values_get( self.spreadsheet_id, absolute_range_name(self.title) ) try: values = fill_gaps(data["values"]) except KeyError: values = [] cells = self._list_cells(values, in_row, in_column) if isinstance(query, str): str_query = query def match(x: Cell) -> bool: if case_sensitive or x.value is None: return x.value == str_query else: return x.value.casefold() == str_query.casefold() elif isinstance(query, re.Pattern): re_query = query def match(x: Cell) -> bool: return re_query.search(x.value) is not None else: raise TypeError( "query must be of type: 'str' or 're.Pattern' (obtained from re.compile())" ) return func(match, cells) def _list_cells( self, values: Sequence[Sequence[Union[str, int, float]]], in_row: Optional[int] = None, in_column: Optional[int] = None, ) -> List[Cell]: """Returns a list of ``Cell`` instances scoped by optional ``in_row``` or ``in_column`` values (both one-based). """ if in_row is not None and in_column is not None: raise TypeError("Either 'in_row' or 'in_column' should be specified.") if in_column is not None: return [ Cell(row=i + 1, col=in_column, value=str(row[in_column - 1])) for i, row in enumerate(values) ] elif in_row is not None: return [ Cell(row=in_row, col=j + 1, value=str(value)) for j, value in enumerate(values[in_row - 1]) ] else: return [ Cell(row=i + 1, col=j + 1, value=str(value)) for i, row in enumerate(values) for j, value in enumerate(row) ] def find( self, query: Union[str, re.Pattern], in_row: Optional[int] = None, in_column: Optional[int] = None, case_sensitive: bool = True, ) -> Optional[Cell]: """Finds the first cell matching the query. :param query: A literal string to match or compiled regular expression. :type query: str, :py:class:`re.RegexObject` :param int in_row: (optional) One-based row number to scope the search. :param int in_column: (optional) One-based column number to scope the search. :param bool case_sensitive: (optional) comparison is case sensitive if set to True, case insensitive otherwise. Default is True. Does not apply to regular expressions. :returns: the first matching cell or None otherwise :rtype: :class:`gspread.cell.Cell` """ try: return next(self._finder(filter, query, case_sensitive, in_row, in_column)) except StopIteration: return None def findall( self, query: Union[str, re.Pattern], in_row: Optional[int] = None, in_column: Optional[int] = None, case_sensitive: bool = True, ) -> List[Cell]: """Finds all cells matching the query. Returns a list of :class:`gspread.cell.Cell`. :param query: A literal string to match or compiled regular expression. :type query: str, :py:class:`re.RegexObject` :param int in_row: (optional) One-based row number to scope the search. :param int in_column: (optional) One-based column number to scope the search. :param bool case_sensitive: (optional) comparison is case sensitive if set to True, case insensitive otherwise. Default is True. Does not apply to regular expressions. :returns: the list of all matching cells or empty list otherwise :rtype: list """ return [ elem for elem in self._finder(filter, query, case_sensitive, in_row, in_column) ] def freeze( self, rows: Optional[int] = None, cols: Optional[int] = None ) -> JSONResponse: """Freeze rows and/or columns on the worksheet. :param rows: Number of rows to freeze. :param cols: Number of columns to freeze. """ grid_properties = {} if rows is not None: grid_properties["frozenRowCount"] = rows if cols is not None: grid_properties["frozenColumnCount"] = cols if not grid_properties: raise TypeError("Either 'rows' or 'cols' should be specified.") fields = ",".join("gridProperties/%s" % p for p in grid_properties.keys()) body = { "requests": [ { "updateSheetProperties": { "properties": { "sheetId": self.id, "gridProperties": grid_properties, }, "fields": fields, } } ] } res = self.client.batch_update(self.spreadsheet_id, body) if rows is not None: self._properties["gridProperties"]["frozenRowCount"] = rows if cols is not None: self._properties["gridProperties"]["frozenColumnCount"] = cols return res @cast_to_a1_notation def set_basic_filter(self, name: Optional[str] = None) -> Any: """Add a basic filter to the worksheet. If a range or boundaries are passed, the filter will be limited to the given range. :param str name: A string with range value in A1 notation, e.g. ``A1:A5``. Alternatively, you may specify numeric boundaries. All values index from 1 (one): :param int first_row: First row number :param int first_col: First column number :param int last_row: Last row number :param int last_col: Last column number .. versionadded:: 3.4 """ grid_range = ( a1_range_to_grid_range(name, self.id) if name is not None else {"sheetId": self.id} ) body = {"requests": [{"setBasicFilter": {"filter": {"range": grid_range}}}]} return self.client.batch_update(self.spreadsheet_id, body) def clear_basic_filter(self) -> JSONResponse: """Remove the basic filter from a worksheet. .. versionadded:: 3.4 """ body = { "requests": [ { "clearBasicFilter": { "sheetId": self.id, } } ] } return self.client.batch_update(self.spreadsheet_id, body) @classmethod def _duplicate( cls, client: HTTPClient, spreadsheet_id: str, sheet_id: int, spreadsheet: Any, insert_sheet_index: Optional[int] = None, new_sheet_id: Optional[int] = None, new_sheet_name: Optional[str] = None, ) -> "Worksheet": """Class method to duplicate a :class:`gspread.worksheet.Worksheet`. :param Session client: The HTTP client used for the HTTP request :param str spreadsheet_id: The spreadsheet ID (used for the HTTP request) :param int sheet_id: The original sheet ID :param int insert_sheet_index: (optional) The zero-based index where the new sheet should be inserted. The index of all sheets after this are incremented. :param int new_sheet_id: (optional) The ID of the new sheet. If not set, an ID is chosen. If set, the ID must not conflict with any existing sheet ID. If set, it must be non-negative. :param str new_sheet_name: (optional) The name of the new sheet. If empty, a new name is chosen for you. :returns: a newly created :class:`gspread.worksheet.Worksheet`. .. note:: This is a class method in order for the spreadsheet class to use it without an instance of a Worksheet object """ body = { "requests": [ { "duplicateSheet": { "sourceSheetId": sheet_id, "insertSheetIndex": insert_sheet_index, "newSheetId": new_sheet_id, "newSheetName": new_sheet_name, } } ] } data = client.batch_update(spreadsheet_id, body) properties = data["replies"][0]["duplicateSheet"]["properties"] return Worksheet(spreadsheet, properties, spreadsheet_id, client) def duplicate( self, insert_sheet_index: Optional[int] = None, new_sheet_id: Optional[int] = None, new_sheet_name: Optional[str] = None, ) -> "Worksheet": """Duplicate the sheet. :param int insert_sheet_index: (optional) The zero-based index where the new sheet should be inserted. The index of all sheets after this are incremented. :param int new_sheet_id: (optional) The ID of the new sheet. If not set, an ID is chosen. If set, the ID must not conflict with any existing sheet ID. If set, it must be non-negative. :param str new_sheet_name: (optional) The name of the new sheet. If empty, a new name is chosen for you. :returns: a newly created :class:`gspread.worksheet.Worksheet`. .. versionadded:: 3.1 """ return Worksheet._duplicate( self.client, self.spreadsheet_id, self.id, self.spreadsheet, insert_sheet_index=insert_sheet_index, new_sheet_id=new_sheet_id, new_sheet_name=new_sheet_name, ) def copy_to( self, destination_spreadsheet_id: str, ) -> JSONResponse: """Copies this sheet to another spreadsheet. :param str spreadsheet_id: The ID of the spreadsheet to copy the sheet to. :returns: a dict with the response containing information about the newly created sheet. :rtype: dict """ return self.client.spreadsheets_sheets_copy_to( self.spreadsheet_id, self.id, destination_spreadsheet_id ) @cast_to_a1_notation def merge_cells(self, name: str, merge_type: str = MergeType.merge_all) -> Any: """Merge cells. :param str name: Range name in A1 notation, e.g. 'A1:A5'. :param merge_type: (optional) one of ``MergeType.merge_all``, ``MergeType.merge_columns``, or ``MergeType.merge_rows``. Defaults to ``MergeType.merge_all``. See `MergeType`_ in the Sheets API reference. :type merge_type: :namedtuple:`~gspread.utils.MergeType` Alternatively, you may specify numeric boundaries. All values index from 1 (one): :param int first_row: First row number :param int first_col: First column number :param int last_row: Last row number :param int last_col: Last column number :returns: the response body from the request :rtype: dict .. _MergeType: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#MergeType """ grid_range = a1_range_to_grid_range(name, self.id) body = { "requests": [{"mergeCells": {"mergeType": merge_type, "range": grid_range}}] } return self.client.batch_update(self.spreadsheet_id, body) @cast_to_a1_notation def unmerge_cells(self, name: str) -> JSONResponse: """Unmerge cells. Unmerge previously merged cells. :param str name: Range name in A1 notation, e.g. 'A1:A5'. Alternatively, you may specify numeric boundaries. All values index from 1 (one): :param int first_row: First row number :param int first_col: First column number :param int last_row: Last row number :param int last_col: Last column number :returns: the response body from the request :rtype: dict """ grid_range = a1_range_to_grid_range(name, self.id) body = { "requests": [ { "unmergeCells": { "range": grid_range, }, }, ] } return self.client.batch_update(self.spreadsheet_id, body) def get_notes(self, default_empty_value: Optional[str] = "") -> List[List[str]]: """Returns a list of lists containing all notes in the sheet. .. note:: The resulting matrix is not necessarily square. The matrix is as tall as the last row with a note, and each row is only as long as the last column in that row with a note. Please see the example below. To ensure it is square, use `gspread.utils.fill_gaps`, for example like `utils.fill_gaps(arr, len(arr), max(len(a) for a in arr), None)` :param str default_empty_value: (optional) Determines which value to use for cells without notes, defaults to None. Examples:: # Note data: # A B # 1 A1 - # 2 - B2 # Read all notes from the sheet >>> arr = worksheet.get_notes() >>> print(arr) [ ["A1"], ["", "B2"] ] >>> print(gspread.utils.fill_gaps(arr, len(arr), max(len(a) for a in arr), None)) [ ["A1", ""], ["", "B2"] ] """ params: ParamsType = {"fields": "sheets.data.rowData.values.note"} res = self.client.spreadsheets_get(self.spreadsheet_id, params) data = res["sheets"][self.index]["data"][0].get("rowData", [{}]) notes: List[List[str]] = [] for row in data: notes.append([]) for cell in row.get("values", []): notes[-1].append(cell.get("note", default_empty_value)) return notes def get_note(self, cell: str) -> str: """Get the content of the note located at `cell`, or the empty string if the cell does not have a note. :param str cell: A string with cell coordinates in A1 notation, e.g. 'D7'. """ absolute_cell = absolute_range_name(self.title, cell) params: ParamsType = { "ranges": absolute_cell, "fields": "sheets/data/rowData/values/note", } res = self.client.spreadsheets_get(self.spreadsheet_id, params) try: note = res["sheets"][0]["data"][0]["rowData"][0]["values"][0]["note"] except (IndexError, KeyError): note = "" return note def update_notes(self, notes: Mapping[str, str]) -> None: """update multiple notes. The notes are attached to a certain cell. :param notes dict: A dict of notes with their cells coordinates and respective content dict format is: * key: the cell coordinates as A1 range format * value: the string content of the cell Example:: { "D7": "Please read my notes", "GH42": "this one is too far", } .. versionadded:: 5.9 """ # No need to type lower than the sequence, it's internal only body: MutableMapping[str, List[Any]] = {"requests": []} for range, content in notes.items(): if not isinstance(content, str): raise TypeError( "Only string allowed as content for a note: '{} - {}'".format( range, content ) ) req = { "updateCells": { "range": a1_range_to_grid_range(range, self.id), "fields": "note", "rows": [ { "values": [ { "note": content, }, ], }, ], }, } body["requests"].append(req) self.client.batch_update(self.spreadsheet_id, body) @cast_to_a1_notation def update_note(self, cell: str, content: str) -> None: """Update the content of the note located at `cell`. :param str cell: A string with cell coordinates in A1 notation, e.g. 'D7'. :param str note: The text note to insert. .. versionadded:: 3.7 """ self.update_notes({cell: content}) @cast_to_a1_notation def insert_note(self, cell: str, content: str) -> None: """Insert a note. The note is attached to a certain cell. :param str cell: A string with cell coordinates in A1 notation, e.g. 'D7'. :param str content: The text note to insert. Alternatively, you may specify numeric boundaries. All values index from 1 (one): :param int first_row: First row number :param int first_col: First column number :param int last_row: Last row number :param int last_col: Last column number .. versionadded:: 3.7 """ self.update_notes({cell: content}) def insert_notes(self, notes: Mapping[str, str]) -> None: """insert multiple notes. The notes are attached to a certain cell. :param notes dict: A dict of notes with their cells coordinates and respective content dict format is: * key: the cell coordinates as A1 range format * value: the string content of the cell Example:: { "D7": "Please read my notes", "GH42": "this one is too far", } .. versionadded:: 5.9 """ self.update_notes(notes) def clear_notes(self, ranges: Iterable[str]) -> None: """Clear all notes located at the at the coordinates pointed to by ``ranges``. :param ranges list: List of A1 coordinates where to clear the notes. e.g. ``["A1", "GH42", "D7"]`` """ notes = {range: "" for range in ranges} self.update_notes(notes) @cast_to_a1_notation def clear_note(self, cell: str) -> None: """Clear a note. The note is attached to a certain cell. :param str cell: A string with cell coordinates in A1 notation, e.g. 'D7'. Alternatively, you may specify numeric boundaries. All values index from 1 (one): :param int first_row: First row number :param int first_col: First column number :param int last_row: Last row number :param int last_col: Last column number .. versionadded:: 3.7 """ # set the note to will clear it self.update_notes({cell: ""}) @cast_to_a1_notation def define_named_range(self, name: str, range_name: str) -> JSONResponse: """ :param str name: A string with range value in A1 notation, e.g. 'A1:A5'. Alternatively, you may specify numeric boundaries. All values index from 1 (one): :param int first_row: First row number :param int first_col: First column number :param int last_row: Last row number :param int last_col: Last column number :param range_name: The name to assign to the range of cells :returns: the response body from the request :rtype: dict """ body = { "requests": [ { "addNamedRange": { "namedRange": { "name": range_name, "range": a1_range_to_grid_range(name, self.id), } } } ] } return self.client.batch_update(self.spreadsheet_id, body) def delete_named_range(self, named_range_id: str) -> JSONResponse: """ :param str named_range_id: The ID of the named range to delete. Can be obtained with Spreadsheet.list_named_ranges() :returns: the response body from the request :rtype: dict """ body = { "requests": [ { "deleteNamedRange": { "namedRangeId": named_range_id, } } ] } return self.client.batch_update(self.spreadsheet_id, body) def _add_dimension_group( self, start: int, end: int, dimension: Dimension ) -> JSONResponse: """ update this sheet by grouping 'dimension' :param int start: The start (inclusive) of the group :param int end: The end (exclusive) of the grou :param str dimension: The dimension to group, can be one of ``ROWS`` or ``COLUMNS``. :type diension: :class:`~gspread.utils.Dimension` """ body = { "requests": [ { "addDimensionGroup": { "range": { "sheetId": self.id, "dimension": dimension, "startIndex": start, "endIndex": end, }, } } ] } return self.client.batch_update(self.spreadsheet_id, body) def add_dimension_group_columns(self, start: int, end: int) -> JSONResponse: """ Group columns in order to hide them in the UI. .. note:: API behavior with nested groups and non-matching ``[start:end)`` range can be found here: `Add Dimension Group Request`_ .. _Add Dimension Group Request: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#AddDimensionGroupRequest :param int start: The start (inclusive) of the group :param int end: The end (exclusive) of the group """ return self._add_dimension_group(start, end, Dimension.cols) def add_dimension_group_rows(self, start: int, end: int) -> JSONResponse: """ Group rows in order to hide them in the UI. .. note:: API behavior with nested groups and non-matching ``[start:end)`` range can be found here `Add Dimension Group Request`_ :param int start: The start (inclusive) of the group :param int end: The end (exclusive) of the group """ return self._add_dimension_group(start, end, Dimension.rows) def _delete_dimension_group( self, start: int, end: int, dimension: Dimension ) -> JSONResponse: """delete a dimension group in this sheet""" body = { "requests": [ { "deleteDimensionGroup": { "range": { "sheetId": self.id, "dimension": dimension, "startIndex": start, "endIndex": end, } } } ] } return self.client.batch_update(self.spreadsheet_id, body) def delete_dimension_group_columns(self, start: int, end: int) -> JSONResponse: """ Remove the grouping of a set of columns. .. note:: API behavior with nested groups and non-matching ``[start:end)`` range can be found here `Delete Dimension Group Request`_ .. _Delete Dimension Group Request: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#DeleteDimensionGroupRequest :param int start: The start (inclusive) of the group :param int end: The end (exclusive) of the group """ return self._delete_dimension_group(start, end, Dimension.cols) def delete_dimension_group_rows(self, start: int, end: int) -> JSONResponse: """ Remove the grouping of a set of rows. .. note:: API behavior with nested groups and non-matching ``[start:end)`` range can be found here `Delete Dimension Group Request`_ :param int start: The start (inclusive) of the group :param int end: The end (exclusive) of the group """ return self._delete_dimension_group(start, end, Dimension.rows) def list_dimension_group_columns(self) -> List[JSONResponse]: """ List all the grouped columns in this worksheet. :returns: list of the grouped columns :rtype: list """ return self._get_sheet_property("columnGroups", []) def list_dimension_group_rows(self) -> List[JSONResponse]: """ List all the grouped rows in this worksheet. :returns: list of the grouped rows :rtype: list """ return self._get_sheet_property("rowGroups", []) def _hide_dimension( self, start: int, end: int, dimension: Dimension ) -> JSONResponse: """ Update this sheet by hiding the given 'dimension' Index starts from 0. :param int start: The (inclusive) start of the dimension to hide :param int end: The (exclusive) end of the dimension to hide :param str dimension: The dimension to hide, can be one of ``ROWS`` or ``COLUMNS``. :type diension: :class:`~gspread.utils.Dimension` """ body = { "requests": [ { "updateDimensionProperties": { "range": { "sheetId": self.id, "dimension": dimension, "startIndex": start, "endIndex": end, }, "properties": { "hiddenByUser": True, }, "fields": "hiddenByUser", } } ] } return self.client.batch_update(self.spreadsheet_id, body) def hide_columns(self, start: int, end: int) -> JSONResponse: """ Explicitly hide the given column index range. Index starts from 0. :param int start: The (inclusive) starting column to hide :param int end: The (exclusive) end column to hide """ return self._hide_dimension(start, end, Dimension.cols) def hide_rows(self, start: int, end: int) -> JSONResponse: """ Explicitly hide the given row index range. Index starts from 0. :param int start: The (inclusive) starting row to hide :param int end: The (exclusive) end row to hide """ return self._hide_dimension(start, end, Dimension.rows) def _unhide_dimension( self, start: int, end: int, dimension: Dimension ) -> JSONResponse: """ Update this sheet by unhiding the given 'dimension' Index starts from 0. :param int start: The (inclusive) start of the dimension to unhide :param int end: The (inclusive) end of the dimension to unhide :param str dimension: The dimension to hide, can be one of ``ROWS`` or ``COLUMNS``. :type dimension: :class:`~gspread.utils.Dimension` """ body = { "requests": [ { "updateDimensionProperties": { "range": { "sheetId": self.id, "dimension": dimension, "startIndex": start, "endIndex": end, }, "properties": { "hiddenByUser": False, }, "fields": "hiddenByUser", } } ] } return self.client.batch_update(self.spreadsheet_id, body) def unhide_columns(self, start: int, end: int) -> JSONResponse: """ Explicitly unhide the given column index range. Index start from 0. :param int start: The (inclusive) starting column to hide :param int end: The (exclusive) end column to hide """ return self._unhide_dimension(start, end, Dimension.cols) def unhide_rows(self, start: int, end: int) -> JSONResponse: """ Explicitly unhide the given row index range. Index start from 0. :param int start: The (inclusive) starting row to hide :param int end: The (exclusive) end row to hide """ return self._unhide_dimension(start, end, Dimension.rows) def _set_hidden_flag(self, hidden: bool) -> JSONResponse: """Send the appropriate request to hide/show the current worksheet""" body = { "requests": [ { "updateSheetProperties": { "properties": { "sheetId": self.id, "hidden": hidden, }, "fields": "hidden", } } ] } res = self.client.batch_update(self.spreadsheet_id, body) self._properties["hidden"] = hidden return res def hide(self) -> JSONResponse: """Hides the current worksheet from the UI.""" return self._set_hidden_flag(True) def show(self) -> JSONResponse: """Show the current worksheet in the UI.""" return self._set_hidden_flag(False) def _set_gridlines_hidden_flag(self, hidden: bool) -> JSONResponse: """Hide/show gridlines on the current worksheet""" body = { "requests": [ { "updateSheetProperties": { "properties": { "sheetId": self.id, "gridProperties": { "hideGridlines": hidden, }, }, "fields": "gridProperties.hideGridlines", } } ] } res = self.client.batch_update(self.spreadsheet_id, body) self._properties["gridProperties"]["hideGridlines"] = hidden return res def hide_gridlines(self) -> JSONResponse: """Hide gridlines on the current worksheet""" return self._set_gridlines_hidden_flag(True) def show_gridlines(self) -> JSONResponse: """Show gridlines on the current worksheet""" return self._set_gridlines_hidden_flag(False) def copy_range( self, source: str, dest: str, paste_type: PasteType = PasteType.normal, paste_orientation: PasteOrientation = PasteOrientation.normal, ) -> JSONResponse: """Copies a range of data from source to dest .. note:: ``paste_type`` values are explained here: `Paste Types`_ .. _Paste Types: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#pastetype :param str source: The A1 notation of the source range to copy :param str dest: The A1 notation of the destination where to paste the data Can be the A1 notation of the top left corner where the range must be paste ex: G16, or a complete range notation ex: G16:I20. The dimensions of the destination range is not checked and has no effect, if the destination range does not match the source range dimension, the entire source range is copies anyway. :param paste_type: the paste type to apply. Many paste type are available from the Sheet API, see above note for detailed values for all values and their effects. Defaults to ``PasteType.normal`` :type paste_type: :class:`~gspread.utils.PasteType` :param paste_orientation: The paste orient to apply. Possible values are: ``normal`` to keep the same orientation, ``transpose`` where all rows become columns and vice versa. :type paste_orientation: :class:`~gspread.utils.PasteOrientation` """ body = { "requests": [ { "copyPaste": { "source": a1_range_to_grid_range(source, self.id), "destination": a1_range_to_grid_range(dest, self.id), "pasteType": paste_type, "pasteOrientation": paste_orientation, } } ] } return self.client.batch_update(self.spreadsheet_id, body) def cut_range( self, source: str, dest: str, paste_type: PasteType = PasteType.normal, ) -> JSONResponse: """Moves a range of data form source to dest .. note:: ``paste_type`` values are explained here: `Paste Types`_ .. _Paste Types: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#pastetype :param str source: The A1 notation of the source range to move :param str dest: The A1 notation of the destination where to paste the data **it must be a single cell** in the A1 notation. ex: G16 :param paste_type: the paste type to apply. Many paste type are available from the Sheet API, see above note for detailed values for all values and their effects. Defaults to ``PasteType.normal`` :type paste_type: :class:`~gspread.utils.PasteType` """ # in the cut/paste request, the destination object # is a `gridCoordinate` and not a `gridRang` # it has different object keys grid_dest = a1_range_to_grid_range(dest, self.id) body = { "requests": [ { "cutPaste": { "source": a1_range_to_grid_range(source, self.id), "destination": { "sheetId": grid_dest["sheetId"], "rowIndex": grid_dest["startRowIndex"], "columnIndex": grid_dest["startColumnIndex"], }, "pasteType": paste_type, } } ] } return self.client.batch_update(self.spreadsheet_id, body) def add_validation( self, range: str, condition_type: ValidationConditionType, values: Iterable[Any], inputMessage: Optional[str] = None, strict: bool = False, showCustomUi: bool = False, ) -> Any: """Adds a data validation rule to any given range. .. note:: ``condition_type`` values are explained here: `ConditionType`_ .. _ConditionType: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ConditionType :param str source: The A1 notation of the source range to move :param condition_type: The sort of condition to apply. :param values: List of condition values. :type values: Any :param str inputMessage: Message to show for the validation. :param bool strict: Whether to reject invalid data or not. :param bool showCustomUi: Whether to show a custom UI(Dropdown) for list values. **Examples** .. code-block:: python import gspread from gspread.utils import ValidationConditionType ... ws = spreadsheet.sheet1 ws.add_validation( 'A1', ValidationConditionType.number_greater, [10], strict=True, inputMessage='Value must be greater than 10', ) ws.add_validation( 'C2:C7', ValidationConditionType.one_of_list, ['Yes','No'], showCustomUi=True ) """ if not isinstance(condition_type, ValidationConditionType): raise TypeError( "condition_type param should be a valid ValidationConditionType." ) grid = a1_range_to_grid_range(range, self.id) body = { "requests": [ { "setDataValidation": { "range": grid, "rule": { "condition": { "type": condition_type, "values": [ ({"userEnteredValue": value}) for value in values ], }, "showCustomUi": showCustomUi, "strict": strict, "inputMessage": inputMessage, }, } } ], } return self.client.batch_update(self.spreadsheet_id, body) python-gspread-6.1.4/pyproject.toml000066400000000000000000000027501472155066000174140ustar00rootroot00000000000000[build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" [project] name = "gspread" authors = [{ name = "Anton Burnashev", email = "fuss.here@gmail.com" }] maintainers = [ { name = "Alexandre Lavigne", email = "lavigne958@gmail.com" }, { name = "alifeee", email = "alifeee.web@outlook.com" }, ] readme = "README.md" keywords = ["spreadsheets", "google-spreadsheets", "google-sheets"] classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: End Users/Desktop", "Intended Audience :: Science/Research", "Topic :: Office/Business :: Financial :: Spreadsheet", "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = ["google-auth>=1.12.0", "google-auth-oauthlib>=0.4.1"] requires-python = ">=3.8" dynamic = ["version", "description"] [project.urls] Documentation = "https://gspread.readthedocs.io/en/latest/" Source = "https://github.com/burnash/gspread" [project.license] file = "LICENSE.txt" [tool.flit.sdist] include = ["docs/", "HISTORY.rst"] [tool.setuptools.package-data] gspread = ["gspread/py.typed"]