├── surveymonkey ├── __init__.py ├── exceptions.py └── client.py ├── MANIFEST.in ├── pyproject.toml ├── LICENSE ├── .gitignore └── README.md /surveymonkey/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | 2 | include README.md -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "surveymonkey-python" 3 | version = "0.1.6" 4 | description = "API wrapper for SurveyMonkey written in Python" 5 | authors = ["Gearplug Apps "] 6 | license = "MIT" 7 | readme = "README.md" 8 | packages = [{include = "surveymonkey"}] 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.7" 12 | requests = "^2.26.0" 13 | 14 | 15 | [build-system] 16 | requires = ["poetry-core"] 17 | build-backend = "poetry.core.masonry.api" 18 | 19 | -------------------------------------------------------------------------------- /surveymonkey/exceptions.py: -------------------------------------------------------------------------------- 1 | class BaseError(Exception): 2 | pass 3 | 4 | 5 | class UnknownError(BaseError): 6 | pass 7 | 8 | 9 | class BadRequestError(BaseError): 10 | pass 11 | 12 | 13 | class AuthorizationError(BaseError): 14 | pass 15 | 16 | 17 | class PermissionError(BaseError): 18 | pass 19 | 20 | 21 | class ResourceNotFoundError(BaseError): 22 | pass 23 | 24 | 25 | class ResourceConflictError(BaseError): 26 | pass 27 | 28 | 29 | class RequestEntityTooLargeError(BaseError): 30 | pass 31 | 32 | 33 | class RateLimitReachedError(BaseError): 34 | pass 35 | 36 | 37 | class InternalServerError(BaseError): 38 | pass 39 | 40 | 41 | class UserSoftDeletedError(BaseError): 42 | pass 43 | 44 | 45 | class UserDeletedError(BaseError): 46 | pass 47 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 GearPlug 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # surveymonkey-python 2 | Python wrapper for SurveyMonkey API 3 | 4 | ## Installing 5 | ``` 6 | pip install surveymonkey-python 7 | ``` 8 | 9 | ## Usage 10 | 11 | - Instantiate client 12 | ``` 13 | from client import Client 14 | # If you do not have access_token, run 15 | 16 | client=Client( 17 | client_id=CLIENT_ID, client_secret=CLIENT_SECRET, redirect_uri=REDIRECT_URI, access_token=None) 18 | # If you have access_token, run 19 | client=Client( 20 | client_id=CLIENT_ID, client_secret=CLIENT_SECRET, redirect_uri=REDIRECT_URI, access_token=ACCESS_TOKEN) 21 | ``` 22 | 23 | - OAuth (instantiate client with `access_token = None`) 24 | 1- Get authorization URL `client.get_authorization_url()` 25 | 2- Extract `code` from the URL and send it as an argument in `client.exchange_code(code)` 26 | 3- Remove the token from the response obtained and send it as an argument in `client.set_access_token(token)` 27 | 28 | - Functionality methods, they refer to methods that make calls to the different endpoints of the SurveyMonkey API, 29 | the use is quite simple: 30 | `client.method(args)` 31 | e.g. `client.get_survey_pages(survey_id)` 32 | where `survey_id` represent the id of the survey. 33 | 34 | # TODO 35 | - Response Counts and Trends endpoints 36 | - Contacts and Contact Lists endpoints 37 | - Translations for Multilingual Surveys endpoints 38 | - Collectors and Invite Messages endpoints 39 | - Benchmarks endpoints 40 | - Organizations endpoint 41 | - Errors endpoint 42 | 43 | ## Contributing 44 | We are always grateful for any kind of contribution including but not limited to bug reports, code enhancements, bug fixes, and even functionality suggestions. 45 | #### You can report any bug you find or suggest new functionality with a new [issue](https://github.com/GearPlug/surveymonkey-python). 46 | #### If you want to add yourself some functionality to the wrapper: 47 | 1. Fork it ( https://github.com/GearPlug/surveymonkey-python ) 48 | 2. Create your feature branch (git checkout -b my-new-feature) 49 | 3. Commit your changes (git commit -am 'Adds my new feature') 50 | 4. Push to the branch (git push origin my-new-feature) 51 | 5. Create a new Pull Request 52 | -------------------------------------------------------------------------------- /surveymonkey/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from urllib.parse import urlencode 4 | from math import ceil 5 | 6 | from surveymonkey.exceptions import UnknownError, BadRequestError, AuthorizationError, PermissionError, \ 7 | ResourceNotFoundError, ResourceConflictError, RequestEntityTooLargeError, RateLimitReachedError, \ 8 | InternalServerError, UserSoftDeletedError, UserDeletedError 9 | 10 | ''' 11 | Token expiration and revocation 12 | Our access tokens don’t currently expire but may in the future. We’ll warn all developers before making changes. 13 | 14 | Access tokens can be revoked by the user. If this happens, you’ll get a JSON-encoded response body including a key 15 | statuswith a value of 1 and a key errmsg with the value of Client revoked access grant when making an API request. 16 | If you get this response, you’ll need to complete OAuth again. 17 | ''' 18 | 19 | BASE_URL = "https://api.surveymonkey.com" 20 | API_URL = "https://api.surveymonkey.com/v3" 21 | AUTH_CODE = "/oauth/authorize" 22 | ACCESS_TOKEN_URL = "/oauth/token" 23 | 24 | 25 | class Client(object): 26 | 27 | def __init__(self, client_id=None, client_secret=None, redirect_uri=None, access_token=None): 28 | self.code = None 29 | self.client_id = client_id 30 | self.redirect_uri = redirect_uri 31 | self.client_secret = client_secret 32 | self._access_token = access_token 33 | 34 | # Authorization 35 | def get_authorization_url(self): 36 | """ 37 | 38 | :return: 39 | """ 40 | params = {'client_id': self.client_id, 'redirect_uri': self.redirect_uri, 'response_type': 'code'} 41 | url = BASE_URL + AUTH_CODE + '?' + urlencode(params) 42 | return url 43 | 44 | def exchange_code(self, code): 45 | """ 46 | 47 | :param code: 48 | :return: 49 | """ 50 | params = {'code': code, 'client_id': self.client_id, 'client_secret': self.client_secret, 51 | 'redirect_uri': self.redirect_uri, 'grant_type': 'authorization_code'} 52 | url = BASE_URL + ACCESS_TOKEN_URL 53 | response = requests.post(url, data=params) 54 | if response.status_code == 200 and 'access_token' in response.text: 55 | return response.text 56 | else: 57 | return False 58 | 59 | def refresh_token(self): 60 | """ 61 | not implemented yet, but may in the future 62 | :return: 63 | """ 64 | pass 65 | 66 | def set_access_token(self, token): 67 | """ 68 | 69 | :param token: 70 | :return: 71 | """ 72 | if isinstance(token, dict): 73 | self._access_token = token['access_token'] 74 | else: 75 | self._access_token = token 76 | 77 | # Functionality 78 | def get_authenticated_user(self): 79 | """ 80 | Returns the current user’s account details including their plan. 81 | :return: 82 | """ 83 | endpoint = "/users/me" 84 | url = API_URL + endpoint 85 | return self._get(url) 86 | 87 | def get_user_workgroup(self, user_id): 88 | """ 89 | Returns the workgroups that a specific user is in. 90 | :return: 91 | """ 92 | endpoint = "/users/{0}/workgroups".format(user_id) 93 | url = API_URL + endpoint 94 | return self._get(url) 95 | 96 | def get_shared_resources_to_user(self, user_id): 97 | """ 98 | Returns the resources shared with a user across all workgroups. 99 | :return: 100 | """ 101 | endpoint = "/users/{user_id}/shared".format(user_id) 102 | url = API_URL + endpoint 103 | return self._get(url) 104 | 105 | def get_authenticated_user_group(self): 106 | """ 107 | Returns a team if the user account belongs to a team (users can only belong to one team). 108 | :return: 109 | """ 110 | endpoint = "/groups" 111 | url = API_URL + endpoint 112 | return self._get(url) 113 | 114 | def get_group_details(self, group_id): 115 | """ 116 | Returns a teams’s details including the teams’s owner and email address. 117 | :param group_id: id of the group you want details from 118 | :return: 119 | """ 120 | endpoint = "/groups/{0}".format(group_id) 121 | url = API_URL + endpoint 122 | return self._get(url) 123 | 124 | def get_group_members(self, group_id): 125 | """ 126 | Returns a list of users who have been added as members of the specified group. 127 | :param group_id: id of the group you want details from 128 | :return: 129 | """ 130 | endpoint = "/groups/{0}/members".format(group_id) 131 | url = API_URL + endpoint 132 | return self._get(url) 133 | 134 | def get_group_member_detail(self, group_id, member_id): 135 | """ 136 | Returns a group member’s details including their role and status. 137 | :param group_id: id of the group you want details from 138 | :param member_id: id of member of the group you want details of 139 | :return: 140 | """ 141 | endpoint = "/groups/{0}/members".format(group_id) 142 | url = API_URL + endpoint 143 | return self._get(url) 144 | 145 | def get_events_list(self): 146 | """ 147 | List all possible events to subscribe 148 | :return: 149 | """ 150 | return ["response_completed", "response_disqualified", "response_updated", "response_created", 151 | "response_deleted", "response_overquota", "survey_created", "survey_updated", "survey_deleted", 152 | "collector_created", "collector_updated", "collector_deleted", "app_installed", "app_uninstalled"] 153 | 154 | def get_webhooks_list(self): 155 | """ 156 | List all create webhooks - subscribed events 157 | :return: 158 | """ 159 | endpoint = "/webhooks" 160 | url = API_URL + endpoint 161 | return self._get(url) 162 | 163 | def create_webhook(self, survey_id, callback_uri, event, webhook_name, object_type): 164 | """ 165 | Create webhook - subscribe to an event 166 | :return: 167 | """ 168 | payload = { 169 | "name": webhook_name, 170 | "event_type": event, 171 | "object_type": object_type, 172 | "object_ids": survey_id, 173 | "subscription_url": callback_uri 174 | } 175 | endpoint = "/webhooks" 176 | url = API_URL + endpoint 177 | return self._post(url, json=payload) 178 | 179 | def delete_webhook(self, webhook_id): 180 | """ 181 | Delete webhook - unsubscribe to an event 182 | :param webhook_id: id of specific webhook or subscription to delete. 183 | :return: 184 | """ 185 | endpoint = '/webhooks/{0}'.format(webhook_id) 186 | url = API_URL + endpoint 187 | return self._delete(url) 188 | 189 | def get_survey_lists(self): 190 | """ 191 | List all created surveys. 192 | :return: 193 | """ 194 | endpoint = "/surveys" 195 | url = API_URL + endpoint 196 | return self._get(url) 197 | 198 | def get_survey_lists_bulk(self, params=None): 199 | """ 200 | List all created surveys, allows for params to be added to the request, (eg. pagination). 201 | :param params: a dict of params to add to the request, possible values can be 202 | found at https://developer.surveymonkey.com/api/v3/#surveys 203 | :return: 204 | """ 205 | endpoint = "/surveys" 206 | url = API_URL + endpoint 207 | return self._get(url, params=params) 208 | 209 | def get_specific_survey(self, survey_id): 210 | """ 211 | Returns a survey’s details. 212 | :param survey_id: id of specific survey from which you want details 213 | :return: 214 | """ 215 | endpoint = "/surveys/{}".format(survey_id) 216 | url = API_URL + endpoint 217 | return self._get(url) 218 | 219 | def modify_specific_survey(self, survey_id, **kwargs): 220 | """ 221 | Modifies a survey’s title, nickname or language. 222 | :param survey_id: id of survey you want to modify 223 | :param kwargs: 224 | title, required: No (PUT default=“New Survey”), description: Survey title, type: String 225 | nickname, required: No (PUT default=“”), description: Survey nickname, type: String 226 | language, required: No (PUT default=“en”), description: Survey language, type: String 227 | buttons_text, required: No, description: Survey Buttons text container, type: Object 228 | buttons_text.next_button, required: No, description: Button text, type: String 229 | buttons_text.prev_button, required: No, description: Button text, type: String 230 | buttons_text.exit_button, required: No, description: Button text. If set to an empty string, button will 231 | be ommitted from survey, type: String 232 | buttons_text.done_button, required: No, description: Button text, type: String 233 | custom_variables, required: No, description: Dictionary of survey variables, type: Object 234 | footer, required: No (default=true), description: If false, SurveyMonkey’s footer is not displayed 235 | type: Boolean 236 | folder_id, required: No, description: If specified, adds the survey to the folder with that id. 237 | type: String 238 | :return: 239 | """ 240 | endpoint = "/surveys/{}".format(survey_id) 241 | url = API_URL + endpoint 242 | return self._patch(url, json=kwargs) 243 | 244 | def delete_survey(self, survey_id): 245 | """ 246 | Deletes a survey. 247 | :param survey_id: id of survey you want to delete 248 | :return: 249 | """ 250 | endpoint = "/surveys/{}".format(survey_id) 251 | url = API_URL + endpoint 252 | return self._delete(url) 253 | 254 | def get_survey_details(self, survey_id): 255 | """ 256 | Details of a specific survey 257 | :param survey_id: id of specific survey to get details from 258 | :return: 259 | """ 260 | endpoint = "/surveys/{0}/details".format(survey_id) 261 | url = API_URL + endpoint 262 | return self._get(url) 263 | 264 | def get_survey_categories(self): 265 | """ 266 | Returns a list of survey categories that can be used to filter survey templates. 267 | :return: 268 | """ 269 | endpoint = "/survey_categories" 270 | url = API_URL + endpoint 271 | return self._get(url) 272 | 273 | def get_survey_templates(self): 274 | """ 275 | Returns a list of survey templates. Survey template ids can be used as an argument to POST a new survey. 276 | :return: 277 | NOTE: for Teams -- Shared Team templates are not available through the API at this time. 278 | This endpoint returns SurveyMonkey’s template list. 279 | """ 280 | endpoint = "/survey_templates" 281 | url = API_URL + endpoint 282 | return self._get(url) 283 | 284 | def get_survey_languages(self): 285 | """ 286 | Returns a list of survey languages that can be used to generate translations for multilingual surveys 287 | :return: 288 | """ 289 | endpoint = "/survey_languages" 290 | url = API_URL + endpoint 291 | return self._get(url) 292 | 293 | def get_survey_pages(self, survey_id): 294 | """ 295 | Returns a survey page’s. 296 | :param survey_id: id of survey you want page's details 297 | :return: 298 | """ 299 | endpoint = "/surveys/{}/pages".format(survey_id) 300 | url = API_URL + endpoint 301 | return self._get(url) 302 | 303 | def create_new_empty_survey_page(self, survey_id, **kwags): 304 | """ 305 | Creates a new, empty page 306 | :param survey_id: id of survey you want page's details 307 | :param kwargs: dictionary with the following data 308 | title, required: No (default=“”), description: Page title, type: String 309 | description, required: No (default: “”), description: Page description, type: String 310 | position, required: No (default=end), description: Position of page in survey, type: Integer 311 | :return: 312 | """ 313 | endpoint = "/surveys/{}/pages".format(survey_id) 314 | url = API_URL + endpoint 315 | return self._post(url, json=kwags) 316 | 317 | def get_survey_page_details(self, survey_id, page_id): 318 | """ 319 | Returns a page’s details. 320 | :param survey_id: id of survey you want page's details 321 | :param page_id: id of page from which you want details 322 | :return: 323 | """ 324 | endpoint = "/surveys/{0}/pages/{1}".format(survey_id, page_id) 325 | url = API_URL + endpoint 326 | return self._get(url) 327 | 328 | def modify_survey_page(self, survey_id, page_id, **kwargs): 329 | """ 330 | Modifies a page (updates any fields accepted as arguments to POST /surveys{id}/pages). 331 | :param survey_id: id of page's survey 332 | :param page_id: id of page you want to modify or edit 333 | :param kwargs: dictionary with the following data 334 | title, required: No (default=“”), description: Page title, type: String 335 | description, required: No (default: “”), description: Page description, type: String 336 | position, required: No (default=end), description: Position of page in survey, type: Integer 337 | :return: 338 | """ 339 | endpoint = "/surveys/{0}/pages/{1}".format(survey_id, page_id) 340 | url = API_URL + endpoint 341 | return self._patch(url, json=kwargs) 342 | 343 | def delete_survey_page(self, survey_id, page_id): 344 | """ 345 | Deletes a page. 346 | :param survey_id: id of page's survey 347 | :param page_id: id of page you want to delete 348 | :return: 349 | """ 350 | endpoint = "/surveys/{0}/pages/{1}".format(survey_id, page_id) 351 | url = API_URL + endpoint 352 | return self._delete(url) 353 | 354 | def get_survey_page_questions(self, survey_id, page_id): 355 | """ 356 | Returns a list of questions on a survey page. 357 | :param survey_id: id of page's survey 358 | :param page_id: id of page you want to get the questions 359 | :return: 360 | """ 361 | endpoint = "/surveys/{0}/pages/{1}/questions".format(survey_id, page_id) 362 | url = API_URL + endpoint 363 | return self._get(url) 364 | 365 | def create_survey_page_question(self, survey_id, page_id, **kwargs): 366 | """ 367 | Creates a new question on a survey page. 368 | :param survey_id: id of page's survey 369 | :param page_id: id of page where you want to create the questions 370 | :param kwargs: dictionary with the following data 371 | title, required: No (default=“”), description: Page title, type: String 372 | headings, required: Yes, description:List of question headings objects, type: Array 373 | headings[_].heading, required: Yes, description:The title of the question, or empty string if 374 | random_assignment is defined, type: String 375 | headings[_].description, required: No, description:If random_assignment is defined, and family is 376 | presentation_image this is the title, type: String 377 | headings[_].image, required: No, description:Image data when question family is presentation_image, 378 | type: Object or null 379 | headings[_].image.url, required: No, description:URL of image when question family is presentation_image, 380 | type: String 381 | headings[_].random_assignment, required: No, description: Random assignment data, type: Object or null 382 | headings[_].random_assignment.percent, required: Yes, description: Percent chance of this random assignment 383 | showing up (must sum to 100), type: Integer 384 | headings[_].random_assignment.position, required: No, description: Position of the random_assignment in 385 | survey creation page, type: Integer 386 | headings[_].random_assignment.variable_name, required: No, description: Internal use name for question 387 | tracking, can be "", type: String 388 | position, required: No (default=last), description: Position of question on page, type: Integer 389 | family, required: Yes, description: Question family determines the type of question, see formatting 390 | question types, type: String 391 | subtype, required: Yes, description: Question family’s subtype further specifies the type of question, 392 | see formatting question types, type: String 393 | sorting, required: No, description:Sorting details of answers, type: Object 394 | sorting.type, required: Yes, description:Sort answer choices by: default, textasc, textdesc, 395 | resp_count_asc, resp_count_desc, random, flip, type: String-ENUM 396 | sorting.ignore_last, required: No, description:If true, does not sort the last answer option 397 | (useful for “none of the above”, etc), type: Boolean 398 | required, required: No, description: Whether an answer is required for this question, type: Object 399 | required.text, required: Yes, description: Text to display if a required question is not answered, 400 | type: String 401 | required.type, required: Yes if question is matrix_single, matrix_ranking, and matrix menu, 402 | description: Specifies how much of the question must be answered: all , at_least, at_most, exactly, 403 | or range, type: String-ENUM 404 | required.amount, required: Yes if type is defined, description: The amount of answers required to be 405 | answered. If the required type is range then this is two numbers separated by a comma, as a string 406 | (e.g. “1,3” to represent the range of 1 to 3) String validation, required: No, 407 | description: Whether the answer must pass certain validation parameters, type: Object 408 | validation.type, required: Yes, description: Type of validation to use: any, integer, decimal, 409 | date_us, date_intl, regex, email, or text_length, type: String-ENUM 410 | validation.text, required: Yes, description:Text to display if validation fails String 411 | validation.min, required: Yes, description: Minimum value an answer can be to pass validation, 412 | type: Date string, integer, or null depending on validation.type 413 | validation.max, required: Yes, description: Maximum value an answer can be to pass validation, 414 | type: Date string, integer, or null depending on validation.type 415 | validation.sum, required: No, description: Only accepted is family=open_ended and subtype=numerical, 416 | the exact integer textboxes must sum to in order to pass validation, type: Integer 417 | validation.sum_text, required: No, description: Only accepted is family=open_ended and subtype=numerical, 418 | the message to display if textboxes do not sum to validation.sum, type: String 419 | forced_ranking, required: No, description: Required if type is matrix and subtype is rating or single, 420 | whether or not to force ranking, type: Boolean 421 | quiz_options, required: No, description: Object containing the quiz properties of this question, 422 | if quiz-mode is enabled 423 | quiz_options.scoring_enabled, required: Yes, description: Whether this question is quiz-enabled, 424 | type: Boolean 425 | quiz_options.feedback, required: Yes, description: Object containing the definitions for feedback on 426 | this quiz question, type: Object 427 | quiz_options.feedback.correct_text, required: Yes, description: Text to show when answer is correct, 428 | type: String 429 | quiz_options.feedback.incorrect_text, required: Yes, description: Text to show when the ansewr is 430 | incorrect, type: String 431 | quiz_options.feedback.partial_text, required: Yes, description: Text to show when the answer is 432 | partially correct, type: String 433 | answers, required: Yes for all question types except open_ended_single, description: Answers object, 434 | refer to Formatting Question Types, type: Object 435 | display_options, required: Yes for File Upload, Slider, Image Choice, & Emoji/Star Rating question types, 436 | description: Display option object, refer to Formatting Question Types, type: Object 437 | :return: 438 | """ 439 | endpoint = "/surveys/{0}/pages/{1}/questions".format(survey_id, page_id) 440 | url = API_URL + endpoint 441 | return self._post(url, json=kwargs) 442 | 443 | def get_specific_question(self, survey_id, page_id, question_id): 444 | """ 445 | Returns a question. 446 | :param survey_id: id of survey 447 | :param page_id: id of page 448 | :param question_id: id of question 449 | :return: 450 | """ 451 | endpoint = "/surveys/{0}/pages/{1}/questions/{2}".format(survey_id, page_id, question_id) 452 | url = API_URL + endpoint 453 | return self._get(url) 454 | 455 | def modify_specific_question(self, survey_id, page_id, question_id, **kwargs): 456 | """ 457 | 458 | :param survey_id: id of survey 459 | :param page_id: id of page 460 | :param question_id: id of question 461 | :param kwargs: dictionary with the following data 462 | title, required: No (default=“”), description: Page title, type: String 463 | headings, required: Yes, description:List of question headings objects, type: Array 464 | headings[_].heading, required: Yes, description:The title of the question, or empty string if 465 | random_assignment is defined, type: String 466 | headings[_].description, required: No, description:If random_assignment is defined, and family is 467 | presentation_image this is the title, type: String 468 | headings[_].image, required: No, description:Image data when question family is presentation_image, 469 | type: Object or null 470 | headings[_].image.url, required: No, description:URL of image when question family is presentation_image, 471 | type: String 472 | headings[_].random_assignment, required: No, description: Random assignment data, type: Object or null 473 | headings[_].random_assignment.percent, required: Yes, description: Percent chance of this random assignment 474 | showing up (must sum to 100), type: Integer 475 | headings[_].random_assignment.position, required: No, description: Position of the random_assignment in 476 | survey creation page, type: Integer 477 | headings[_].random_assignment.variable_name, required: No, description: Internal use name for question 478 | tracking, can be "", type: String 479 | position, required: No (default=last), description: Position of question on page, type: Integer 480 | family, required: Yes, description: Question family determines the type of question, see formatting 481 | question types, type: String 482 | subtype, required: Yes, description: Question family’s subtype further specifies the type of question, 483 | see formatting question types, type: String 484 | sorting, required: No, description:Sorting details of answers, type: Object 485 | sorting.type, required: Yes, description:Sort answer choices by: default, textasc, textdesc, 486 | resp_count_asc, resp_count_desc, random, flip, type: String-ENUM 487 | sorting.ignore_last, required: No, description:If true, does not sort the last answer option 488 | (useful for “none of the above”, etc), type: Boolean 489 | required, required: No, description: Whether an answer is required for this question, type: Object 490 | required.text, required: Yes, description: Text to display if a required question is not answered, 491 | type: String 492 | required.type, required: Yes if question is matrix_single, matrix_ranking, and matrix menu, 493 | description: Specifies how much of the question must be answered: all , at_least, at_most, exactly, 494 | or range, type: String-ENUM 495 | required.amount, required: Yes if type is defined, description: The amount of answers required to be 496 | answered. If the required type is range then this is two numbers separated by a comma, as a string 497 | (e.g. “1,3” to represent the range of 1 to 3) String validation, required: No, 498 | description: Whether the answer must pass certain validation parameters, type: Object 499 | validation.type, required: Yes, description: Type of validation to use: any, integer, decimal, 500 | date_us, date_intl, regex, email, or text_length, type: String-ENUM 501 | validation.text, required: Yes, description:Text to display if validation fails String 502 | validation.min, required: Yes, description: Minimum value an answer can be to pass validation, 503 | type: Date string, integer, or null depending on validation.type 504 | validation.max, required: Yes, description: Maximum value an answer can be to pass validation, 505 | type: Date string, integer, or null depending on validation.type 506 | validation.sum, required: No, description: Only accepted is family=open_ended and subtype=numerical, 507 | the exact integer textboxes must sum to in order to pass validation, type: Integer 508 | validation.sum_text, required: No, description: Only accepted is family=open_ended and subtype=numerical, 509 | the message to display if textboxes do not sum to validation.sum, type: String 510 | forced_ranking, required: No, description: Required if type is matrix and subtype is rating or single, 511 | whether or not to force ranking, type: Boolean 512 | quiz_options, required: No, description: Object containing the quiz properties of this question, 513 | if quiz-mode is enabled 514 | quiz_options.scoring_enabled, required: Yes, description: Whether this question is quiz-enabled, 515 | type: Boolean 516 | quiz_options.feedback, required: Yes, description: Object containing the definitions for feedback on 517 | this quiz question, type: Object 518 | quiz_options.feedback.correct_text, required: Yes, description: Text to show when answer is correct, 519 | type: String 520 | quiz_options.feedback.incorrect_text, required: Yes, description: Text to show when the ansewr is 521 | incorrect, type: String 522 | quiz_options.feedback.partial_text, required: Yes, description: Text to show when the answer is 523 | partially correct, type: String 524 | answers, required: Yes for all question types except open_ended_single, description: Answers object, 525 | refer to Formatting Question Types, type: Object 526 | display_options, required: Yes for File Upload, Slider, Image Choice, & Emoji/Star Rating question types, 527 | description: Display option object, refer to Formatting Question Types, type: Object 528 | :return: 529 | """ 530 | endpoint = "/surveys/{0}/pages/{1}/questions/{2}".format(survey_id, page_id, question_id) 531 | url = API_URL + endpoint 532 | return self._patch(url, json=kwargs) 533 | 534 | def delete_question(self, survey_id, page_id, question_id): 535 | """ 536 | Deletes a question. 537 | :param survey_id: id of survey 538 | :param page_id: id of page 539 | :param question_id: id of question 540 | :return: 541 | """ 542 | endpoint = "/surveys/{0}/pages/{1}/questions/{2}".format(survey_id, page_id, question_id) 543 | url = API_URL + endpoint 544 | return self._delete(url) 545 | 546 | def get_questions_bank(self): 547 | """ 548 | Get a list of questions that exist in the question bank 549 | :return: 550 | """ 551 | endpoint = "/question_bank/questions" 552 | url = API_URL + endpoint 553 | return self._get(url) 554 | 555 | def get_survey_response(self, survey_id): 556 | """ 557 | Returns a list of responses. 558 | :param survey_id: id of survey 559 | :return: 560 | """ 561 | endpoint = "/surveys/{}/responses/".format(survey_id) 562 | url = API_URL + endpoint 563 | return self._get(url) 564 | 565 | def get_survey_response_bulk(self, survey_id): 566 | """ 567 | Retrieves a list of full expanded responses, including answers to all questions. 568 | :param survey_id: id of survey 569 | :return: 570 | """ 571 | endpoint = "/surveys/{}/responses/bulk".format(survey_id) 572 | url = API_URL + endpoint 573 | return self._get(url) 574 | 575 | def create_survey_from_template_or_existing_survey(self, title=None, template_id=None, survey_id=None): 576 | """ 577 | Creates a new survey from a template using the template_id or existing survey using the survey_id 578 | :param title: Survey title, not required, String 579 | :param template_id: Survey template to copy from, not required, String 580 | :param survey_id: Survey id to copy from, not required, String 581 | :return: 582 | """ 583 | payload = { 584 | "title": title, 585 | "from_template_id": template_id, 586 | "from_survey_id": survey_id 587 | } 588 | endpoint = "/surveys" 589 | url = API_URL + endpoint 590 | return self._post(url, json=payload) 591 | 592 | def create_new_blank_survey(self, **kwargs): 593 | """ 594 | Creates a new empty survey 595 | :param kwargs: dictionary with the following data. 596 | title, required: No (default=“New Survey”), description: Survey title, type: String 597 | nickname, required: No (default=“”), description: Survey nickname, type: String 598 | language, required: No (default=“en”), description: Survey language, type: String 599 | buttons_text, required: No, description: Survey Buttons text container, type: Object 600 | buttons_text.next_button, required: No, description: Button text, type: String 601 | buttons_text.prev_button, required: No, description: Button text, type: String 602 | buttons_text.exit_button, required: No, description: Button text. If set to an empty string, 603 | button will be ommitted from survey, type: String 604 | buttons_text.done_button, required: No, description: Button text, type: String 605 | custom_variables, required: No, description: Dictionary of survey variables, type: Object 606 | footer, required: No (default=true), description: If false, SurveyMonkey’s footer is not displayed, 607 | type: Boolean 608 | folder_id, required: No, description: If specified, adds the survey to the folder with that id. type: String 609 | quiz_options, required: No, description: An object describing the quiz settings, if this survey is a quiz 610 | type: Object 611 | quiz_options.is_quiz_mode, required: Yes, description: On/off toggle for setting this survey as a quiz 612 | type: Boolean 613 | quiz_options.default_question_feedback, required: No, description: An object containing the default 614 | feedback for newly created questions in this survey, type: Object 615 | quiz_options.default_question_feedback.correct_text, required: Yes, description: Text to show when answer 616 | is correct, type: String 617 | quiz_options.default_question_feedback.incorrect_text, required: Yes, description: Text to show when answer 618 | is incorrect, type: String 619 | quiz_options.default_question_feedback.partial_text, required: Yes, description: Text to show when answer 620 | is partially correct, type: String 621 | quiz_options.show_results_type, required: Yes, description: What to reveal to the user when they complete 622 | the quiz: disabled, results_only or results_and_answers, type: String-ENUM 623 | quiz_options.feedback, required: Yes, description: Text to show the user when they complete the quiz 624 | type: Object 625 | quiz_options.feedback.ranges, required: Yes, description: The ranges at which to show users certain 626 | feedback, type: Array 627 | quiz_options.feedback.ranges_type, required: Yes, description: Configure whether the following parameters 628 | use percentage or points. Note that these ranges are inclusive and may not overlap, type: String-ENUM 629 | quiz_options.feedback.ranges[_].min, required: Yes, description: Minimum score for this feedback, 630 | type: Integer 631 | quiz_options.feedback.ranges[_].max, required: Yes, description: Maximum score for this feedback, 632 | type: Integer 633 | quiz_options.feedback.ranges[_].message, required: Yes, description: Feedback message, type: String 634 | :return: 635 | """ 636 | endpoint = "/surveys" 637 | url = API_URL + endpoint 638 | return self._post(url, json=kwargs) 639 | 640 | def get_survey_folders(self): 641 | """ 642 | Returns available folders 643 | :return: 644 | """ 645 | endpoint = "/survey_folders" 646 | url = API_URL + endpoint 647 | return self._get(url) 648 | 649 | def create_survey_folder(self, **kwargs): 650 | """ 651 | 652 | :param kwargs: diccionarion con los siguientes valores 653 | Title, required: No, description: Title for the folder, type: String 654 | :return: 655 | """ 656 | endpoint = "/survey_folders" 657 | url = API_URL + endpoint 658 | return self._post(url, json=kwargs) 659 | 660 | def get_survey_translations(self, survey_id): 661 | """ 662 | Returns all existing translations 663 | :param survey_id: id of survey 664 | :return: 665 | """ 666 | endpoint = "/surveys/{0}/languages".format(survey_id) 667 | url = API_URL + endpoint 668 | return self._get(url) 669 | 670 | def get_responses(self, survey_id): 671 | """ 672 | List all responses given to a specific survey. 673 | :param survey_id: id of specific survey to from 674 | :return: 675 | """ 676 | endpoint = "/surveys/{0}/responses/".format(survey_id) 677 | url = API_URL + endpoint 678 | return self._get(url) 679 | 680 | def get_response_bulk(self, survey_id,params=None): 681 | """ 682 | Retrieves a list of full expanded responses, including answers to all questions 683 | :param survey_id: id of survey to responses from 684 | :param params: a dict of params to add to the request, possible values can be 685 | found at https://developer.surveymonkey.com/api/v3/#surveys-id-responses-bulk 686 | :return: 687 | """ 688 | endpoint = "/surveys/{0}/responses/bulk".format(survey_id) 689 | 690 | url = API_URL + endpoint 691 | return self._get(url,params=params) 692 | 693 | def get_all_pages_response(self, survey_id, per_page=100) -> list: 694 | """Get bulk responses from all pages. 695 | 696 | :return list of bulk responses""" 697 | all_resp = [] 698 | params = {'per_page': per_page} 699 | resp = self.get_response_bulk(survey_id, params=params) 700 | pages = ceil(resp['total']/per_page) 701 | if pages > 1: 702 | all_resp.append(resp) 703 | if pages == 2: 704 | params = {'per_page': per_page, 705 | 'page': 2} 706 | all_resp.append(self.get_response_bulk(survey_id, params=params)) 707 | else: 708 | for page in range(2, pages+1, 1): 709 | params = {'per_page': per_page, 710 | 'page': page} 711 | all_resp.append(self.get_response_bulk(survey_id, params=params)) 712 | return all_resp 713 | else: 714 | return [resp] 715 | 716 | def get_response_details(self, survey_id, response_id): 717 | """ 718 | List answers of a survey given by an user. 719 | answers correspond to every question in a survey. 720 | response is survey completed by an user. 721 | :param survey_id: id of specific survey to get responses from 722 | :param response_id: id of specific response for a survey 723 | :return: 724 | 725 | """ 726 | endpoint = "/surveys/{0}/responses/{1}/details".format(survey_id, response_id) 727 | url = API_URL + endpoint 728 | return self._get(url) 729 | 730 | # Communications 731 | def _get(self, endpoint, **kwargs): 732 | return self._request('GET', endpoint, **kwargs) 733 | 734 | def _post(self, endpoint, **kwargs): 735 | return self._request('POST', endpoint, **kwargs) 736 | 737 | def _put(self, endpoint, **kwargs): 738 | return self._request('PUT', endpoint, **kwargs) 739 | 740 | def _patch(self, endpoint, **kwargs): 741 | return self._request('PATCH', endpoint, **kwargs) 742 | 743 | def _delete(self, endpoint, **kwargs): 744 | return self._request('DELETE', endpoint, **kwargs) 745 | 746 | def _request(self, method, url, **kwargs): 747 | _headers = {"Authorization": "Bearer %s" % self._access_token, "Content-Type": "application/json"} 748 | return self._parse(requests.request(method, url, headers=_headers, **kwargs)) 749 | 750 | def _parse(self, response): 751 | status_code = response.status_code 752 | if 'application/json' in response.headers['Content-Type']: 753 | r = response.json() 754 | else: 755 | r = response.text 756 | if "error" in r: 757 | self.get_error(dict(r)) 758 | if "error" not in r and status_code not in [200, 201, 204]: 759 | raise UnknownError() 760 | if status_code in (200, 201): 761 | return r 762 | if status_code == 204: 763 | return None 764 | 765 | def get_error(self, error): 766 | """ 767 | 768 | :return: 769 | """ 770 | try: 771 | error_code = error['error']['id'] 772 | error_message = error['error']['message'] 773 | except: 774 | error_code = error['error'] 775 | error_message = error['message'] 776 | if error_code == "1000": 777 | raise BadRequestError(error_message) 778 | elif error_code == "1001": 779 | raise BadRequestError(error_message) 780 | elif error_code == "1002": 781 | raise BadRequestError(error_message) 782 | elif error_code == "1003": 783 | raise BadRequestError(error_message) 784 | elif error_code == "1004": 785 | raise BadRequestError(error_message) 786 | elif error_code == "1010": 787 | raise AuthorizationError(error_message) 788 | elif error_code == "1011": 789 | raise AuthorizationError(error_message) 790 | elif error_code == "1012": 791 | raise AuthorizationError(error_message) 792 | elif error_code == "1013": 793 | raise AuthorizationError(error_message) 794 | elif error_code == "1014": 795 | raise PermissionError(error_message) 796 | elif error_code == "1015": 797 | raise PermissionError(error_message) 798 | elif error_code == "1016": 799 | raise PermissionError(error_message) 800 | elif error_code == "1017": 801 | raise PermissionError(error_message) 802 | elif error_code == "1018": 803 | raise PermissionError(error_message) 804 | elif error_code == "1020": 805 | raise ResourceNotFoundError(error_message) 806 | elif error_code == "1025": 807 | raise ResourceConflictError(error_message) 808 | elif error_code == "1026": 809 | raise ResourceConflictError(error_message) 810 | elif error_code == "1030": 811 | raise RequestEntityTooLargeError(error_message) 812 | elif error_code == "1040": 813 | raise RateLimitReachedError(error_message) 814 | elif error_code == "1050": 815 | raise InternalServerError(error_message) 816 | elif error_code == "1051": 817 | raise InternalServerError(error_message) 818 | elif error_code == "1052": 819 | raise UserSoftDeletedError(error_message) 820 | elif error_code == "1053": 821 | raise UserDeletedError(error_message) 822 | else: 823 | raise UnknownError("UNKNOWN ERROR: {}".format(error['message'])) 824 | --------------------------------------------------------------------------------