-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathquery.py
More file actions
443 lines (318 loc) · 12.9 KB
/
Copy pathquery.py
File metadata and controls
443 lines (318 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
from datetime import datetime
from enum import Enum
from os import urandom
class QueryError(Exception):
def __init__(self, message):
super(QueryError, self).__init__(message)
class Query(object):
Sort = Enum('Sort', 'asc desc')
_sort_map = {Sort.asc: 'asc', Sort.desc: 'desc'}
def __init__(self, suite_id):
if not isinstance(suite_id, basestring):
raise QueryError("suite_id type should be string")
self._suite_id = suite_id
self._dimension_id = None
self._metrics = None
self._start_date = None
self._end_date = None
self._metrics_filter = None
self._dimension_filter = None
self._segments = None
self._sort_mode = None
self._limit = None
self._page = 0
self._translation = None
def select(self, dimension_id, metrics):
if not isinstance(dimension_id, basestring):
raise QueryError("dimension_id type should be string")
if not isinstance(metrics, list):
raise QueryError("metrics type should be list")
if not len(metrics):
raise QueryError(
"metrics_ids is expected to have atleast one element")
self._dimension_id = dimension_id
self._metrics = metrics
return self
def for_range(self, start_date, end_date):
"""Composes report dates range.
Args:
start_date (datetime)
end_date (datetime)
"""
if not isinstance(start_date, datetime):
raise QueryError("start_date type should be datetime")
if not isinstance(end_date, datetime):
raise QueryError("end_date type should be datetime")
if start_date > end_date:
raise QueryError("start_date can't be greater than end_date")
self._start_date = start_date
self._end_date = end_date
return self
def with_segments(self, segments):
if not isinstance(segments, list):
raise QueryError("segments type should be list")
self._segments = segments
return self
def filter(self, dimension_filter):
"""Composes report quering conditions.
Args:
dimension_filter (:obj:`DimensionFilter`)
"""
if dimension_filter and not isinstance(dimension_filter, DimensionFilter):
raise QueryError("dimension_filter type should be DimensionFilter")
self._dimension_filter = dimension_filter
return self
def sort(self, sort_mode):
if not isinstance(sort_mode, Query.Sort):
raise QueryError("sort_mode type should be Sort")
self._sort_mode = sort_mode
return self
def limit(self, size):
if not isinstance(size, int):
raise QueryError("limit size type should be int")
if size < 0:
raise QueryError("limit size can't have a negative value")
self._limit = size
return self
def next(self):
"""Handles report pagination"""
self._page += 1
self._translation['settings'].update({'page': str(self._page)})
def compile(self):
if not self._dimension_id:
raise QueryError("dimension_id must be specified")
if not self._start_date or not self._end_date:
raise QueryError(
"start_date and end_date are required by global filters")
global_filters = [{'type': 'dateRange',
'dateRange': self._compose_date_range()}]
if self._segments:
for segment in self._segments:
global_filters.append(
{'type': 'segment', 'segmentId': segment})
self._translation = {'rsid': self._suite_id,
'globalFilters': global_filters,
'metricContainer': {'metrics': self._compose_metric_container()},
'metricFilters': self._compose_metrics_filters(),
'dimension': self._dimension_id,
'settings': self._compose_settings()}
if self._dimension_filter:
self._translation.update(
{'search': self._dimension_filter.compile()})
return self._translation
@staticmethod
def brakedown_filters(dimensions):
"""Breaks down the report based on the specified dimensions"""
if not isinstance(dimensions, dict):
raise QueryError("dimensions type should be dict")
metrics_filter = []
for dimension, items_ids in dimensions.items():
metric_filter = MetricFilter(
FilterType.breakdown, (dimension, items_ids,))
metrics_filter.append(metric_filter)
return metrics_filter
def _compose_date_range(self):
return "{}/{}".format(self._start_date.isoformat(), self._end_date.isoformat())
def _compose_metrics_filters(self):
self._metrics_filter = []
exists = {}
for metric in self._metrics:
for filter in metric.filters:
if filter.id not in exists:
exists[filter.id] = filter
self._metrics_filter.append(filter)
filters = []
for filter in self._metrics_filter:
output = filter.compile()
filters.append(output)
return filters
def _compose_metric_container(self):
if not self._metrics:
raise QueryError("metrics must be specified")
index = 0
metrics = []
for metric in self._metrics:
metrics.append({'columnId': str(index) if not metric.column_id else str(metric.column_id),
'id': metric.id,
'filters': [f.id for f in metric.filters]})
index += 1
return metrics
def _compose_settings(self):
settings = {}
if self._limit:
settings.update({'limit': str(self._limit)})
settings.update({'page': str(self._page)})
if not self._sort_mode:
self._sort_mode = Query.Sort.asc
settings.update(
{'dimensionSort': Query._sort_map[self._sort_mode]})
return settings
class Metric(object):
def __init__(self, id, column_id=None, filters=None):
self._id = id
# If column_id is not specified it is going to be generated.
self._column_id = column_id
if filters and not isinstance(filters, list):
raise QueryError("filters type should be list")
self._filters = []
if filters:
self._filters = filters
@property
def id(self):
return self._id
@property
def column_id(self):
return self._column_id
@property
def filters(self):
return self._filters
def compile(self):
pass
FilterType = Enum('FilterType', 'dateRange segment breakdown')
class MetricFilter(object):
_type_map = {FilterType.dateRange: 'dateRange',
FilterType.segment: 'segmentId',
FilterType.breakdown: 'breakdown'}
def __init__(self, filter_type, value, id=None):
self._id = urandom(8).encode('hex') if not id else id
self._filter_type = filter_type
# Attr value data structure depends on filter type (e.g.: dimension, granularity, etc)
if filter_type == FilterType.breakdown and not isinstance(value, tuple):
raise QueryError(
"for breakdown filter `value` arg should be a tuple with dimension id and rows ids")
self._value = value
@property
def id(self):
return self._id
@property
def filter_type(self):
return self._filter_type
@property
def value(self):
return self._value
def _compose_type(self):
return MetricFilter._type_map[self._filter_type]
def _compose_type_key(self):
if self._filter_type == FilterType.breakdown:
return 'dimension'
return MetricFilter._type_map[self._filter_type]
def compile(self):
metric_filter_map = {'id': self._id,
'type': self._compose_type()}
if self._filter_type == FilterType.breakdown:
metric_filter_map.update(
{self._compose_type_key(): self._value[0], 'itemIds': self.value[1]})
else:
metric_filter_map.update(
{self._compose_type_key(): self._value})
return metric_filter_map
class DimensionFilter(object):
def __init__(self, *args, **kwargs):
self._include_search_total = True
self._items_ids = kwargs.get('items_ids')
self._excluded_items_ids = kwargs.get('exclude_items_ids')
self._criteria = args # criteria
@property
def items_ids(self):
return self._items_ids
@property
def exluce_items_ids(self):
return self._excluded_items_ids
@property
def include_search_total(self):
return self._include_search_total
def compile(self):
dimension_filter_map = {
'includeSearchTotal': self._include_search_total}
if self._items_ids:
if len(self._items_ids) == 1:
dimension_filter_map['itemId'] = self._items_ids[0]
else:
dimension_filter_map['itemIds'] = self._items_ids
if self._excluded_items_ids:
dimension_filter_map['excludeItemIds'] = self._excluded_items_ids
if self._criteria:
dimension_filter_map['clause'] = self._compile_search()
return dimension_filter_map
def _compile_search(self):
if len(self._criteria) == 1 and isinstance(self._criteria[0], Clause):
return self._criteria[0].compile()
translation = []
for criterion in self._criteria:
if isinstance(criterion, Conjunction):
translation.append(criterion.compile())
if isinstance(criterion, tuple):
output = [" ( "]
for item in criterion:
output.append(item.compile())
output.append(" )")
translation.append(''.join(output))
return ''.join(translation)
Operator = Enum('Operator', 'match contains begins_with ends_with')
class Conjunction(object):
def __init__(self, conjunction, *clauses):
self._conjunction = conjunction
self._clauses = []
for item in clauses:
self._clauses.append(Clause._to_clause(item, conjunction))
def compile(self):
translation = []
fmt = " {} {}"
for index, clause in enumerate(self._clauses):
if isinstance(clause, tuple):
output = []
for index, item in enumerate(clause):
if not index:
output.append(
" {} ( {}".format(Clause._conjunction_map[item._conjunction], item.compile()))
continue
output.append(fmt.format(
Clause._conjunction_map[item._conjunction], item.compile()))
output.append(" )")
translation.append(''.join(output))
continue
if not index:
translation.append("{}".format(clause.compile()))
continue
output = fmt.format(
Clause._conjunction_map[clause._conjunction], clause.compile())
translation.append(output)
return ''.join(translation)
class and_(Conjunction):
def __init__(self, *clauses):
super(and_, self).__init__(Clause.Conjunction.and_, *clauses)
class or_(Conjunction):
def __init__(self, *clauses):
super(or_, self).__init__(Clause.Conjunction.or_, *clauses)
class Clause(object):
Conjunction = Enum('Conjunction', 'and_ or_ not_')
_operator_map = {Operator.match: 'MATCH',
Operator.contains: 'CONTAINS',
Operator.begins_with: 'BEGINS-WITH',
Operator.ends_with: 'ENDS-WITH'}
_conjunction_map = {Conjunction.and_: 'AND',
Conjunction.or_: 'OR',
Conjunction.not_: 'NOT',
None: ''}
def __init__(self, operator, operand):
self._operator = operator
self._operand = operand
self._conjunction = None
self._clauses = []
def compile(self):
return "{} {}".format(
Clause._operator_map[self._operator], self._operand)
@staticmethod
def _to_clause(obj, conjunction):
if isinstance(obj, tuple):
clauses = []
for clause in obj:
if isinstance(clause, tuple):
tpl = Clause._to_clause(clause, conjunction)
clauses += list(tpl)
continue
clause._conjunction = conjunction
clauses.append(clause)
return tuple(clauses)
obj._conjunction = conjunction
return obj