Skip to content

Commit 49d0948

Browse files
Merge pull request #10 from dynata/POP-2279
[POP-2279] Added create Line Item function and tests.
2 parents 7bd93d9 + 1f33d6b commit 49d0948

File tree

6 files changed

+321
-1
lines changed

6 files changed

+321
-1
lines changed

dynatademand/api.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,33 @@ def get_project_detailed_report(self, project_id):
242242
)
243243
return self._api_get('/projects/{}/detailedReport'.format(project_id))
244244

245+
def add_line_item(self, project_id, lineitem_data):
246+
'''
247+
A line item is a project entity that exist for a specific market and
248+
language that your survey is aimed at. It defines the target panelists
249+
for the market that the survey is looking for, and the number of
250+
completes required. A line item is our unit of work and is what
251+
gets billed to you.
252+
'''
253+
'''
254+
#TODO: Waiting on a valid request body and path schema.
255+
self.validator.validate_request(
256+
'create_line_item',
257+
path_data={
258+
'extProjectId': '{}'.format(project_id)
259+
},
260+
request_body=lineitem_data
261+
)
262+
'''
263+
response_data = self._api_post('/projects/{}/lineItems'.format(project_id), lineitem_data)
264+
if response_data.get('status').get('message') != 'success':
265+
raise DemandAPIError(
266+
"Could not add line item. Demand API responded with: {}".format(
267+
response_data
268+
)
269+
)
270+
return response_data
271+
245272
def launch_line_item(self, project_id, line_item_id):
246273
# Starts traffic to a line item.
247274
self.validator.validate_request(
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
{
2+
"type": "object",
3+
"title": "New Line Item",
4+
"properties": {
5+
"extLineItemId": {
6+
"type": "string",
7+
"description": "A unique identifier for your Line Item"
8+
},
9+
"title": {
10+
"type": "string",
11+
"description": "A Line Item title of your choosing"
12+
},
13+
"countryISOCode": {
14+
"type": "string",
15+
"description": "2 letter ISO Country Code",
16+
"minLength": 2,
17+
"maxLength": 2
18+
},
19+
"languageISOCode": {
20+
"type": "string",
21+
"description": "2 letter ISO Language Code",
22+
"minLength": 2,
23+
"maxLength": 2
24+
},
25+
"surveyURL": {
26+
"type": "string",
27+
"description": "Survey URL to send panelist into. "
28+
},
29+
"surveyTestURL": {
30+
"type": "string",
31+
"description": "Survey Test URL. Required for survey verification"
32+
},
33+
"indicativeIncidence": {
34+
"type": "integer",
35+
"description": "Percent of panelists you expect to qualify for the survey after they go in"
36+
},
37+
"daysInField": {
38+
"type": "integer",
39+
"description": "Number of days the survey should be active in the field. We do not stop the survey after daysInField has expired"
40+
},
41+
"lengthOfInterview": {
42+
"type": "integer",
43+
"description": "Average number of minutes it takes a user to complete your survey"
44+
},
45+
"deliveryType": {
46+
"type": "string",
47+
"default": "BALANCED",
48+
"description": "The plan on how responses will flow into the survey.",
49+
"enum": [
50+
"SLOW",
51+
"BALANCED",
52+
"FAST"
53+
]
54+
},
55+
"sources": {
56+
"type": "array",
57+
"description": "The supplier source that you would like to buy sample from. You can get the list of sources available to your account using the [Get List of Sources](/demand-api-reference/data_endpoints/supplier-sources/get-sources) endpoint",
58+
"default": 100,
59+
"items": {
60+
"type": "object",
61+
"properties": {
62+
"id": {
63+
"type": "integer"
64+
}
65+
}
66+
}
67+
},
68+
"targets": {
69+
"type": "array",
70+
"description": "Total count of survey completes required for this line item",
71+
"items": {
72+
"type": "object",
73+
"properties": {
74+
"count": {
75+
"description": "Total number of counts required for this line item for the specified target type",
76+
"type": "integer"
77+
},
78+
"dailyLimit": {
79+
"description": "Total number of daily counts required for this line item",
80+
"type": "integer"
81+
},
82+
"type": {
83+
"type": "string",
84+
"description": "The type of counts required for this line item.",
85+
"enum": [
86+
"COMPLETE"
87+
]
88+
}
89+
}
90+
}
91+
},
92+
"quotaPlan": {
93+
"type": "object",
94+
"title": "Quota Plan",
95+
"description": "Defines the type of respondents you want to invite for the survey",
96+
"properties": {
97+
"filters": {
98+
"type": "array",
99+
"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`",
100+
"items": {
101+
"type": "object",
102+
"properties": {
103+
"attributeId": {
104+
"type": "string",
105+
"description": "The attribute you want to target respondents on"
106+
},
107+
"options": {
108+
"type": "array",
109+
"description": "The options of the attribute you want to target respondents on",
110+
"uniqueItems": true,
111+
"items": {
112+
"type": "string"
113+
}
114+
},
115+
"operator": {
116+
"type": "string",
117+
"enum": [
118+
"exclude",
119+
"include"
120+
],
121+
"default": "include",
122+
"description": "The operator to use for the attribute options."
123+
}
124+
}
125+
}
126+
},
127+
"quotaGroups": {
128+
"type": "array",
129+
"description": "Quota groups define the allocated targeting attributes for panelists within this line item. Only attributes that have `isAllowedInQuotas = true` is allowed in `quotaGroups`.",
130+
"items": {
131+
"type": "object",
132+
"properties": {
133+
"name": {
134+
"type": "string",
135+
"description": "A quota group name of your choosing"
136+
},
137+
"quotaCells": {
138+
"type": "array",
139+
"description": "Quota Cells define the percentage allocation for the required targeting. A quota cell is made up of a collection of quota Nodes",
140+
"items": {
141+
"type": "object",
142+
"properties": {
143+
"quotaNodes": {
144+
"type": "array",
145+
"description": "Quota Nodes define the collection of attributes and options being targeted.",
146+
"items": {
147+
"type": "object",
148+
"properties": {
149+
"attributeId": {
150+
"type": "string",
151+
"description": "The attribute you want to target respondents on"
152+
},
153+
"options": {
154+
"type": "array",
155+
"description": "The options of the attribute you want to target respondents on",
156+
"uniqueItems": true,
157+
"items": {
158+
"type": "string"
159+
}
160+
},
161+
"operator": {
162+
"type": "string",
163+
"enum": [
164+
"exclude",
165+
"include"
166+
],
167+
"default": "include",
168+
"description": "**Deprecated field** The operator to use for the attribute options."
169+
}
170+
}
171+
}
172+
},
173+
"count": {
174+
"type": "integer",
175+
"description": "The count of respondents you want to qualify for the defined quota cell"
176+
}
177+
}
178+
}
179+
}
180+
}
181+
}
182+
}
183+
}
184+
}
185+
},
186+
"required": [
187+
"extLineItemId",
188+
"countryISOCode",
189+
"languageISOCode",
190+
"indicativeIncidence",
191+
"daysInField",
192+
"lengthOfInterview",
193+
"targets"
194+
]
195+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"extProjectId": {
5+
"type": "string",
6+
"required": true
7+
}
8+
},
9+
"required": [
10+
"extProjectId"
11+
]
12+
}

dynatademand/validator.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
'get_project_detailed_report': ['path', ],
1717

1818
# Line items
19+
'create_line_item': ['path', 'body', ],
1920
'get_line_item': ['path', ],
2021
'get_line_items': ['path', 'query'],
2122
'get_line_item_detailed_report': ['path', ],
@@ -59,7 +60,14 @@ def __init__(self, ):
5960
def _validate_object(self, schema_type, endpoint_name, data):
6061
jsonschema.validate(schema=self.schemas[schema_type][endpoint_name], instance=data)
6162

62-
def validate_request(self, endpoint_name, path_data={}, query_params={}, request_body={}):
63+
def validate_request(self, endpoint_name, path_data=None, query_params=None, request_body=None):
64+
if path_data is None:
65+
path_data = {}
66+
if query_params is None:
67+
query_params = {}
68+
if request_body is None:
69+
request_body = {}
70+
6371
'''
6472
# TODO: None of the path schemas from the documentation are currently valid.
6573
if 'path' in ENDPOINTS[endpoint_name]:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"extLineItemId": "lineItem001",
3+
"title": "US College",
4+
"countryISOCode": "US",
5+
"languageISOCode": "en",
6+
"surveyURL": "www.mysurvey.com/live/survey?pid=<#DubKnowledge[1500/Entity id]>&k2=<#Project[Secure Key 2]>&psid=<#IdParameter[Value]>",
7+
"surveyTestURL": "www.mysurvey.com/test/survey?pid=<#DubKnowledge[1500/Entity id]>&k2=<#Project[Secure Key 2]>&psid=<#IdParameter[Value]>",
8+
"indicativeIncidence": 20,
9+
"daysInField": 20,
10+
"lengthOfInterview": 10,
11+
"deliveryType": "BALANCED",
12+
"requiredCompletes": 200,
13+
"quotaPlan": {
14+
"filters": [
15+
{
16+
"attributeId": "4091",
17+
"options": [
18+
"3",
19+
"4"
20+
]
21+
}
22+
],
23+
"quotaGroups": [
24+
{
25+
"name": "Gender Distribution",
26+
"quotaCells": [
27+
{
28+
"quotaNodes": [
29+
{
30+
"attributeId": "11",
31+
"options": [
32+
"1"
33+
]
34+
}
35+
],
36+
"count": 130
37+
},
38+
{
39+
"quotaNodes": [
40+
{
41+
"attributeId": "11",
42+
"options": [
43+
"2"
44+
]
45+
}
46+
],
47+
"count": 70
48+
}
49+
]
50+
}
51+
]
52+
}
53+
}

tests/test_line_items.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,31 @@ def test_get_line_item_detailed_report(self):
5151
self.assertEqual(len(responses.calls), 1)
5252
self.assertEqual(responses.calls[0].response.json(), line_item_detailed_report_json)
5353

54+
@responses.activate
55+
def test_add_line_item(self):
56+
# Tests creating a project. This also tests validating the project data as part of `api.create_project`.
57+
with open('./tests/test_files/create_line_item.json', 'r') as new_lineitem_file:
58+
new_lineitem_data = json.load(new_lineitem_file)
59+
# Success response
60+
responses.add(
61+
responses.POST,
62+
'{}/sample/v1/projects/24/lineItems'.format(BASE_HOST),
63+
json={'status': {'message': 'success'}},
64+
status=200)
65+
# Response with error status
66+
responses.add(
67+
responses.POST,
68+
'{}/sample/v1/projects/24/lineItems'.format(BASE_HOST),
69+
json={'status': {'message': 'error'}},
70+
status=200)
71+
# Test success response
72+
self.api.add_line_item(24, new_lineitem_data)
73+
self.assertEqual(len(responses.calls), 1)
74+
75+
with self.assertRaises(DemandAPIError):
76+
self.api.add_line_item(24, new_lineitem_data)
77+
self.assertEqual(len(responses.calls), 2)
78+
5479
@responses.activate
5580
def test_launch_line_item(self):
5681
# Tests closing a project.

0 commit comments

Comments
 (0)