diff --git a/.gitignore b/.gitignore index 894a44c..b580a8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# IDE files +.idea +.vscode + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b71981..5adb243 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,9 +6,10 @@ This is an InnerSource python project. It is the work of someone who thought it This repository is maintained by -1. [Ridley Larsen](@RidleyLarsen) -2. [Bradley Wogsland](@wogsland) -3. [Nathan Workman](@nathanworkman) +1. [Ridley Larsen](https://github.com/RidleyLarsen) +1. [Uthayakumar Kumarasamy](https://github.com/ukumark) +1. [Bradley Wogsland](https://github.com/wogsland) +1. [Nathan Workman](https://github.com/nathanworkman) ### Community Guidelines diff --git a/README.md b/README.md index 0b37c02..a9e7510 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ Links to the Demand API documentation are included for each function. [Get Project Detailed Report](https://developers.dynata.com/demand-api-reference/core-resources/projects/get-project-detailed-report): get_project_detailed_report(project_id) [Get Pricing & Feasibility](https://developers.dynata.com/demand-api-reference/core-resources/pricing-feasibility/get-pricing-feasibility): get_feasibility(project_id) [Get Invoice PDF](https://developers.dynata.com/demand-api-reference/billing_invoicing/invoicing/get-invoices): get_invoice(project_id) +[Get Invoices Summary PDF](https://developers.dynata.com): get_invoices_summary(\*\*kwargs) ### Line Item Functions @@ -72,7 +73,9 @@ Links to the Demand API documentation are included for each function. [Launch Line Item](https://developers.dynata.com/demand-api-reference/core-resources/lineitems/post-lineitem-launch): launch_line_item(project_id, line_item_id) [Pause Line Item](https://developers.dynata.com/demand-api-reference/core-resources/lineitems/post-lineitem-pause): pause_line_item(project_id, line_item_id) [Update Line Item](https://developers.dynata.com/demand-api-reference/core-resources/lineitems/post-lineitem): update_line_item(project_id, line_item_id, line_item_data) -[Get Line Item Detailed Report](https://developers.dynata.com/demand-api-reference/core-resources/lineitems/get-detailed-line-item): get_line_item_detailed_report(project_id, line_item_id) +[Get Line Item Detailed Report](https://developers.dynata.com/demand-api-reference/core-resources/lineitems/get-detailed-line-item): get_line_item_detailed_report(project_id, line_item_id) +[Launch Quota cell](https://developers.dynata.com/demand-api-reference/core-resources/lineitems/post-quota-cell-launch): set_quotacell_status(project_id, line_item_id, quota_cell_id, launch) +[Pause Quota cell](https://developers.dynata.com/demand-api-reference/core-resources/lineitems/post-quota-cell-pause): set_quotacell_status(project_id, line_item_id, quota_cell_id, pause) ### Misc Functions diff --git a/dynatademand/api.py b/dynatademand/api.py index ed95cbd..939e90a 100644 --- a/dynatademand/api.py +++ b/dynatademand/api.py @@ -72,6 +72,23 @@ def _api_get(self, uri, query_params=None): return response.content return response.json() + def _api_delete(self, uri): + # Send an authenticated DELETE request to an API endpoint. + self._check_authentication() + url = '{}{}'.format(self.base_url, uri) + request_headers = { + 'Authorization': 'Bearer {}'.format(self._access_token), + 'Content-Type': "application/json", + } + response = requests.delete(url=url, headers=request_headers) + if response.status_code > 399: + raise DemandAPIError('Demand API request to {} failed with status {}. Response: {}'.format( + url, response.status_code, response.content + )) + if response.headers['content-type'] == 'application/pdf': + return response.content + return response.json() + def authenticate(self): # Sends the authentication data to the access token endpoint. url = '{}/token/password'.format(self.auth_base_url) @@ -158,6 +175,9 @@ def get_countries(self, **kwargs): ) return self._api_get('/countries', kwargs) + def get_study_metadata(self): + return self._api_get('/studyMetadata') + def get_event(self, event_id): self.validator.validate_request( 'get_event', @@ -271,6 +291,44 @@ def get_project_detailed_report(self, project_id): ) return self._api_get('/projects/{}/detailedReport'.format(project_id)) + def get_user_info(self): + return self._api_get('/user') + + def get_company_users(self): + return self._api_get('/users') + + def get_company_teams(self): + return self._api_get('/teams') + + def get_roles(self, **kwargs): + self.validator.validate_request( + 'get_roles', + query_params=kwargs + ) + return self._api_get('/roles', kwargs) + + def get_project_permissions(self, project_id): + self.validator.validate_request( + 'get_project_permissions', + path_data={'extProjectId': '{}'.format(project_id)}, + ) + return self._api_get('/projects/{}/permissions'.format(project_id)) + + def upsert_project_permissions(self, project_id, upsert_permissions_data): + self.validator.validate_request( + 'upsert_project_permissions', + path_data={'extProjectId': '{}'.format(project_id)}, + request_body=upsert_permissions_data, + ) + response_data = self._api_post('/projects/{}/permissions'.format(project_id), upsert_permissions_data) + if response_data.get('status').get('message') != 'success': + raise DemandAPIError( + "Could not upsert project permissions. Demand API responded with: {}".format( + response_data + ) + ) + return response_data + def add_line_item(self, project_id, lineitem_data): ''' A line item is a project entity that exist for a specific market and @@ -352,6 +410,27 @@ def pause_line_item(self, project_id, line_item_id): ) return response_data + def set_quotacell_status(self, project_id, line_item_id, quota_cell_id, action): + # Stops traffic to a line item. + self.validator.validate_request( + 'set_quotacell_status', + path_data={ + 'extProjectId': '{}'.format(project_id), + 'extLineItemId': '{}'.format(line_item_id), + 'quotaCellId': '{}'.format(quota_cell_id), + 'action': '{}'.format(action), + }, + ) + response_data = self._api_post('/projects/{}/lineItems/{}/quotaCells/{}/{}'.format( + project_id, line_item_id, quota_cell_id, action), {}) + if response_data.get('status').get('message') != 'success': + raise DemandAPIError( + "Could not {} quotacell. Demand API responded with: {}".format( + action, response_data + ) + ) + return response_data + def get_line_item(self, project_id, line_item_id): self.validator.validate_request( 'get_line_item', @@ -425,6 +504,13 @@ def get_sources(self): ) return self._api_get('/sources') + def get_invoices_summary(self, **kwargs): + self.validator.validate_request( + 'get_invoices_summary', + query_params=kwargs + ) + return self._api_get('/projects/invoices/summary', kwargs) + def reconcile_project(self, project_id, file, message): ''' Sends a reconciliation request @@ -454,3 +540,38 @@ def reconcile_project(self, project_id, file, message): url, response.status_code, response.content )) return response.json() + + def create_template(self, template): + # TODO: Waiting on a valid path and request body schema. + # self.validator.validate_request( + # 'create_template', + # request_body=template, + # ) + return self._api_post('/templates/quotaplan', template) + + def update_template(self, id, template): + # TODO: Waiting on a valid path and request body schema. + # self.validator.validate_request( + # 'update_template', + # path_data={'id': '{}'.format(id)}, + # request_body=template, + # ) + return self._api_post('/templates/quotaplan/{}'.format(id), template) + + def delete_template(self, id): + self.validator.validate_request( + 'delete_template', + path_data={'id': '{}'.format(id)}, + ) + return self._api_delete('/templates/quotaplan/{}'.format(id)) + + def get_templates(self, country, lang, **kwargs): + self.validator.validate_request( + 'get_templates', + path_data={ + 'countryCode': '{}'.format(country), + 'languageCode': '{}'.format(lang) + }, + query_params=kwargs, + ) + return self._api_get('/templates/quotaplan/{}/{}'.format(country, lang), kwargs) diff --git a/dynatademand/schemas/request/body/buy_project.json b/dynatademand/schemas/request/body/buy_project.json index 02e3793..70ec56b 100644 --- a/dynatademand/schemas/request/body/buy_project.json +++ b/dynatademand/schemas/request/body/buy_project.json @@ -12,7 +12,8 @@ "type": "string" }, "surveyURL": { - "type": "string" + "type": "string", + "maxLength": 2000 }, "surveyTestURL": { "type": "string" diff --git a/dynatademand/schemas/request/body/create_line_item.json b/dynatademand/schemas/request/body/create_line_item.json index e9deff7..7ab161e 100644 --- a/dynatademand/schemas/request/body/create_line_item.json +++ b/dynatademand/schemas/request/body/create_line_item.json @@ -24,6 +24,7 @@ }, "surveyURL": { "type": "string", + "maxLength": 2000, "description": "Survey URL to send panelist into. " }, "surveyTestURL": { @@ -45,12 +46,7 @@ "deliveryType": { "type": "string", "default": "BALANCED", - "description": "The plan on how responses will flow into the survey.", - "enum": [ - "SLOW", - "BALANCED", - "FAST" - ] + "description": "The plan on how responses will flow into the survey." }, "sources": { "type": "array", diff --git a/dynatademand/schemas/request/body/create_project.json b/dynatademand/schemas/request/body/create_project.json index dcd496a..575f5b5 100644 --- a/dynatademand/schemas/request/body/create_project.json +++ b/dynatademand/schemas/request/body/create_project.json @@ -55,6 +55,20 @@ "type": "string", "minLength": 1 } + }, + "studyType": { + "type": "array", + "description": "List of possible types of survey.", + "items": { + "type": "string" + } + }, + "surveyRequirements": { + "type": "array", + "description": "List of possible requirements for a survey.", + "items": { + "type": "string" + } } } }, @@ -97,6 +111,7 @@ }, "surveyURL": { "type": "string", + "maxLength": 2000, "description": "Survey URL to send panelist into. " }, "surveyTestURL": { @@ -118,12 +133,7 @@ "deliveryType": { "type": "string", "default": "BALANCED", - "description": "The plan on how responses will flow into the survey.", - "enum": [ - "SLOW", - "BALANCED", - "FAST" - ] + "description": "The plan on how responses will flow into the survey." }, "sources": { "type": "array", diff --git a/dynatademand/schemas/request/body/create_template.json b/dynatademand/schemas/request/body/create_template.json new file mode 100644 index 0000000..1d0f011 --- /dev/null +++ b/dynatademand/schemas/request/body/create_template.json @@ -0,0 +1,131 @@ +{ + "type": "object", + "title": "New Template", + "properties": { + "name": { + "type": "string", + "description": "Name of the template" + }, + "description": { + "type": "string", + "description": "template description" + }, + "countryISOCode": { + "type": "string", + "description": "2 letter ISO Country Code", + "minLength": 2, + "maxLength": 2 + }, + "languageISOCode": { + "type": "string", + "description": "2 letter ISO Language Code", + "minLength": 2, + "maxLength": 2 + }, + "tags": { + "type": "array", + "description": "keyword tags" + }, + "quotaPlan": { + "type": "object", + "title": "Quota Plan", + "description": "Defines the type of respondents you want to invite for the survey", + "properties": { + "filters": { + "type": "array", + "description": "Filters are minimum set of targeting that every respondent must have in order to qualify for the study. Only attributes that have `isAllowedInFilters = true` is allowed to be used in `filters`", + "items": { + "type": "object", + "properties": { + "attributeId": { + "type": "string", + "description": "The attribute you want to target respondents on" + }, + "options": { + "type": "array", + "description": "The options of the attribute you want to target respondents on", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "operator": { + "type": "string", + "enum": [ + "exclude", + "include" + ], + "default": "include", + "description": "The operator to use for the attribute options." + } + } + } + }, + "quotaGroups": { + "type": "array", + "description": "Quota groups define the allocated targeting attributes for panelists within this line item. Only attributes that have `isAllowedInQuotas = true` is allowed in `quotaGroups`.", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "A quota group name of your choosing" + }, + "quotaCells": { + "type": "array", + "description": "Quota Cells define the percentage allocation for the required targeting. A quota cell is made up of a collection of quota Nodes", + "items": { + "type": "object", + "properties": { + "quotaNodes": { + "type": "array", + "description": "Quota Nodes define the collection of attributes and options being targeted.", + "items": { + "type": "object", + "properties": { + "attributeId": { + "type": "string", + "description": "The attribute you want to target respondents on" + }, + "options": { + "type": "array", + "description": "The options of the attribute you want to target respondents on", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "operator": { + "type": "string", + "enum": [ + "exclude", + "include" + ], + "default": "include", + "description": "**Deprecated field** The operator to use for the attribute options." + } + } + } + }, + "count": { + "type": "integer", + "description": "The count of respondents you want to qualify for the defined quota cell" + } + } + } + } + } + } + } + } + } + }, + "required": [ + "name", + "countryISOCode", + "languageISOCode", + "description", + "quotaPlan" + ] + } + \ No newline at end of file diff --git a/dynatademand/schemas/request/body/update_line_item.json b/dynatademand/schemas/request/body/update_line_item.json index 14088bb..f12b8ec 100644 --- a/dynatademand/schemas/request/body/update_line_item.json +++ b/dynatademand/schemas/request/body/update_line_item.json @@ -11,7 +11,8 @@ "type": "string" }, "surveyURL": { - "type": "string" + "type": "string", + "maxLength": 2000 }, "surveyTestURL": { "type": "string" diff --git a/dynatademand/schemas/request/body/update_project.json b/dynatademand/schemas/request/body/update_project.json index afe6bad..dfca5fd 100644 --- a/dynatademand/schemas/request/body/update_project.json +++ b/dynatademand/schemas/request/body/update_project.json @@ -25,6 +25,18 @@ "items": { "type": "string" } + }, + "studyType": { + "type": "array", + "items": { + "type": "string" + } + }, + "studyRequirements": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -50,7 +62,8 @@ "type": "string" }, "surveyURL": { - "type": "string" + "type": "string", + "maxLength": 2000 }, "surveyTestURL": { "type": "string" diff --git a/dynatademand/schemas/request/body/update_template.json b/dynatademand/schemas/request/body/update_template.json new file mode 100644 index 0000000..1d0f011 --- /dev/null +++ b/dynatademand/schemas/request/body/update_template.json @@ -0,0 +1,131 @@ +{ + "type": "object", + "title": "New Template", + "properties": { + "name": { + "type": "string", + "description": "Name of the template" + }, + "description": { + "type": "string", + "description": "template description" + }, + "countryISOCode": { + "type": "string", + "description": "2 letter ISO Country Code", + "minLength": 2, + "maxLength": 2 + }, + "languageISOCode": { + "type": "string", + "description": "2 letter ISO Language Code", + "minLength": 2, + "maxLength": 2 + }, + "tags": { + "type": "array", + "description": "keyword tags" + }, + "quotaPlan": { + "type": "object", + "title": "Quota Plan", + "description": "Defines the type of respondents you want to invite for the survey", + "properties": { + "filters": { + "type": "array", + "description": "Filters are minimum set of targeting that every respondent must have in order to qualify for the study. Only attributes that have `isAllowedInFilters = true` is allowed to be used in `filters`", + "items": { + "type": "object", + "properties": { + "attributeId": { + "type": "string", + "description": "The attribute you want to target respondents on" + }, + "options": { + "type": "array", + "description": "The options of the attribute you want to target respondents on", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "operator": { + "type": "string", + "enum": [ + "exclude", + "include" + ], + "default": "include", + "description": "The operator to use for the attribute options." + } + } + } + }, + "quotaGroups": { + "type": "array", + "description": "Quota groups define the allocated targeting attributes for panelists within this line item. Only attributes that have `isAllowedInQuotas = true` is allowed in `quotaGroups`.", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "A quota group name of your choosing" + }, + "quotaCells": { + "type": "array", + "description": "Quota Cells define the percentage allocation for the required targeting. A quota cell is made up of a collection of quota Nodes", + "items": { + "type": "object", + "properties": { + "quotaNodes": { + "type": "array", + "description": "Quota Nodes define the collection of attributes and options being targeted.", + "items": { + "type": "object", + "properties": { + "attributeId": { + "type": "string", + "description": "The attribute you want to target respondents on" + }, + "options": { + "type": "array", + "description": "The options of the attribute you want to target respondents on", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "operator": { + "type": "string", + "enum": [ + "exclude", + "include" + ], + "default": "include", + "description": "**Deprecated field** The operator to use for the attribute options." + } + } + } + }, + "count": { + "type": "integer", + "description": "The count of respondents you want to qualify for the defined quota cell" + } + } + } + } + } + } + } + } + } + }, + "required": [ + "name", + "countryISOCode", + "languageISOCode", + "description", + "quotaPlan" + ] + } + \ No newline at end of file diff --git a/dynatademand/schemas/request/body/upsert_project_permissions.json b/dynatademand/schemas/request/body/upsert_project_permissions.json new file mode 100644 index 0000000..10416d2 --- /dev/null +++ b/dynatademand/schemas/request/body/upsert_project_permissions.json @@ -0,0 +1,30 @@ +{ + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "role": { + "type": "string" + } + } + } + }, + "teams": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "integer" + } + } + } + } + } +} diff --git a/dynatademand/schemas/request/path/create_template.json b/dynatademand/schemas/request/path/create_template.json new file mode 100644 index 0000000..ee03c17 --- /dev/null +++ b/dynatademand/schemas/request/path/create_template.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string", + "required": true + } + }, + "required": [ + "name" + ] + } \ No newline at end of file diff --git a/dynatademand/schemas/request/path/delete_template.json b/dynatademand/schemas/request/path/delete_template.json new file mode 100644 index 0000000..70f4d06 --- /dev/null +++ b/dynatademand/schemas/request/path/delete_template.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "properties": { + "id": { + "type": "integer", + "required": true + } + }, + "required": [ + "id" + ] +} \ No newline at end of file diff --git a/dynatademand/schemas/request/path/get_project_permissions.json b/dynatademand/schemas/request/path/get_project_permissions.json new file mode 100644 index 0000000..42fe507 --- /dev/null +++ b/dynatademand/schemas/request/path/get_project_permissions.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "properties": { + "extProjectId": { + "type": "string", + "required": true + } + }, + "required": [ + "extProjectId" + ] +} diff --git a/dynatademand/schemas/request/path/get_templates.json b/dynatademand/schemas/request/path/get_templates.json new file mode 100644 index 0000000..f9bd6f9 --- /dev/null +++ b/dynatademand/schemas/request/path/get_templates.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "properties": { + "countryCode": { + "type": "string", + "required": true + }, + "languageCode": { + "type": "string", + "required": true + } + }, + "required": [ + "countryCode", + "languageCode" + ] +} \ No newline at end of file diff --git a/dynatademand/schemas/request/path/set_quotacell_status.json b/dynatademand/schemas/request/path/set_quotacell_status.json new file mode 100644 index 0000000..e4b1d83 --- /dev/null +++ b/dynatademand/schemas/request/path/set_quotacell_status.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "properties": { + "extProjectId": { + "type": "string", + "required": true + }, + "extLineItemId": { + "type": "string", + "required": true + }, + "quotaCellId": { + "type": "string", + "required": true + }, + "action": { + "type": "string", + "required": true + } + }, + "required": [ + "extProjectId", + "extLineItemId", + "quotaCellId", + "action" + ] +} \ No newline at end of file diff --git a/dynatademand/schemas/request/path/update_template.json b/dynatademand/schemas/request/path/update_template.json new file mode 100644 index 0000000..ee03c17 --- /dev/null +++ b/dynatademand/schemas/request/path/update_template.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string", + "required": true + } + }, + "required": [ + "name" + ] + } \ No newline at end of file diff --git a/dynatademand/schemas/request/path/upsert_project_permissions.json b/dynatademand/schemas/request/path/upsert_project_permissions.json new file mode 100644 index 0000000..42fe507 --- /dev/null +++ b/dynatademand/schemas/request/path/upsert_project_permissions.json @@ -0,0 +1,12 @@ +{ + "type": "object", + "properties": { + "extProjectId": { + "type": "string", + "required": true + } + }, + "required": [ + "extProjectId" + ] +} diff --git a/dynatademand/schemas/request/query/get_invoices_summary.json b/dynatademand/schemas/request/query/get_invoices_summary.json new file mode 100644 index 0000000..94f62ef --- /dev/null +++ b/dynatademand/schemas/request/query/get_invoices_summary.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "properties": { + "startDate": { + "type": "string", + "description": "The format is YY-MM-DD" + }, + "endDate": { + "type": "string", + "description": "The format is YY-MM-DD" + }, + "extProjectId": { + "type": "string" + } + }, + "required": [] +} diff --git a/dynatademand/schemas/request/query/get_roles.json b/dynatademand/schemas/request/query/get_roles.json new file mode 100644 index 0000000..363c981 --- /dev/null +++ b/dynatademand/schemas/request/query/get_roles.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "This is the name of the role. eg: manager" + }, + "id": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [] +} diff --git a/dynatademand/schemas/request/query/get_templates.json b/dynatademand/schemas/request/query/get_templates.json new file mode 100644 index 0000000..cf0b7aa --- /dev/null +++ b/dynatademand/schemas/request/query/get_templates.json @@ -0,0 +1,41 @@ +{ + "type": "object", + "properties": { + "limit": { + "type": "integer", + "format": "int32", + "default": 10, + "description": "The number of results to return when viewing a page. For example, setting limit to 20 means that, at most, 20 results will be returned in the request.", + "maximum": 100 + }, + "offset": { + "type": "integer", + "format": "int32", + "default": 0, + "description": "The number of results to skip before returning results. For example, setting an offset of 20 means that the API will discard the first 20 results." + }, + "created_at": { + "type": "string", + "format": "date" + }, + "sort": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "tags": { + "type": "array" + } + }, + "required": [] +} \ No newline at end of file diff --git a/dynatademand/validator.py b/dynatademand/validator.py index b146b16..5070380 100644 --- a/dynatademand/validator.py +++ b/dynatademand/validator.py @@ -17,9 +17,15 @@ 'buy_project': ['path', 'body', ], 'get_project_detailed_report': ['path', ], 'reconcile_project': ['path', ], + 'get_project_permissions': ['path', ], + 'upsert_project_permissions': ['path', 'body', ], + + # Roles + 'get_roles': ['query', ], # Invoices 'get_invoice': ['path', ], + 'get_invoices_summary': ['query', ], # Line items 'close_line_item': ['path', ], @@ -31,6 +37,9 @@ 'pause_line_item': ['path', ], 'update_line_item': ['path', 'body'], + # quotaCells + 'set_quotacell_status': ['path', ], + # Events 'get_events': ['query', ], 'get_event': ['path', ], @@ -49,6 +58,12 @@ # Supplier Sources 'get_sources': [], + + # Template + 'create_template': ['path', 'body', ], + 'update_template': ['path', 'body', ], + 'delete_template': ['path', ], + 'get_templates': ['path', 'query', ], } diff --git a/pyproject.toml b/pyproject.toml index 81303fd..d02265e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ pytest = "4.6.6" jsonschema = "3.2.0" pytest-runner = "5.2" flake8 = "^3.7.9" +pyrsistent = "0.14.11" [build-system] requires = ["poetry>=0.12"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..87608f6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +responses>=0.10.0 +jsonschema>=2.5.0 diff --git a/tests/test_categories.py b/tests/test_categories.py index 6706986..3e8ffbd 100644 --- a/tests/test_categories.py +++ b/tests/test_categories.py @@ -27,3 +27,22 @@ def test_get_survey_topics(self): self.api.get_survey_topics() self.assertEqual(len(responses.calls), 1) self.assertEqual(responses.calls[0].response.json(), survey_topics_json) + + +class TestStudyMetadataEndpoint(unittest.TestCase): + def setUp(self): + self.api = DemandAPIClient(client_id='test', username='testuser', password='testpass', base_host=BASE_HOST) + self.api._access_token = 'Bearer testtoken' + + @responses.activate + def test_get_study_metadata(self): + with open('./tests/test_files/get_study_metadata.json', 'r') as survey_metadata_file: + survey_metadata_json = json.load(survey_metadata_file) + responses.add( + responses.GET, + '{}/sample/v1/studyMetadata'.format(BASE_HOST), + json=survey_metadata_json, + status=200) + self.api.get_study_metadata() + self.assertEqual(len(responses.calls), 1) + self.assertEqual(responses.calls[0].response.json(), survey_metadata_json) diff --git a/tests/test_files/create_template.json b/tests/test_files/create_template.json new file mode 100644 index 0000000..49f18f3 --- /dev/null +++ b/tests/test_files/create_template.json @@ -0,0 +1,58 @@ +{ + "countryISOCode": "US", + "languageISOCode": "en", + "description": "Test Quota Template", + "name": "Test Template", + "quotaPlan": { + "filters": [ + { + "attributeId": "4091", + "options": [ + "3", + "4" + ], + "operator": null + } + ], + "quotaGroups": [ + { + "name": "Gender distribution", + "quotaCells": [ + { + "quotaNodes": [ + { + "attributeId": "11", + "options": [ + "1" + ], + "operator": null + } + ], + "count": 0, + "perc": 30, + "quotaCellId": null + }, + { + "quotaNodes": [ + { + "attributeId": "11", + "options": [ + "2" + ], + "operator": null + } + ], + "count": 0, + "perc": 70, + "quotaCellId": null + } + ], + "quotaGroupId": null + } + ] + }, + "tags": [ + "ABC Ltd", + "Shopping" + ] +} \ No newline at end of file diff --git a/tests/test_files/get_company_users.json b/tests/test_files/get_company_users.json new file mode 100644 index 0000000..f8fe240 --- /dev/null +++ b/tests/test_files/get_company_users.json @@ -0,0 +1,14 @@ +[ + { + "email": "samplifyweb@dynata.com", + "id": 71, + "name": "Bears Beets", + "userName": "battlestargalactica" + }, + { + "email": "samplify@dynata.com", + "id": 102, + "name": "Michael Scott", + "userName": "papercompany" + } +] diff --git a/tests/test_files/get_invoices_summary.pdf b/tests/test_files/get_invoices_summary.pdf new file mode 100644 index 0000000..f7a5d24 Binary files /dev/null and b/tests/test_files/get_invoices_summary.pdf differ diff --git a/tests/test_files/get_project_permissions.json b/tests/test_files/get_project_permissions.json new file mode 100644 index 0000000..e9a6d36 --- /dev/null +++ b/tests/test_files/get_project_permissions.json @@ -0,0 +1,21 @@ +{ + "currentUser": { + "roles": [ + "PROJECT_MANAGER" + ] + }, + "extProjectId": "1910a9fe-59de-4796-aac6-8b7356a7d340", + "teams": [ + { + "id": 139, + "name": "All Users" + } + ], + "users": [ + { + "id": 71, + "role": "PROJECT_MANAGER", + "username": "samplifyweb" + } + ] +} diff --git a/tests/test_files/get_roles.json b/tests/test_files/get_roles.json new file mode 100644 index 0000000..dd41fa9 --- /dev/null +++ b/tests/test_files/get_roles.json @@ -0,0 +1,22 @@ +[ + { + "allowedActions": [ + { + "action": "GET PROJECT", + "description": "Ability to view all the project details", + "id": "GET_PROJECT" + }, + { + "action": "LIST ALL PROJECTS", + "description": "Ability to view a list of projects on samplify", + "id": "LIST_ALL_PROJECTS" + } + ], + "assignableRoles": [ + "SURVEY_AUTHOR" + ], + "description": "", + "id": "SURVEY_AUTHOR", + "name": "Survey Author" + } +] diff --git a/tests/test_files/get_study_metadata.json b/tests/test_files/get_study_metadata.json new file mode 100644 index 0000000..5c509e6 --- /dev/null +++ b/tests/test_files/get_study_metadata.json @@ -0,0 +1,20 @@ +{ + "category": { + "studyRequirements": [ + { + "allowed": false, + "description": "PII Collection", + "id": "PII_COLLECTION", + "name": "PII Collection" + } + ] + }, + "deliveryTypes": [ + { + "allowed": true, + "description": "Completes come in at an even pace throughout fielding", + "id": "SLOW", + "name": "Slow" + } + ] +} \ No newline at end of file diff --git a/tests/test_files/get_survey_topics.json b/tests/test_files/get_survey_topics.json index 7bec34d..a8b897d 100644 --- a/tests/test_files/get_survey_topics.json +++ b/tests/test_files/get_survey_topics.json @@ -2,4 +2,4 @@ "survey": { "topic": "milk" } -} +} \ No newline at end of file diff --git a/tests/test_files/get_teams.json b/tests/test_files/get_teams.json new file mode 100644 index 0000000..5e3e868 --- /dev/null +++ b/tests/test_files/get_teams.json @@ -0,0 +1,11 @@ +[ + { + "createdAt": "2020/05/06 15:39:03", + "default": true, + "description": "", + "id": 139, + "name": "That's what she said", + "status": "ACTIVE", + "updatedAt": "2020/05/06 15:39:03" + } +] diff --git a/tests/test_files/get_templates.json b/tests/test_files/get_templates.json new file mode 100644 index 0000000..00ec81e --- /dev/null +++ b/tests/test_files/get_templates.json @@ -0,0 +1,4 @@ +{ + "limit": 100 +} + \ No newline at end of file diff --git a/tests/test_files/get_user_info.json b/tests/test_files/get_user_info.json new file mode 100644 index 0000000..52e893f --- /dev/null +++ b/tests/test_files/get_user_info.json @@ -0,0 +1,23 @@ +{ + "companies": [ + { + "default": true, + "defaultRole": "PROJECT_MANAGER", + "id": 51, + "name": "Samplify Web", + "teams": [ + { + "default": true, + "id": 139, + "name": "All Users", + "role": "PROJECT_MANAGER", + "status": "ACTIVE" + } + ] + } + ], + "email": "samplifyweb@dynata.com", + "fullName": "Monica Faloola Gellar", + "id": 71, + "userName": "monicagellarrr" +} diff --git a/tests/test_files/update_template.json b/tests/test_files/update_template.json new file mode 100644 index 0000000..07e2e79 --- /dev/null +++ b/tests/test_files/update_template.json @@ -0,0 +1,58 @@ +{ + "countryISOCode": "US", + "languageISOCode": "en", + "description": "Test Quota Template", + "name": "Test Template Update", + "quotaPlan": { + "filters": [ + { + "attributeId": "4091", + "options": [ + "3", + "4" + ], + "operator": null + } + ], + "quotaGroups": [ + { + "name": "Gender distribution", + "quotaCells": [ + { + "quotaNodes": [ + { + "attributeId": "11", + "options": [ + "1" + ], + "operator": null + } + ], + "count": 0, + "perc": 40, + "quotaCellId": null + }, + { + "quotaNodes": [ + { + "attributeId": "11", + "options": [ + "2" + ], + "operator": null + } + ], + "count": 0, + "perc": 60, + "quotaCellId": null + } + ], + "quotaGroupId": null + } + ] + }, + "tags": [ + "ABC Ltd", + "Shopping" + ] +} \ No newline at end of file diff --git a/tests/test_files/upsert_project_permissions.json b/tests/test_files/upsert_project_permissions.json new file mode 100644 index 0000000..e9a6d36 --- /dev/null +++ b/tests/test_files/upsert_project_permissions.json @@ -0,0 +1,21 @@ +{ + "currentUser": { + "roles": [ + "PROJECT_MANAGER" + ] + }, + "extProjectId": "1910a9fe-59de-4796-aac6-8b7356a7d340", + "teams": [ + { + "id": 139, + "name": "All Users" + } + ], + "users": [ + { + "id": 71, + "role": "PROJECT_MANAGER", + "username": "samplifyweb" + } + ] +} diff --git a/tests/test_invoices.py b/tests/test_invoices.py index 64e6998..699362e 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -27,3 +27,19 @@ def test_get_invoice(self): self.api.get_invoice(1337) self.assertEqual(len(responses.calls), 1) self.assertEqual(responses.calls[0].response.headers['content-type'], 'application/pdf') + + @responses.activate + def test_get_invoices_summary(self): + with open('./tests/test_files/get_invoices_summary.pdf', 'rb') as summary_file: + responses.add( + responses.GET, + '{}/sample/v1/projects/invoices/summary'.format(BASE_HOST), + body=summary_file.read(), + content_type='application/pdf', + stream=True, + status=200) + self.api.get_invoices_summary(startDate='2019-06-12', + endDate='2019-06-19', + extProjectId='010528ef-8984-48c1-a06d-4dae730da027') + self.assertEqual(len(responses.calls), 1) + self.assertEqual(responses.calls[0].response.headers['content-type'], 'application/pdf') diff --git a/tests/test_line_items.py b/tests/test_line_items.py index ce96072..33e9d32 100644 --- a/tests/test_line_items.py +++ b/tests/test_line_items.py @@ -159,6 +159,55 @@ def test_pause_line_item(self): self.api.pause_line_item(24, 180) self.assertEqual(len(responses.calls), 2) + @responses.activate + def test_set_quotacell_status(self): + # Tests launching a quotacell. + responses.add( + responses.POST, + '{}/sample/v1/projects/24/lineItems/180/quotaCells/1/launch'.format(BASE_HOST), + json={'status': {'message': 'success'}}, + status=200 + ) + # Response with error launching a quotacell. + responses.add( + responses.POST, + '{}/sample/v1/projects/24/lineItems/180/quotaCells/1/launch'.format(BASE_HOST), + json={'status': {'message': 'error'}}, + status=200 + ) + # Tests pausing a quotacell. + responses.add( + responses.POST, + '{}/sample/v1/projects/24/lineItems/180/quotaCells/1/pause'.format(BASE_HOST), + json={'status': {'message': 'success'}}, + status=200 + ) + # Response with error for pausing a quotacell + responses.add( + responses.POST, + '{}/sample/v1/projects/24/lineItems/180/quotaCells/1/pause'.format(BASE_HOST), + json={'status': {'message': 'error'}}, + status=200 + ) + + # Test successful response for launch quotacell. + self.api.set_quotacell_status(24, 180, 1, "launch") + self.assertEqual(len(responses.calls), 1) + + # Test error response for launch quotacell. + with self.assertRaises(DemandAPIError): + self.api.set_quotacell_status(24, 180, 1, "launch") + self.assertEqual(len(responses.calls), 2) + + # Test successful response for pause quotacell. + self.api.set_quotacell_status(24, 180, 1, "pause") + self.assertEqual(len(responses.calls), 3) + + # Test error response for pause quotacell. + with self.assertRaises(DemandAPIError): + self.api.set_quotacell_status(24, 180, 1, "pause") + self.assertEqual(len(responses.calls), 4) + @responses.activate def test_update_line_item(self): # Tests updating a line item. diff --git a/tests/test_permissions.py b/tests/test_permissions.py new file mode 100644 index 0000000..60ff85f --- /dev/null +++ b/tests/test_permissions.py @@ -0,0 +1,59 @@ +# encoding: utf-8 +from __future__ import unicode_literals, print_function + +import json +import unittest +import responses + +from dynatademand.api import DemandAPIClient +from dynatademand.errors import DemandAPIError + +BASE_HOST = "http://test-url.example" + + +class TestProjectPermissionsEndpoints(unittest.TestCase): + def setUp(self): + self.api = DemandAPIClient(client_id='test', username='testuser', password='testpass', base_host=BASE_HOST) + self.api._access_token = 'Bearer testtoken' + + @responses.activate + def test_get_project_permissions(self): + with open('./tests/test_files/get_project_permissions.json', 'r') as get_permissions_file: + permissions_json = json.load(get_permissions_file) + responses.add( + responses.GET, + '{}/sample/v1/projects/1/permissions'.format(BASE_HOST), + json=permissions_json, + status=200 + ) + self.api.get_project_permissions(1) + self.assertEqual(len(responses.calls), 1) + self.assertEqual(responses.calls[0].response.json(), permissions_json) + + @responses.activate + def test_upsert_project_permissions(self): + # Tests updating a project. + with open('./tests/test_files/upsert_project_permissions.json', 'r') as upsert_project_file: + upsert_project_data = json.load(upsert_project_file) + + # Success response + responses.add( + responses.POST, + '{}/sample/v1/projects/1/permissions'.format(BASE_HOST), + json={'status': {'message': 'success'}}, + status=200) + # Error message included + responses.add( + responses.POST, + '{}/sample/v1/projects/1/permissions'.format(BASE_HOST), + json={'status': {'message': 'error'}}, + status=200) + + # Test successful response. + self.api.upsert_project_permissions(1, upsert_project_data) + self.assertEqual(len(responses.calls), 1) + + # Test response with error included. + with self.assertRaises(DemandAPIError): + self.api.upsert_project_permissions(1, upsert_project_data) + self.assertEqual(len(responses.calls), 2) diff --git a/tests/test_roles.py b/tests/test_roles.py new file mode 100644 index 0000000..5bfc717 --- /dev/null +++ b/tests/test_roles.py @@ -0,0 +1,26 @@ +# encoding: utf-8 +from __future__ import unicode_literals, print_function + +import json +import unittest +import responses + +from dynatademand.api import DemandAPIClient + +BASE_HOST = "http://test-url.example" + + +class TestRolesEndpoints(unittest.TestCase): + def setUp(self): + self.api = DemandAPIClient(client_id='test', username='testuser', password='testpass', base_host=BASE_HOST) + self.api._access_token = 'Bearer testtoken' + + @responses.activate + def test_get_roles(self): + # Tests getting all roles. + with open('./tests/test_files/get_roles.json', 'r') as roles: + roles_json = json.load(roles) + # Success response + responses.add(responses.GET, '{}/sample/v1/roles'.format(BASE_HOST), json=roles_json, status=200) + self.api.get_roles() + self.assertEqual(len(responses.calls), 1) diff --git a/tests/test_teams.py b/tests/test_teams.py new file mode 100644 index 0000000..47b987e --- /dev/null +++ b/tests/test_teams.py @@ -0,0 +1,26 @@ +# encoding: utf-8 +from __future__ import unicode_literals, print_function + +import json +import unittest +import responses + +from dynatademand.api import DemandAPIClient + +BASE_HOST = "http://test-url.example" + + +class TestTeamsEndpoints(unittest.TestCase): + def setUp(self): + self.api = DemandAPIClient(client_id='test', username='testuser', password='testpass', base_host=BASE_HOST) + self.api._access_token = 'Bearer testtoken' + + @responses.activate + def test_get_teams(self): + # Tests getting all teams. + with open('./tests/test_files/get_teams.json', 'r') as teams: + teams_json = json.load(teams) + # Success response + responses.add(responses.GET, '{}/sample/v1/teams'.format(BASE_HOST), json=teams_json, status=200) + self.api.get_company_teams() + self.assertEqual(len(responses.calls), 1) diff --git a/tests/test_template.py b/tests/test_template.py new file mode 100644 index 0000000..d56ef4a --- /dev/null +++ b/tests/test_template.py @@ -0,0 +1,90 @@ +# encoding: utf-8 +from __future__ import unicode_literals, print_function + +import json +import unittest +import responses + +from dynatademand.api import DemandAPIClient + +BASE_HOST = "http://test-url.example" + + +class TestTemplateEndpoints(unittest.TestCase): + def setUp(self): + self.api = DemandAPIClient(client_id='test', username='testuser', password='testpass', base_host=BASE_HOST) + self.api._access_token = 'Bearer testtoken' + + @responses.activate + def test_get_templates(self): + # Tests getting all templates. + with open('./tests/test_files/get_templates.json', 'r') as options: + options_json = json.load(options) + # Success response + responses.add( + responses.GET, + '{}/sample/v1/templates/quotaplan/{}/{}'.format(BASE_HOST, 'US', 'en'), + json=options_json, + status=200) + self.api.get_templates('US', 'en') + self.assertEqual(len(responses.calls), 1) + + @responses.activate + def test_create_template(self): + # Tests creating a template. + with open('./tests/test_files/create_template.json', 'r') as new_template_file: + new_template_data = json.load(new_template_file) + # Success response + responses.add( + responses.POST, + '{}/sample/v1/templates/quotaplan'.format(BASE_HOST), + json={'status': {'message': 'success'}}, + status=200) + # Response with error status + responses.add( + responses.POST, + '{}/sample/v1/templates/quotaplan'.format(BASE_HOST), + json={'status': {'message': 'error'}}, + status=200) + # Test success response + self.api.create_template(new_template_data) + self.assertEqual(len(responses.calls), 1) + + @responses.activate + def test_update_template(self): + # Tests creating a template. + with open('./tests/test_files/update_template.json', 'r') as new_template_file: + new_template_data = json.load(new_template_file) + # Success response + responses.add( + responses.POST, + '{}/sample/v1/templates/quotaplan/{}'.format(BASE_HOST, 1), + json={'status': {'message': 'success'}}, + status=200) + # Response with error status + responses.add( + responses.POST, + '{}/sample/v1/templates/quotaplan/{}'.format(BASE_HOST, 1), + json={'status': {'message': 'error'}}, + status=200) + # Test success response + self.api.update_template(1, new_template_data) + self.assertEqual(len(responses.calls), 1) + + @responses.activate + def test_delete_template(self): + # Tests deleteing templates + responses.add( + responses.DELETE, + '{}/sample/v1/templates/quotaplan/1'.format(BASE_HOST), + json={'status': {'message': 'success'}}, + status=200) + # Response with error status + responses.add( + responses.DELETE, + '{}/sample/v1/templates/quotaplan/1'.format(BASE_HOST), + json={'status': {'message': 'error'}}, + status=200) + # Test successful response + self.api.delete_template(1) + self.assertEqual(len(responses.calls), 1) diff --git a/tests/test_users.py b/tests/test_users.py new file mode 100644 index 0000000..486ccd8 --- /dev/null +++ b/tests/test_users.py @@ -0,0 +1,36 @@ +# encoding: utf-8 +from __future__ import unicode_literals, print_function + +import json +import unittest +import responses + +from dynatademand.api import DemandAPIClient + +BASE_HOST = "http://test-url.example" + + +class TestUsersEndpoints(unittest.TestCase): + def setUp(self): + self.api = DemandAPIClient(client_id='test', username='testuser', password='testpass', base_host=BASE_HOST) + self.api._access_token = 'Bearer testtoken' + + @responses.activate + def test_get_user_info(self): + # Tests getting currently logged in user info. + with open('./tests/test_files/get_user_info.json', 'r') as user_info: + user_info_json = json.load(user_info) + # Success response + responses.add(responses.GET, '{}/sample/v1/user'.format(BASE_HOST), json=user_info_json, status=200) + self.api.get_user_info() + self.assertEqual(len(responses.calls), 1) + + @responses.activate + def test_get_company_users(self): + # Tests getting all company users. + with open('./tests/test_files/get_company_users.json', 'r') as company_users: + company_users_json = json.load(company_users) + # Success response + responses.add(responses.GET, '{}/sample/v1/users'.format(BASE_HOST), json=company_users_json, status=200) + self.api.get_company_users() + self.assertEqual(len(responses.calls), 1)