From c9859b368ebff6543f54b8758fb54cd5dda54fe5 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 1 Dec 2014 14:59:35 -0800 Subject: [PATCH 001/224] Added `max_age` query param. --- keen/__init__.py | 76 +++++++++++++++++++++++++++------------ keen/client.py | 92 +++++++++++++++++++++++++++++++++--------------- 2 files changed, 116 insertions(+), 52 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index 2a1be01..f8c45f6 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -42,7 +42,7 @@ def generate_image_beacon(event_collection, body, timestamp=None): return _client.generate_image_beacon(event_collection, body, timestamp=timestamp) -def count(event_collection, timeframe=None, timezone=None, interval=None, filters=None, group_by=None): +def count(event_collection, timeframe=None, timezone=None, interval=None, filters=None, group_by=None, max_age=None): """ Performs a count query Counts the number of events that meet the given criteria. @@ -58,15 +58,17 @@ def count(event_collection, timeframe=None, timezone=None, interval=None, filter example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.count(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by) + interval=interval, filters=filters, group_by=group_by, max_age=max_age) def sum(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None): + group_by=None, max_age=None): """ Performs a sum query Adds the values of a target property for events that meet the given criteria. @@ -83,15 +85,18 @@ def sum(event_collection, target_property, timeframe=None, timezone=None, interv example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.sum(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) def minimum(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None): + group_by=None, max_age=None): """ Performs a minimum query Finds the minimum value of a target property for events that meet the given criteria. @@ -108,15 +113,18 @@ def minimum(event_collection, target_property, timeframe=None, timezone=None, in example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.minimum(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) def maximum(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None): + group_by=None, max_age=None): """ Performs a maximum query Finds the maximum value of a target property for events that meet the given criteria. @@ -133,15 +141,18 @@ def maximum(event_collection, target_property, timeframe=None, timezone=None, in example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.maximum(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) def average(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None): + group_by=None, max_age=None): """ Performs a average query Finds the average of a target property for events that meet the given criteria. @@ -158,15 +169,18 @@ def average(event_collection, target_property, timeframe=None, timezone=None, in example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.average(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) -def percentile(event_collection, target_property, percentile, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None): +def percentile(event_collection, target_property, percentile, timeframe=None, timezone=None, interval=None, + filters=None, group_by=None, max_age=None): """ Performs a percentile query Finds the percentile of a target property for events that meet the given criteria. @@ -185,6 +199,8 @@ def percentile(event_collection, target_property, percentile, timeframe=None, ti example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() @@ -196,12 +212,13 @@ def percentile(event_collection, target_property, percentile, timeframe=None, ti interval=interval, filters=filters, group_by=group_by, - target_property=target_property + target_property=target_property, + max_age=max_age, ) def count_unique(event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None): + filters=None, group_by=None, max_age=None): """ Performs a count unique query Counts the unique values of a target property for events that meet the given criteria. @@ -218,15 +235,18 @@ def count_unique(event_collection, target_property, timeframe=None, timezone=Non example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.count_unique(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) def select_unique(event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None): + filters=None, group_by=None, max_age=None): """ Performs a select unique query Returns an array of the unique values of a target property for events that meet the given criteria. @@ -243,15 +263,18 @@ def select_unique(event_collection, target_property, timeframe=None, timezone=No example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.select_unique(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) def extraction(event_collection, timeframe=None, timezone=None, filters=None, latest=None, email=None, - property_names=None): + property_names=None, max_age=None): """ Performs a data extraction Returns either a JSON object of events or a response @@ -267,14 +290,17 @@ def extraction(event_collection, timeframe=None, timezone=None, filters=None, la :param latest: int, the number of most recent records you'd like to return :param email: string, optional string containing an email address to email results to :param property_names: string or list of strings, used to limit the properties returned + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.extraction(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - filters=filters, latest=latest, email=email, property_names=property_names) + filters=filters, latest=latest, email=email, property_names=property_names, + max_age=max_age) -def funnel(steps, timeframe=None, timezone=None): +def funnel(steps, timeframe=None, timezone=None, max_age=None): """ Performs a Funnel query Returns an object containing the results for each step of the funnel. @@ -286,14 +312,16 @@ def funnel(steps, timeframe=None, timezone=None): happened example: "previous_7_days" :param timezone: int, the timezone you'd like to use for the timeframe and interval in seconds + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() - return _client.funnel(steps=steps, timeframe=timeframe, timezone=timezone) + return _client.funnel(steps=steps, timeframe=timeframe, timezone=timezone, max_age=max_age) def multi_analysis(event_collection, analyses, timeframe=None, interval=None, - timezone=None, filters=None, group_by=None): + timezone=None, filters=None, group_by=None, max_age=None): """ Performs a multi-analysis query Returns a dictionary of analysis results. @@ -312,9 +340,11 @@ def multi_analysis(event_collection, analyses, timeframe=None, interval=None, example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.multi_analysis(event_collection=event_collection, timeframe=timeframe, interval=interval, timezone=timezone, filters=filters, - group_by=group_by, analyses=analyses) + group_by=group_by, analyses=analyses, max_age=max_age) diff --git a/keen/client.py b/keen/client.py index b37a406..68b17f7 100644 --- a/keen/client.py +++ b/keen/client.py @@ -109,7 +109,6 @@ def check_project_id(project_id): if not project_id or not isinstance(project_id, str): raise exceptions.InvalidProjectIdError(project_id) - def add_event(self, event_collection, event_body, timestamp=None): """ Adds an event. @@ -175,7 +174,8 @@ def _url_escape(self, url): import urllib.parse return urllib.parse.quote(url) - def count(self, event_collection, timeframe=None, timezone=None, interval=None, filters=None, group_by=None): + def count(self, event_collection, timeframe=None, timezone=None, interval=None, + filters=None, group_by=None, max_age=None): """ Performs a count query Counts the number of events that meet the given criteria. @@ -191,14 +191,16 @@ def count(self, event_collection, timeframe=None, timezone=None, interval=None, example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by) + interval=interval, filters=filters, group_by=group_by, max_age=max_age) return self.api.query("count", params) def sum(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None): + group_by=None, max_age=None): """ Performs a sum query Adds the values of a target property for events that meet the given criteria. @@ -215,14 +217,17 @@ def sum(self, event_collection, target_property, timeframe=None, timezone=None, example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) return self.api.query("sum", params) - def minimum(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None): + def minimum(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, + filters=None, group_by=None, max_age=None): """ Performs a minimum query Finds the minimum value of a target property for events that meet the given criteria. @@ -239,14 +244,17 @@ def minimum(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) return self.api.query("minimum", params) - def maximum(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None): + def maximum(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, + filters=None, group_by=None, max_age=None): """ Performs a maximum query Finds the maximum value of a target property for events that meet the given criteria. @@ -263,14 +271,17 @@ def maximum(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) return self.api.query("maximum", params) - def average(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None): + def average(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, + filters=None, group_by=None, max_age=None): """ Performs a average query Finds the average of a target property for events that meet the given criteria. @@ -287,14 +298,17 @@ def average(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) return self.api.query("average", params) - def percentile(self, event_collection, target_property, percentile, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None): + def percentile(self, event_collection, target_property, percentile, timeframe=None, timezone=None, + interval=None, filters=None, group_by=None, max_age=None): """ Performs a percentile query Finds the percentile of a target property for events that meet the given criteria. @@ -313,6 +327,8 @@ def percentile(self, event_collection, target_property, percentile, timeframe=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ params = self.get_params( @@ -323,12 +339,13 @@ def percentile(self, event_collection, target_property, percentile, timeframe=No interval=interval, filters=filters, group_by=group_by, - target_property=target_property + target_property=target_property, + max_age=max_age, ) return self.api.query("percentile", params) def count_unique(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None): + filters=None, group_by=None, max_age=None): """ Performs a count unique query Counts the unique values of a target property for events that meet the given criteria. @@ -345,14 +362,17 @@ def count_unique(self, event_collection, target_property, timeframe=None, timezo example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) return self.api.query("count_unique", params) def select_unique(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None): + filters=None, group_by=None, max_age=None): """ Performs a select unique query Returns an array of the unique values of a target property for events that meet the given criteria. @@ -369,14 +389,17 @@ def select_unique(self, event_collection, target_property, timeframe=None, timez example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, target_property=target_property) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) return self.api.query("select_unique", params) - def extraction(self, event_collection, timeframe=None, timezone=None, filters=None, latest=None, email=None, - property_names=None): + def extraction(self, event_collection, timeframe=None, timezone=None, filters=None, latest=None, + email=None, property_names=None, max_age=None): """ Performs a data extraction Returns either a JSON object of events or a response @@ -392,12 +415,15 @@ def extraction(self, event_collection, timeframe=None, timezone=None, filters=No :param latest: int, the number of most recent records you'd like to return :param email: string, optional string containing an email address to email results to :param property_names: string or list of strings, used to limit the properties returned + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - filters=filters, latest=latest, email=email, property_names=property_names) + filters=filters, latest=latest, email=email, property_names=property_names, + max_age=max_age) return self.api.query("extraction", params) - def funnel(self, steps, timeframe=None, timezone=None): + def funnel(self, steps, timeframe=None, timezone=None, max_age=None): """ Performs a Funnel query Returns an object containing the results for each step of the funnel. @@ -409,13 +435,15 @@ def funnel(self, steps, timeframe=None, timezone=None): happened example: "previous_7_days" :param timezone: int, the timezone you'd like to use for the timeframe and interval in seconds + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ - params = self.get_params(steps=steps, timeframe=timeframe, timezone=timezone) + params = self.get_params(steps=steps, timeframe=timeframe, timezone=timezone, max_age=max_age) return self.api.query("funnel", params) def multi_analysis(self, event_collection, analyses, timeframe=None, interval=None, timezone=None, filters=None, - group_by=None): + group_by=None, max_age=None): """ Performs a multi-analysis query Returns a dictionary of analysis results. @@ -434,6 +462,8 @@ def multi_analysis(self, event_collection, analyses, timeframe=None, interval=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + willing to trade for increased query performance, in seconds """ params = self.get_params( @@ -443,13 +473,15 @@ def multi_analysis(self, event_collection, analyses, timeframe=None, interval=No timezone=timezone, filters=filters, group_by=group_by, - analyses=analyses) + analyses=analyses, + max_age=max_age, + ) return self.api.query("multi_analysis", params) def get_params(self, event_collection=None, timeframe=None, timezone=None, interval=None, filters=None, group_by=None, target_property=None, latest=None, email=None, analyses=None, steps=None, - property_names=None, percentile=None): + property_names=None, percentile=None, max_age=None): params = {} if event_collection: params["event_collection"] = event_collection @@ -483,5 +515,7 @@ def get_params(self, event_collection=None, timeframe=None, timezone=None, inter params["property_names"] = json.dumps(property_names) if percentile: params["percentile"] = percentile + if max_age: + params["max_age"] = max_age return params From 2aeca0d4c3131fd7f267c345337d8d8d60b5745c Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 1 Dec 2014 20:36:10 -0800 Subject: [PATCH 002/224] Removed `max_age` from the extraction method. --- keen/client.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/keen/client.py b/keen/client.py index 68b17f7..981a391 100644 --- a/keen/client.py +++ b/keen/client.py @@ -399,7 +399,7 @@ def select_unique(self, event_collection, target_property, timeframe=None, timez return self.api.query("select_unique", params) def extraction(self, event_collection, timeframe=None, timezone=None, filters=None, latest=None, - email=None, property_names=None, max_age=None): + email=None, property_names=None): """ Performs a data extraction Returns either a JSON object of events or a response @@ -415,12 +415,10 @@ def extraction(self, event_collection, timeframe=None, timezone=None, filters=No :param latest: int, the number of most recent records you'd like to return :param email: string, optional string containing an email address to email results to :param property_names: string or list of strings, used to limit the properties returned - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re - willing to trade for increased query performance, in seconds + """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - filters=filters, latest=latest, email=email, property_names=property_names, - max_age=max_age) + filters=filters, latest=latest, email=email, property_names=property_names) return self.api.query("extraction", params) def funnel(self, steps, timeframe=None, timezone=None, max_age=None): From 58dcb4006b086f12400c14e4b601636a5b1b2924 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 1 Dec 2014 20:37:17 -0800 Subject: [PATCH 003/224] Removed `max_age` from extraction method. --- keen/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index f8c45f6..749fc81 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -274,7 +274,7 @@ def select_unique(event_collection, target_property, timeframe=None, timezone=No def extraction(event_collection, timeframe=None, timezone=None, filters=None, latest=None, email=None, - property_names=None, max_age=None): + property_names=None): """ Performs a data extraction Returns either a JSON object of events or a response @@ -290,14 +290,11 @@ def extraction(event_collection, timeframe=None, timezone=None, filters=None, la :param latest: int, the number of most recent records you'd like to return :param email: string, optional string containing an email address to email results to :param property_names: string or list of strings, used to limit the properties returned - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re - willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.extraction(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - filters=filters, latest=latest, email=email, property_names=property_names, - max_age=max_age) + filters=filters, latest=latest, email=email, property_names=property_names) def funnel(steps, timeframe=None, timezone=None, max_age=None): From 6421c203af691dab03bd514756add7502fd70ba6 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Tue, 2 Dec 2014 11:38:30 -0800 Subject: [PATCH 004/224] Corrected formatting --- keen/__init__.py | 20 ++++++++++---------- keen/client.py | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index 749fc81..bc66ef0 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -58,7 +58,7 @@ def count(event_collection, timeframe=None, timezone=None, interval=None, filter example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -85,7 +85,7 @@ def sum(event_collection, target_property, timeframe=None, timezone=None, interv example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -113,7 +113,7 @@ def minimum(event_collection, target_property, timeframe=None, timezone=None, in example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -141,7 +141,7 @@ def maximum(event_collection, target_property, timeframe=None, timezone=None, in example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -169,7 +169,7 @@ def average(event_collection, target_property, timeframe=None, timezone=None, in example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -199,7 +199,7 @@ def percentile(event_collection, target_property, percentile, timeframe=None, ti example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -235,7 +235,7 @@ def count_unique(event_collection, target_property, timeframe=None, timezone=Non example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -263,7 +263,7 @@ def select_unique(event_collection, target_property, timeframe=None, timezone=No example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -309,7 +309,7 @@ def funnel(steps, timeframe=None, timezone=None, max_age=None): happened example: "previous_7_days" :param timezone: int, the timezone you'd like to use for the timeframe and interval in seconds - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -337,7 +337,7 @@ def multi_analysis(event_collection, analyses, timeframe=None, interval=None, example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ diff --git a/keen/client.py b/keen/client.py index 981a391..8143229 100644 --- a/keen/client.py +++ b/keen/client.py @@ -191,7 +191,7 @@ def count(self, event_collection, timeframe=None, timezone=None, interval=None, example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -217,7 +217,7 @@ def sum(self, event_collection, target_property, timeframe=None, timezone=None, example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -244,7 +244,7 @@ def minimum(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -271,7 +271,7 @@ def maximum(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -298,7 +298,7 @@ def average(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -327,7 +327,7 @@ def percentile(self, event_collection, target_property, percentile, timeframe=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -362,7 +362,7 @@ def count_unique(self, event_collection, target_property, timeframe=None, timezo example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -389,7 +389,7 @@ def select_unique(self, event_collection, target_property, timeframe=None, timez example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -433,7 +433,7 @@ def funnel(self, steps, timeframe=None, timezone=None, max_age=None): happened example: "previous_7_days" :param timezone: int, the timezone you'd like to use for the timeframe and interval in seconds - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ @@ -460,7 +460,7 @@ def multi_analysis(self, event_collection, analyses, timeframe=None, interval=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum ‘staleness’ you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re willing to trade for increased query performance, in seconds """ From 0b86c825d1dce93f2371a8f0d857f01f26e8b6f7 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Tue, 2 Dec 2014 17:11:46 -0800 Subject: [PATCH 005/224] Fixed formatting issue. --- keen/__init__.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index bc66ef0..d6c505a 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -58,7 +58,7 @@ def count(event_collection, timeframe=None, timezone=None, interval=None, filter example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -85,7 +85,7 @@ def sum(event_collection, target_property, timeframe=None, timezone=None, interv example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -113,7 +113,7 @@ def minimum(event_collection, target_property, timeframe=None, timezone=None, in example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -141,7 +141,7 @@ def maximum(event_collection, target_property, timeframe=None, timezone=None, in example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -169,7 +169,7 @@ def average(event_collection, target_property, timeframe=None, timezone=None, in example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -199,7 +199,7 @@ def percentile(event_collection, target_property, percentile, timeframe=None, ti example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -235,7 +235,7 @@ def count_unique(event_collection, target_property, timeframe=None, timezone=Non example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -263,7 +263,7 @@ def select_unique(event_collection, target_property, timeframe=None, timezone=No example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -309,7 +309,7 @@ def funnel(steps, timeframe=None, timezone=None, max_age=None): happened example: "previous_7_days" :param timezone: int, the timezone you'd like to use for the timeframe and interval in seconds - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -337,7 +337,7 @@ def multi_analysis(event_collection, analyses, timeframe=None, interval=None, example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ From 10a38af3020fb5804c72dff1793db166de996836 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Tue, 2 Dec 2014 21:34:32 -0800 Subject: [PATCH 006/224] Fixed formatting --- keen/client.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/keen/client.py b/keen/client.py index 8143229..994a1d2 100644 --- a/keen/client.py +++ b/keen/client.py @@ -191,7 +191,7 @@ def count(self, event_collection, timeframe=None, timezone=None, interval=None, example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -217,7 +217,7 @@ def sum(self, event_collection, target_property, timeframe=None, timezone=None, example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -244,7 +244,7 @@ def minimum(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -271,7 +271,7 @@ def maximum(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -298,7 +298,7 @@ def average(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -327,7 +327,7 @@ def percentile(self, event_collection, target_property, percentile, timeframe=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -362,7 +362,7 @@ def count_unique(self, event_collection, target_property, timeframe=None, timezo example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -389,7 +389,7 @@ def select_unique(self, event_collection, target_property, timeframe=None, timez example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -433,7 +433,7 @@ def funnel(self, steps, timeframe=None, timezone=None, max_age=None): happened example: "previous_7_days" :param timezone: int, the timezone you'd like to use for the timeframe and interval in seconds - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ @@ -460,7 +460,7 @@ def multi_analysis(self, event_collection, analyses, timeframe=None, interval=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you’re + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ From 6dc9cfc0640fa986c910d45bd3ff98331511fc70 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 5 Dec 2014 14:40:38 -0800 Subject: [PATCH 007/224] Updated README and version number. --- README.md | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 144f29c..afb6a15 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,8 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.6 ++ Added ```max_age``` parameter for caching. ##### 0.3.5 + Added client configurable timeout to gets. diff --git a/setup.py b/setup.py index 299a905..89b9a87 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup(name="keen", - version="0.3.5", + version="0.3.6", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From d89c00efc2d7c444893e9d083c721af4de50a2d6 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 5 Dec 2014 15:32:09 -0800 Subject: [PATCH 008/224] Tweaked formatting --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 89b9a87..a7363b4 100644 --- a/setup.py +++ b/setup.py @@ -11,4 +11,4 @@ packages=["keen"], install_requires=["requests", "pycrypto", "Padding"], tests_require=["nose"] -) + ) From 61ed1ae1c5a5165204bc7c7e6022c484e192097b Mon Sep 17 00:00:00 2001 From: Rebecca Standig Date: Wed, 14 Jan 2015 12:31:57 -0800 Subject: [PATCH 009/224] Updating requests to 2.5.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c786cbc..8582f37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ Padding==0.4 nose==1.1.2 pycrypto==2.6 -requests==2.2.1 +requests==2.5.1 From 57a0e2c852b06fd82d353668dd96b466fad18591 Mon Sep 17 00:00:00 2001 From: Rebecca Standig Date: Wed, 14 Jan 2015 12:43:46 -0800 Subject: [PATCH 010/224] Updating README to include new version of requests in changelog. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index afb6a15..fbcf9e8 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,9 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +#### 0.3.7 ++ Upgraded to requests==2.5.1 + ##### 0.3.6 + Added ```max_age``` parameter for caching. From b5a739542897ae3c668014bfb25082649e6234b6 Mon Sep 17 00:00:00 2001 From: Rebecca Standig Date: Wed, 14 Jan 2015 13:00:14 -0800 Subject: [PATCH 011/224] Updating version in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a7363b4..7465c27 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup(name="keen", - version="0.3.6", + version="0.3.7", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From 441f4f064fdcb1a16153eabe024fd3ac8815236f Mon Sep 17 00:00:00 2001 From: Rebecca Standig Date: Wed, 14 Jan 2015 13:06:02 -0800 Subject: [PATCH 012/224] Fixing typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fbcf9e8..c5e5cb7 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog -#### 0.3.7 +##### 0.3.7 + Upgraded to requests==2.5.1 ##### 0.3.6 From 9edbf53c8f872d90d99ed3887e30aefa5c557c3e Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Sun, 18 Jan 2015 16:30:13 +0100 Subject: [PATCH 013/224] define keen.master_key --- keen/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keen/__init__.py b/keen/__init__.py index d6c505a..ecf9a68 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -8,16 +8,18 @@ project_id = None write_key = None read_key = None +master_key = None def _initialize_client_from_environment(): - global _client, project_id, write_key, read_key + global _client, project_id, write_key, read_key, master_key if _client is None: # check environment for project ID and keys project_id = project_id or os.environ.get("KEEN_PROJECT_ID") write_key = write_key or os.environ.get("KEEN_WRITE_KEY") read_key = read_key or os.environ.get("KEEN_READ_KEY") + master_key = master_key or os.environ.get("KEEN_MASTER_KEY") if not project_id: raise InvalidEnvironmentError("Please set the KEEN_PROJECT_ID environment variable or set keen.project_id!") From afab4935fe9d736d9626e7fc5ee8ba29026291a2 Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Sun, 18 Jan 2015 16:33:31 +0100 Subject: [PATCH 014/224] add master_key to KeenApi class --- keen/api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/keen/api.py b/keen/api.py index 762f883..bcd1dbb 100644 --- a/keen/api.py +++ b/keen/api.py @@ -50,7 +50,8 @@ class KeenApi(object): # self says it belongs to KeenApi/andOr is the object passed into KeenApi # __init__ create keenapi object whenever KeenApi class is invoked def __init__(self, project_id, write_key=None, read_key=None, - base_url=None, api_version=None, get_timeout=None, post_timeout=None): + base_url=None, api_version=None, get_timeout=None, post_timeout=None, + master_key=None): """ Initializes a KeenApi object @@ -60,6 +61,7 @@ def __init__(self, project_id, write_key=None, read_key=None, :param base_url: optional, set this to override where API requests are sent :param api_version: string, optional, set this to override what API + :param master_key: a Keen IO Master API Key version is used """ # super? recreates the object with values passed into KeenApi @@ -67,6 +69,7 @@ def __init__(self, project_id, write_key=None, read_key=None, self.project_id = project_id self.write_key = write_key self.read_key = read_key + self.master_key = master_key if base_url: self.base_url = base_url if api_version: From 2c617e6395343843d8fea4332edaf57a2a452852 Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Sun, 18 Jan 2015 16:43:50 +0100 Subject: [PATCH 015/224] add master_key to KeenClient class --- keen/client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/keen/client.py b/keen/client.py index 994a1d2..bc970ad 100644 --- a/keen/client.py +++ b/keen/client.py @@ -57,7 +57,8 @@ class KeenClient(object): """ def __init__(self, project_id, write_key=None, read_key=None, - persistence_strategy=None, api_class=KeenApi, get_timeout=305, post_timeout=305): + persistence_strategy=None, api_class=KeenApi, get_timeout=305, post_timeout=305 + master_key=None): """ Initializes a KeenClient object. :param project_id: the Keen IO project ID @@ -67,6 +68,7 @@ def __init__(self, project_id, write_key=None, read_key=None, the event :param get_timeout: optional, the timeout on GET requests :param post_timeout: optional, the timeout on POST requests + :param master_key: a Keen IO Master API Key """ super(KeenClient, self).__init__() @@ -76,7 +78,8 @@ def __init__(self, project_id, write_key=None, read_key=None, # Set up an api client to be used for querying and optionally passed # into a default persistence strategy. self.api = api_class(project_id, write_key=write_key, read_key=read_key, - get_timeout=get_timeout, post_timeout=post_timeout) + get_timeout=get_timeout, post_timeout=post_timeout, + master_key=master_key) if persistence_strategy: # validate the given persistence strategy From c3a6da471acadf380166fca0e9a0fc13c155740a Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Sun, 18 Jan 2015 17:08:38 +0100 Subject: [PATCH 016/224] create KeenClient instance setting master_key --- keen/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keen/__init__.py b/keen/__init__.py index ecf9a68..6305043 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -26,7 +26,8 @@ def _initialize_client_from_environment(): _client = KeenClient(project_id, write_key=write_key, - read_key=read_key) + read_key=read_key, + master_key=master_key) def add_event(event_collection, body, timestamp=None): From 6ef64fa9ae397779ab429e54ea8b3f9823090a55 Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Sun, 18 Jan 2015 17:20:56 +0100 Subject: [PATCH 017/224] add missing comma --- keen/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/client.py b/keen/client.py index bc970ad..c90424f 100644 --- a/keen/client.py +++ b/keen/client.py @@ -57,7 +57,7 @@ class KeenClient(object): """ def __init__(self, project_id, write_key=None, read_key=None, - persistence_strategy=None, api_class=KeenApi, get_timeout=305, post_timeout=305 + persistence_strategy=None, api_class=KeenApi, get_timeout=305, post_timeout=305, master_key=None): """ Initializes a KeenClient object. From e2f8c2d24ce05a866b273b23aa8f9a409670776d Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 30 Jan 2015 12:41:24 -0800 Subject: [PATCH 018/224] Mocked client_tests. This is _first step_ towards refactoring tests, the focus is on removing network requests. --- keen/tests/client_tests.py | 180 ++++++++++++++++++++----------------- requirements.txt | 1 + 2 files changed, 101 insertions(+), 80 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 20a5c4f..83734bb 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -8,18 +8,34 @@ import keen from keen.client import KeenClient from keen.tests.base_test_case import BaseTestCase +from mock import patch, MagicMock import sys __author__ = 'dkador' +class MockedRequest(object): + def __init__(self, status_code, json_response): + self.status_code = status_code + self.json_response = json_response + + def json(self): + return {"result": self.json_response} + + +class MockedFailedRequest(MockedRequest): + def json(self): + return self.json_response + + class ClientTests(BaseTestCase): def setUp(self): super(ClientTests, self).setUp() + self.api_key = "2e79c6ec1d0145be8891bf668599c79a" keen._client = None - keen.project_id = None - keen.write_key = None - keen.read_key = None + keen.project_id = "5004ded1163d66114f000000" + keen.write_key = scoped_keys.encrypt(self.api_key, {"allowed_operations": ["write"]}) + keen.read_key = scoped_keys.encrypt(self.api_key, {"allowed_operations": ["read"]}) def test_init(self): def positive_helper(project_id, **kwargs): @@ -65,59 +81,50 @@ def negative_helper(expected_exception, project_id, persistence_strategy=persistence_strategies.DirectPersistenceStrategy) def test_direct_persistence_strategy(self): - project_id = "5004ded1163d66114f000000" - api_key = "2e79c6ec1d0145be8891bf668599c79a" - write_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}) - read_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["read"]}) - client = KeenClient(project_id, write_key=write_key, read_key=read_key) - client.add_event("python_test", {"hello": "goodbye"}) - client.add_event("python_test", {"hello": "goodbye"}) - client.add_events( - { - "sign_ups": [{ - "username": "timmy", - "referred_by": "steve", - "son_of": "my_mom" - }], - "purchases": [ - {"price": 5}, - {"price": 6}, - {"price": 7} - ]} - ) - + with patch("requests.Session.post") as post: + post.return_value = MockedRequest(status_code=201, json_response={"hello": "goodbye"}) + keen.add_event("python_test", {"hello": "goodbye"}) + keen.add_event("python_test", {"hello": "goodbye"}) + + with patch("requests.Session.post") as post: + post.return_value = MockedRequest(status_code=200, json_response={"hello": "goodbye"}) + keen.add_events( + { + "sign_ups": [{ + "username": "timmy", + "referred_by": "steve", + "son_of": "my_mom" + }], + "purchases": [ + {"price": 5}, + {"price": 6}, + {"price": 7} + ]} + ) + + @patch("requests.Session.post", + MagicMock(return_value=MockedRequest(status_code=201, json_response={"hello": "goodbye"}))) def test_module_level_add_event(self): - keen.project_id = "5004ded1163d66114f000000" - api_key = "2e79c6ec1d0145be8891bf668599c79a" - keen.write_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}) - # client = KeenClient(project_id, write_key=write_key, read_key=read_key) keen.add_event("python_test", {"hello": "goodbye"}) + @patch("requests.Session.post", + MagicMock(return_value=MockedRequest(status_code=200, json_response={"hello": "goodbye"}))) def test_module_level_add_events(self): - keen.project_id = "5004ded1163d66114f000000" - api_key = "2e79c6ec1d0145be8891bf668599c79a" - keen.write_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}) - # client = KeenClient(project_id, write_key=write_key, read_key=read_key) keen.add_events({"python_test": [{"hello": "goodbye"}]}) - @raises(requests.Timeout) + @patch("requests.Session.post", MagicMock(side_effect=requests.Timeout)) def test_post_timeout_single(self): - keen.project_id = "5004ded1163d66114f000000" - api_key = "2e79c6ec1d0145be8891bf668599c79a" - keen.write_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}) - client = KeenClient(keen.project_id, write_key=keen.write_key, read_key=None, - post_timeout=0.0001) - client.add_event("python_test", {"hello": "goodbye"}) + with self.assert_raises(requests.Timeout): + keen.add_event("python_test", {"hello": "goodbye"}) - @raises(requests.Timeout) + @patch("requests.Session.post", MagicMock(side_effect=requests.Timeout)) def test_post_timeout_batch(self): - keen.project_id = "5004ded1163d66114f000000" - api_key = "2e79c6ec1d0145be8891bf668599c79a" - keen.write_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}) - client = KeenClient(keen.project_id, write_key=keen.write_key, read_key=None, - post_timeout=0.0001) - client.add_events({"python_test": [{"hello": "goodbye"}]}) + with self.assert_raises(requests.Timeout): + keen.add_events({"python_test": [{"hello": "goodbye"}]}) + @patch("requests.Session.post", + MagicMock(return_value=MockedFailedRequest(status_code=401, + json_response={"message": "authorization error", "error_code": 401}))) def test_environment_variables(self): # try addEvent w/out having environment variables keen._client = None @@ -134,26 +141,27 @@ def test_environment_variables(self): # force client to reinitialize keen._client = None + os.environ["KEEN_PROJECT_ID"] = "12345" os.environ["KEEN_WRITE_KEY"] = "abcde" + self.assert_raises(exceptions.KeenApiError, keen.add_event, "python_test", {"hello": "goodbye"}) def test_configure_through_code(self): - keen.project_id = "123456" + client = KeenClient(project_id="123456", read_key=None, write_key=None) self.assert_raises(exceptions.InvalidEnvironmentError, - keen.add_event, "python_test", {"hello": "goodbye"}) + client.add_event, "python_test", {"hello": "goodbye"}) # force client to reinitialize - keen._client = None - keen.write_key = "abcdef" - self.assert_raises(exceptions.KeenApiError, - keen.add_event, "python_test", {"hello": "goodbye"}) + client = KeenClient(project_id="123456", read_key=None, write_key="abcdef") + with patch("requests.Session.post") as post: + post.return_value = MockedFailedRequest( + status_code=401, json_response={"message": "authorization error", "error_code": 401} + ) + self.assert_raises(exceptions.KeenApiError, + client.add_event, "python_test", {"hello": "goodbye"}) def test_generate_image_beacon(self): - keen.project_id = "5004ded1163d66114f000000" - api_key = "2e79c6ec1d0145be8891bf668599c79a" - keen.write_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}) - event_collection = "python_test hello!?" event_data = {"a": "b"} data = self.base64_encode(json.dumps(event_data)) @@ -170,19 +178,9 @@ def test_generate_image_beacon(self): url = client.generate_image_beacon(event_collection, event_data) self.assert_equal(expected, url) - # make sure URL works - response = requests.get(url) - self.assert_equal(200, response.status_code) - self.assert_equal(b"GIF89a\x01\x00\x01\x00\x80\x01\x00\xff\xff\xff\x00\x00\x00!\xf9\x04\x01\n\x00\x01\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02L\x01\x00;", - response.content) - def test_generate_image_beacon_timestamp(self): # make sure using a timestamp works - keen.project_id = "5004ded1163d66114f000000" - api_key = "2e79c6ec1d0145be8891bf668599c79a" - keen.write_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}) - event_collection = "python_test" event_data = {"a": "b"} timestamp = datetime.datetime.utcnow() @@ -214,7 +212,6 @@ def url_escape(self, url): return urllib.parse.quote(url) - class QueryTests(BaseTestCase): def setUp(self): super(QueryTests, self).setUp() @@ -223,8 +220,8 @@ def setUp(self): api_key = "2e79c6ec1d0145be8891bf668599c79a" keen.write_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}) keen.read_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["read"]}) - keen.add_event("query test", {"number": 5, "string": "foo"}) - keen.add_event("step2", {"number": 5, "string": "foo"}) + # keen.add_event("query test", {"number": 5, "string": "foo"}) + # keen.add_event("step2", {"number": 5, "string": "foo"}) def tearDown(self): keen.project_id = None @@ -236,44 +233,58 @@ def tearDown(self): def get_filter(self): return [{"property_name": "number", "operator": "eq", "property_value": 5}] + @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) def test_count(self): resp = keen.count("query test", timeframe="today", filters=self.get_filter()) self.assertEqual(type(resp), int) + @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) def test_sum(self): resp = keen.sum("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) + @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) def test_minimum(self): resp = keen.minimum("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) + @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) def test_maximum(self): resp = keen.maximum("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) + @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) def test_average(self): resp = keen.average("query test", target_property="number", timeframe="today") self.assertTrue(type(resp) in (int, float), type(resp)) + @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) def test_percentile(self): resp = keen.percentile("query test", target_property="number", percentile=80, timeframe="today") self.assertTrue(type(resp) in (int, float), type(resp)) + @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) def test_count_unique(self): resp = keen.count_unique("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) + @patch("requests.Session.get", + MagicMock(return_value=MockedRequest(status_code=200, json_response=[0, 1, 2]))) def test_select_unique(self): resp = keen.select_unique("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), list) + @patch("requests.Session.get", + MagicMock(return_value=MockedRequest(status_code=200, json_response=[{"result": 1}, {"result": 1}]))) def test_extraction(self): - resp = keen.extraction("query test", timeframe="today", property_names=["number"]) + resp = keen.extraction("query test", timeframe="today", property_names=["number"]) self.assertEqual(type(resp), list) for event in resp: self.assertTrue("string" not in event) + @patch("requests.Session.get", MagicMock(return_value=MockedRequest( + status_code=200, json_response=[{"value": {"total": 1}}, {"value": {"total": 2}}]) + )) def test_multi_analysis(self): resp = keen.multi_analysis("query test", analyses={"total": {"analysis_type": "sum", "target_property": "number"}}, @@ -282,6 +293,8 @@ def test_multi_analysis(self): for result in resp: self.assertEqual(type(result["value"]["total"]), int) + @patch("requests.Session.get", + MagicMock(return_value=MockedRequest(status_code=200, json_response=[{"result": 1}, {"result": 1}]))) def test_funnel(self): step1 = { "event_collection": "query test", @@ -296,23 +309,28 @@ def test_funnel(self): resp = keen.funnel([step1, step2]) self.assertEqual(type(resp), list) + @patch("requests.Session.get", + MagicMock(return_value=MockedRequest(status_code=200, json_response=[0, 1, 2]))) def test_group_by(self): resp = keen.count("query test", timeframe="today", group_by="number") self.assertEqual(type(resp), list) + @patch("requests.Session.get", + MagicMock(return_value=MockedRequest(status_code=200, json_response=[0, 1, 2]))) def test_multi_group_by(self): resp = keen.count("query test", timeframe="today", group_by=["number", "string"]) self.assertEqual(type(resp), list) + @patch("requests.Session.get", + MagicMock(return_value=MockedRequest(status_code=200, json_response=[0, 1, 2]))) def test_interval(self): resp = keen.count("query test", timeframe="this_2_days", interval="daily") self.assertEqual(type(resp), list) def test_passing_custom_api_client(self): class CustomApiClient(object): - def __init__(self, project_id, - write_key=None, read_key=None, - base_url=None, api_version=None, **kwargs): + def __init__(self, project_id, write_key=None, read_key=None, + base_url=None, api_version=None, **kwargs): super(CustomApiClient, self).__init__() self.project_id = project_id self.write_key = write_key @@ -329,16 +347,17 @@ def __init__(self, project_id, # But it shows it is actually using our class self.assertRaises(TypeError, client.add_event) - @raises(requests.Timeout) - def test_timeout_count(self): - keen.project_id = "5004ded1163d66114f000000" - api_key = "2e79c6ec1d0145be8891bf668599c79a" - keen.read_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["read"]}) + @patch("requests.Session.get") + def test_timeout_count(self, get): + get.side_effect = requests.Timeout client = KeenClient(keen.project_id, write_key=None, read_key=keen.read_key, get_timeout=0.0001) - resp = client.count("query test", timeframe="today", filters=self.get_filter()) + with self.assert_raises(requests.Timeout): + client.count("query test", timeframe="today", filters=self.get_filter()) + # Make sure the requests library was called with `timeout`. + self.assert_equals(get.call_args[1]["timeout"], 0.0001) # only need to test unicode separately in python2 -if sys.version_info[0] > 3: +if sys.version_info[0] < 3: class UnicodeTests(BaseTestCase): def setUp(self): @@ -348,6 +367,7 @@ def setUp(self): api_key = unicode("2e79c6ec1d0145be8891bf668599c79a") keen.write_key = unicode(api_key) + @patch("requests.Session.post", MagicMock(return_value=MockedRequest(status_code=201, json_response=[0, 1, 2]))) def test_unicode(self): keen.add_event(unicode("unicode test"), {unicode("number"): 5, "string": unicode("foo")}) diff --git a/requirements.txt b/requirements.txt index 8582f37..ae2ee74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ Padding==0.4 nose==1.1.2 pycrypto==2.6 requests==2.5.1 +mock==1.0.1 From 90d8467c02373bcea71680b2b24895ffee295588 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 30 Jan 2015 16:09:18 -0800 Subject: [PATCH 019/224] Made `assertRaises` compatible for python 2.6. --- keen/tests/client_tests.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 83734bb..f86861a 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -114,13 +114,11 @@ def test_module_level_add_events(self): @patch("requests.Session.post", MagicMock(side_effect=requests.Timeout)) def test_post_timeout_single(self): - with self.assert_raises(requests.Timeout): - keen.add_event("python_test", {"hello": "goodbye"}) + self.assert_raises(requests.Timeout, keen.add_event, "python_test", {"hello": "goodbye"}) @patch("requests.Session.post", MagicMock(side_effect=requests.Timeout)) def test_post_timeout_batch(self): - with self.assert_raises(requests.Timeout): - keen.add_events({"python_test": [{"hello": "goodbye"}]}) + self.assert_raises(requests.Timeout, keen.add_events, {"python_test": [{"hello": "goodbye"}]}) @patch("requests.Session.post", MagicMock(return_value=MockedFailedRequest(status_code=401, @@ -351,8 +349,7 @@ def __init__(self, project_id, write_key=None, read_key=None, def test_timeout_count(self, get): get.side_effect = requests.Timeout client = KeenClient(keen.project_id, write_key=None, read_key=keen.read_key, get_timeout=0.0001) - with self.assert_raises(requests.Timeout): - client.count("query test", timeframe="today", filters=self.get_filter()) + self.assert_raises(requests.Timeout, client.count, "query test", timeframe="today", filters=self.get_filter()) # Make sure the requests library was called with `timeout`. self.assert_equals(get.call_args[1]["timeout"], 0.0001) From 199835cb76d6afc8046ae420eaac659c2a5664b2 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 30 Jan 2015 16:47:07 -0800 Subject: [PATCH 020/224] Made a couple test names clearer. --- keen/tests/client_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index f86861a..fc3ab73 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -325,7 +325,7 @@ def test_interval(self): resp = keen.count("query test", timeframe="this_2_days", interval="daily") self.assertEqual(type(resp), list) - def test_passing_custom_api_client(self): + def test_passing_invalid_custom_api_client(self): class CustomApiClient(object): def __init__(self, project_id, write_key=None, read_key=None, base_url=None, api_version=None, **kwargs): @@ -365,7 +365,7 @@ def setUp(self): keen.write_key = unicode(api_key) @patch("requests.Session.post", MagicMock(return_value=MockedRequest(status_code=201, json_response=[0, 1, 2]))) - def test_unicode(self): + def test_add_event_with_unicode(self): keen.add_event(unicode("unicode test"), {unicode("number"): 5, "string": unicode("foo")}) def tearDown(self): From fbf3b7a52dd1d02a79b9156d51e511b322edcdd7 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Sat, 31 Jan 2015 20:59:34 -0800 Subject: [PATCH 021/224] Updated setup.py so `python setup.py test` command works. Tested in 2.6 and 2.7. --- setup.py | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index 7465c27..8d54fa9 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,32 @@ #!/usr/bin/env python -from distutils.core import setup - -setup(name="keen", - version="0.3.7", - description="Python Client for Keen IO", - author="Keen IO", - author_email="team@keen.io", - url="https://github.com/keenlabs/KeenClient-Python", - packages=["keen"], - install_requires=["requests", "pycrypto", "Padding"], - tests_require=["nose"] - ) +# from distutils.core import setup +from setuptools import setup, find_packages +import sys + +try: + import multiprocessing # NOQA +except ImportError: + pass + +tests_require = ['nose'] + +if sys.version_info < (2, 7): + tests_require.append('unittest2') + +setup_requires = [] +if 'nosetests' in sys.argv[1:]: + setup_requires.append('nose') + +setup( + name="keen", + version="0.3.7", + description="Python Client for Keen IO", + author="Keen IO", + author_email="team@keen.io", + url="https://github.com/keenlabs/KeenClient-Python", + packages=["keen"], + install_requires=["requests", "pycrypto", "Padding"], + tests_require=tests_require, + test_suite='nose.collector', +) From 4f087747ec7c8d2dad3f1f6470f990ace2c87757 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Sun, 1 Feb 2015 19:59:22 -0800 Subject: [PATCH 022/224] DRYing up test mocks. --- keen/tests/client_tests.py | 146 ++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 76 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index fc3ab73..25d57e7 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -28,6 +28,7 @@ def json(self): return self.json_response +@patch("requests.Session.post") class ClientTests(BaseTestCase): def setUp(self): super(ClientTests, self).setUp() @@ -37,7 +38,7 @@ def setUp(self): keen.write_key = scoped_keys.encrypt(self.api_key, {"allowed_operations": ["write"]}) keen.read_key = scoped_keys.encrypt(self.api_key, {"allowed_operations": ["read"]}) - def test_init(self): + def test_init(self, post): def positive_helper(project_id, **kwargs): client = KeenClient(project_id, **kwargs) self.assert_not_equal(client, None) @@ -80,50 +81,45 @@ def negative_helper(expected_exception, project_id, "project_id", persistence_strategy=persistence_strategies.DirectPersistenceStrategy) - def test_direct_persistence_strategy(self): - with patch("requests.Session.post") as post: - post.return_value = MockedRequest(status_code=201, json_response={"hello": "goodbye"}) - keen.add_event("python_test", {"hello": "goodbye"}) - keen.add_event("python_test", {"hello": "goodbye"}) + def test_direct_persistence_strategy(self, post): + post.return_value = MockedRequest(status_code=201, json_response={"hello": "goodbye"}) + keen.add_event("python_test", {"hello": "goodbye"}) + keen.add_event("python_test", {"hello": "goodbye"}) - with patch("requests.Session.post") as post: - post.return_value = MockedRequest(status_code=200, json_response={"hello": "goodbye"}) - keen.add_events( - { - "sign_ups": [{ - "username": "timmy", - "referred_by": "steve", - "son_of": "my_mom" - }], - "purchases": [ - {"price": 5}, - {"price": 6}, - {"price": 7} - ]} - ) + post.return_value = MockedRequest(status_code=200, json_response={"hello": "goodbye"}) + keen.add_events( + { + "sign_ups": [{ + "username": "timmy", + "referred_by": "steve", + "son_of": "my_mom" + }], + "purchases": [ + {"price": 5}, + {"price": 6}, + {"price": 7} + ]} + ) - @patch("requests.Session.post", - MagicMock(return_value=MockedRequest(status_code=201, json_response={"hello": "goodbye"}))) - def test_module_level_add_event(self): + def test_module_level_add_event(self, post): + post.return_value = MockedRequest(status_code=201, json_response={"hello": "goodbye"}) keen.add_event("python_test", {"hello": "goodbye"}) - @patch("requests.Session.post", - MagicMock(return_value=MockedRequest(status_code=200, json_response={"hello": "goodbye"}))) - def test_module_level_add_events(self): + def test_module_level_add_events(self, post): + post.return_value = MockedRequest(status_code=200, json_response={"hello": "goodbye"}) keen.add_events({"python_test": [{"hello": "goodbye"}]}) - @patch("requests.Session.post", MagicMock(side_effect=requests.Timeout)) - def test_post_timeout_single(self): + def test_post_timeout_single(self, post): + post.side_effect = requests.Timeout self.assert_raises(requests.Timeout, keen.add_event, "python_test", {"hello": "goodbye"}) - @patch("requests.Session.post", MagicMock(side_effect=requests.Timeout)) - def test_post_timeout_batch(self): + def test_post_timeout_batch(self, post): + post.side_effect = requests.Timeout self.assert_raises(requests.Timeout, keen.add_events, {"python_test": [{"hello": "goodbye"}]}) - @patch("requests.Session.post", - MagicMock(return_value=MockedFailedRequest(status_code=401, - json_response={"message": "authorization error", "error_code": 401}))) - def test_environment_variables(self): + def test_environment_variables(self, post): + post.return_value = MockedFailedRequest(status_code=401, + json_response={"message": "authorization error", "error_code": 401}) # try addEvent w/out having environment variables keen._client = None keen.project_id = None @@ -145,7 +141,7 @@ def test_environment_variables(self): self.assert_raises(exceptions.KeenApiError, keen.add_event, "python_test", {"hello": "goodbye"}) - def test_configure_through_code(self): + def test_configure_through_code(self, post): client = KeenClient(project_id="123456", read_key=None, write_key=None) self.assert_raises(exceptions.InvalidEnvironmentError, client.add_event, "python_test", {"hello": "goodbye"}) @@ -159,7 +155,7 @@ def test_configure_through_code(self): self.assert_raises(exceptions.KeenApiError, client.add_event, "python_test", {"hello": "goodbye"}) - def test_generate_image_beacon(self): + def test_generate_image_beacon(self, post): event_collection = "python_test hello!?" event_data = {"a": "b"} data = self.base64_encode(json.dumps(event_data)) @@ -176,7 +172,7 @@ def test_generate_image_beacon(self): url = client.generate_image_beacon(event_collection, event_data) self.assert_equal(expected, url) - def test_generate_image_beacon_timestamp(self): + def test_generate_image_beacon_timestamp(self, post): # make sure using a timestamp works event_collection = "python_test" @@ -210,7 +206,14 @@ def url_escape(self, url): return urllib.parse.quote(url) +@patch("requests.Session.get") class QueryTests(BaseTestCase): + + int_response = MockedRequest(status_code=200, json_response=2) + + list_response = MockedRequest( + status_code=200, json_response=[{"value": {"total": 1}}, {"value": {"total": 2}}]) + def setUp(self): super(QueryTests, self).setUp() keen._client = None @@ -231,59 +234,55 @@ def tearDown(self): def get_filter(self): return [{"property_name": "number", "operator": "eq", "property_value": 5}] - @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) - def test_count(self): + def test_count(self, get): + get.return_value = self.int_response resp = keen.count("query test", timeframe="today", filters=self.get_filter()) self.assertEqual(type(resp), int) - @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) - def test_sum(self): + def test_sum(self, get): + get.return_value = self.int_response resp = keen.sum("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) - @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) - def test_minimum(self): + def test_minimum(self, get): + get.return_value = self.int_response resp = keen.minimum("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) - @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) - def test_maximum(self): + def test_maximum(self, get): + get.return_value = self.int_response resp = keen.maximum("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) - @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) - def test_average(self): + def test_average(self, get): + get.return_value = self.int_response resp = keen.average("query test", target_property="number", timeframe="today") self.assertTrue(type(resp) in (int, float), type(resp)) - @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) - def test_percentile(self): + def test_percentile(self, get): + get.return_value = self.int_response resp = keen.percentile("query test", target_property="number", percentile=80, timeframe="today") self.assertTrue(type(resp) in (int, float), type(resp)) - @patch("requests.Session.get", MagicMock(return_value=MockedRequest(status_code=200, json_response=2))) - def test_count_unique(self): + def test_count_unique(self, get): + get.return_value = self.int_response resp = keen.count_unique("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) - @patch("requests.Session.get", - MagicMock(return_value=MockedRequest(status_code=200, json_response=[0, 1, 2]))) - def test_select_unique(self): + def test_select_unique(self, get): + get.return_value = self.list_response resp = keen.select_unique("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), list) - @patch("requests.Session.get", - MagicMock(return_value=MockedRequest(status_code=200, json_response=[{"result": 1}, {"result": 1}]))) - def test_extraction(self): + def test_extraction(self, get): + get.return_value = self.list_response resp = keen.extraction("query test", timeframe="today", property_names=["number"]) self.assertEqual(type(resp), list) for event in resp: self.assertTrue("string" not in event) - @patch("requests.Session.get", MagicMock(return_value=MockedRequest( - status_code=200, json_response=[{"value": {"total": 1}}, {"value": {"total": 2}}]) - )) - def test_multi_analysis(self): + def test_multi_analysis(self, get): + get.return_value = self.list_response resp = keen.multi_analysis("query test", analyses={"total": {"analysis_type": "sum", "target_property": "number"}}, timeframe="today", interval="hourly") @@ -291,9 +290,8 @@ def test_multi_analysis(self): for result in resp: self.assertEqual(type(result["value"]["total"]), int) - @patch("requests.Session.get", - MagicMock(return_value=MockedRequest(status_code=200, json_response=[{"result": 1}, {"result": 1}]))) - def test_funnel(self): + def test_funnel(self, get): + get.return_value = self.list_response step1 = { "event_collection": "query test", "actor_property": "number", @@ -307,25 +305,22 @@ def test_funnel(self): resp = keen.funnel([step1, step2]) self.assertEqual(type(resp), list) - @patch("requests.Session.get", - MagicMock(return_value=MockedRequest(status_code=200, json_response=[0, 1, 2]))) - def test_group_by(self): + def test_group_by(self, get): + get.return_value = self.list_response resp = keen.count("query test", timeframe="today", group_by="number") self.assertEqual(type(resp), list) - @patch("requests.Session.get", - MagicMock(return_value=MockedRequest(status_code=200, json_response=[0, 1, 2]))) - def test_multi_group_by(self): + def test_multi_group_by(self, get): + get.return_value = self.list_response resp = keen.count("query test", timeframe="today", group_by=["number", "string"]) self.assertEqual(type(resp), list) - @patch("requests.Session.get", - MagicMock(return_value=MockedRequest(status_code=200, json_response=[0, 1, 2]))) - def test_interval(self): + def test_interval(self, get): + get.return_value = self.list_response resp = keen.count("query test", timeframe="this_2_days", interval="daily") self.assertEqual(type(resp), list) - def test_passing_invalid_custom_api_client(self): + def test_passing_invalid_custom_api_client(self, get): class CustomApiClient(object): def __init__(self, project_id, write_key=None, read_key=None, base_url=None, api_version=None, **kwargs): @@ -345,7 +340,6 @@ def __init__(self, project_id, write_key=None, read_key=None, # But it shows it is actually using our class self.assertRaises(TypeError, client.add_event) - @patch("requests.Session.get") def test_timeout_count(self, get): get.side_effect = requests.Timeout client = KeenClient(keen.project_id, write_key=None, read_key=keen.read_key, get_timeout=0.0001) From 5a811b1baf4838ab3be5d1553cb68b4d2900ef86 Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Thu, 5 Feb 2015 20:09:22 +0100 Subject: [PATCH 023/224] add test for master_key --- keen/tests/client_tests.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 20a5c4f..2492f2b 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -20,6 +20,7 @@ def setUp(self): keen.project_id = None keen.write_key = None keen.read_key = None + keen.master_key = None def test_init(self): def positive_helper(project_id, **kwargs): @@ -124,6 +125,7 @@ def test_environment_variables(self): keen.project_id = None keen.write_key = None keen.read_key = None + keen.master_key = None self.assert_raises(exceptions.InvalidEnvironmentError, keen.add_event, "python_test", {"hello": "goodbye"}) @@ -138,6 +140,22 @@ def test_environment_variables(self): self.assert_raises(exceptions.KeenApiError, keen.add_event, "python_test", {"hello": "goodbye"}) + def test_set_master_key_env_var(self): + exp_master_key = os.environ["KEEN_MASTER_KEY"] = "abcd1234" + keen._initialize_client_from_environment() + + self.assertEquals(exp_master_key, keen.master_key) + self.assertEquals(exp_master_key, keen._client.api.master_key) + + del os.environ["KEEN_MASTER_KEY"] + + def test_set_master_key_env_var(self): + exp_master_key = keen.master_key = "abcd4567" + keen._initialize_client_from_environment() + + self.assertEquals(exp_master_key, keen.master_key) + self.assertEquals(exp_master_key, keen._client.api.master_key) + def test_configure_through_code(self): keen.project_id = "123456" self.assert_raises(exceptions.InvalidEnvironmentError, @@ -230,6 +248,7 @@ def tearDown(self): keen.project_id = None keen.write_key = None keen.read_key = None + keen.master_key = None keen._client = None super(QueryTests, self).tearDown() @@ -355,5 +374,6 @@ def tearDown(self): keen.project_id = None keen.write_key = None keen.read_key = None + keen.master_key = None keen._client = None super(UnicodeTests, self).tearDown() From 57976ce8b324630301f7bc51d463095cb3e83047 Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Thu, 5 Feb 2015 20:17:41 +0100 Subject: [PATCH 024/224] rename test --- keen/tests/client_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 2492f2b..127c25f 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -149,7 +149,7 @@ def test_set_master_key_env_var(self): del os.environ["KEEN_MASTER_KEY"] - def test_set_master_key_env_var(self): + def test_set_master_key_package_var(self): exp_master_key = keen.master_key = "abcd4567" keen._initialize_client_from_environment() From dcf76007dc1d12f219eb2d5d6eca10ac62a2f58e Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Thu, 5 Feb 2015 20:31:28 +0100 Subject: [PATCH 025/224] update Documentation [skip ci] --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c5e5cb7..99508c3 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This client is known to work on Python 2.6, 2.7, 3.2 and 3.3 To use this client with the Keen IO API, you have to configure your Keen IO Project ID and its access keys (if you need an account, [sign up here](https://keen.io/) - it's free). -Setting a write key is required for publishing events. Setting a read key is required for running queries. The recommended way to set this configuration information is via the environment. The keys you can set are `KEEN_PROJECT_ID`, `KEEN_WRITE_KEY`, and `KEEN_READ_KEY`. +Setting a write key is required for publishing events. Setting a read key is required for running queries. The recommended way to set this configuration information is via the environment. The keys you can set are `KEEN_PROJECT_ID`, `KEEN_WRITE_KEY`, `KEEN_READ_KEY`, and `KEEN_MASTER_KEY`. If you don't want to use environment variables for some reason, you can directly set values as follows: @@ -28,6 +28,7 @@ If you don't want to use environment variables for some reason, you can directly keen.project_id = "xxxx" keen.write_key = "yyyy" keen.read_key = "zzzz" + keen.master_key = "abcd" ``` You can also configure unique client instances as follows: @@ -38,7 +39,8 @@ You can also configure unique client instances as follows: client = KeenClient( project_id="xxxx", write_key="yyyy", - read_key="zzzz" + read_key="zzzz", + master_key="abcd" ) ``` @@ -154,6 +156,7 @@ By default, POST requests will timeout after 305 seconds. If you want to manuall project_id="xxxx", write_key="yyyy", read_key="zzzz", + master_key="abcd", post_timeout=100 ) From 2d8a6d415392428f1c20393633bf89838ebfbde5 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Thu, 5 Feb 2015 15:23:54 -0800 Subject: [PATCH 026/224] Cleaned up code a bit. --- keen/tests/client_tests.py | 52 +++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 25d57e7..bacb903 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -30,13 +30,19 @@ def json(self): @patch("requests.Session.post") class ClientTests(BaseTestCase): + + SINGLE_ADD_RESPONSE = MockedRequest(status_code=201, json_response={"hello": "goodbye"}) + + MULTI_ADD_RESPONSE = MockedRequest(status_code=200, json_response={"hello": "goodbye"}) + + def setUp(self): super(ClientTests, self).setUp() - self.api_key = "2e79c6ec1d0145be8891bf668599c79a" + api_key = "2e79c6ec1d0145be8891bf668599c79a" keen._client = None keen.project_id = "5004ded1163d66114f000000" - keen.write_key = scoped_keys.encrypt(self.api_key, {"allowed_operations": ["write"]}) - keen.read_key = scoped_keys.encrypt(self.api_key, {"allowed_operations": ["read"]}) + keen.write_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}) + keen.read_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["read"]}) def test_init(self, post): def positive_helper(project_id, **kwargs): @@ -82,11 +88,11 @@ def negative_helper(expected_exception, project_id, persistence_strategy=persistence_strategies.DirectPersistenceStrategy) def test_direct_persistence_strategy(self, post): - post.return_value = MockedRequest(status_code=201, json_response={"hello": "goodbye"}) + post.return_value = self.SINGLE_ADD_RESPONSE keen.add_event("python_test", {"hello": "goodbye"}) keen.add_event("python_test", {"hello": "goodbye"}) - post.return_value = MockedRequest(status_code=200, json_response={"hello": "goodbye"}) + post.return_value = self.MULTI_ADD_RESPONSE keen.add_events( { "sign_ups": [{ @@ -102,11 +108,11 @@ def test_direct_persistence_strategy(self, post): ) def test_module_level_add_event(self, post): - post.return_value = MockedRequest(status_code=201, json_response={"hello": "goodbye"}) + post.return_value = self.SINGLE_ADD_RESPONSE keen.add_event("python_test", {"hello": "goodbye"}) def test_module_level_add_events(self, post): - post.return_value = MockedRequest(status_code=200, json_response={"hello": "goodbye"}) + post.return_value = self.MULTI_ADD_RESPONSE keen.add_events({"python_test": [{"hello": "goodbye"}]}) def test_post_timeout_single(self, post): @@ -209,9 +215,9 @@ def url_escape(self, url): @patch("requests.Session.get") class QueryTests(BaseTestCase): - int_response = MockedRequest(status_code=200, json_response=2) + INT_RESPONSE = MockedRequest(status_code=200, json_response=2) - list_response = MockedRequest( + LIST_RESPONSE = MockedRequest( status_code=200, json_response=[{"value": {"total": 1}}, {"value": {"total": 2}}]) def setUp(self): @@ -235,54 +241,54 @@ def get_filter(self): return [{"property_name": "number", "operator": "eq", "property_value": 5}] def test_count(self, get): - get.return_value = self.int_response + get.return_value = self.INT_RESPONSE resp = keen.count("query test", timeframe="today", filters=self.get_filter()) self.assertEqual(type(resp), int) def test_sum(self, get): - get.return_value = self.int_response + get.return_value = self.INT_RESPONSE resp = keen.sum("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) def test_minimum(self, get): - get.return_value = self.int_response + get.return_value = self.INT_RESPONSE resp = keen.minimum("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) def test_maximum(self, get): - get.return_value = self.int_response + get.return_value = self.INT_RESPONSE resp = keen.maximum("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) def test_average(self, get): - get.return_value = self.int_response + get.return_value = self.INT_RESPONSE resp = keen.average("query test", target_property="number", timeframe="today") self.assertTrue(type(resp) in (int, float), type(resp)) def test_percentile(self, get): - get.return_value = self.int_response + get.return_value = self.INT_RESPONSE resp = keen.percentile("query test", target_property="number", percentile=80, timeframe="today") self.assertTrue(type(resp) in (int, float), type(resp)) def test_count_unique(self, get): - get.return_value = self.int_response + get.return_value = self.INT_RESPONSE resp = keen.count_unique("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), int) def test_select_unique(self, get): - get.return_value = self.list_response + get.return_value = self.LIST_RESPONSE resp = keen.select_unique("query test", target_property="number", timeframe="today") self.assertEqual(type(resp), list) def test_extraction(self, get): - get.return_value = self.list_response + get.return_value = self.LIST_RESPONSE resp = keen.extraction("query test", timeframe="today", property_names=["number"]) self.assertEqual(type(resp), list) for event in resp: self.assertTrue("string" not in event) def test_multi_analysis(self, get): - get.return_value = self.list_response + get.return_value = self.LIST_RESPONSE resp = keen.multi_analysis("query test", analyses={"total": {"analysis_type": "sum", "target_property": "number"}}, timeframe="today", interval="hourly") @@ -291,7 +297,7 @@ def test_multi_analysis(self, get): self.assertEqual(type(result["value"]["total"]), int) def test_funnel(self, get): - get.return_value = self.list_response + get.return_value = self.LIST_RESPONSE step1 = { "event_collection": "query test", "actor_property": "number", @@ -306,17 +312,17 @@ def test_funnel(self, get): self.assertEqual(type(resp), list) def test_group_by(self, get): - get.return_value = self.list_response + get.return_value = self.LIST_RESPONSE resp = keen.count("query test", timeframe="today", group_by="number") self.assertEqual(type(resp), list) def test_multi_group_by(self, get): - get.return_value = self.list_response + get.return_value = self.LIST_RESPONSE resp = keen.count("query test", timeframe="today", group_by=["number", "string"]) self.assertEqual(type(resp), list) def test_interval(self, get): - get.return_value = self.list_response + get.return_value = self.LIST_RESPONSE resp = keen.count("query test", timeframe="this_2_days", interval="daily") self.assertEqual(type(resp), list) From 94d1f4effd85475f6f48fedd32b3c8698e8ecb80 Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Fri, 6 Feb 2015 17:07:27 +0100 Subject: [PATCH 027/224] add test for creating new Client instance --- keen/tests/client_tests.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 127c25f..cee629b 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -140,6 +140,26 @@ def test_environment_variables(self): self.assert_raises(exceptions.KeenApiError, keen.add_event, "python_test", {"hello": "goodbye"}) + def test_new_client_instance(self): + exp_project_id = "xxxx1234" + exp_write_key = "yyyy4567" + exp_read_key = "zzzz8912" + exp_master_key = "abcd3456" + + # create Client instance + client = KeenClient( + project_id=exp_project_id, + write_key=exp_write_key, + read_key=exp_read_key, + master_key=exp_master_key + ) + + # assert values + self.assertEquals(exp_project_id, client.api.project_id) + self.assertEquals(exp_write_key, client.api.write_key) + self.assertEquals(exp_read_key, client.api.read_key) + self.assertEquals(exp_master_key, client.api.master_key) + def test_set_master_key_env_var(self): exp_master_key = os.environ["KEEN_MASTER_KEY"] = "abcd1234" keen._initialize_client_from_environment() From b46414cc546428d62ead7f88d33a0532657dcbc2 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 6 Feb 2015 13:25:34 -0800 Subject: [PATCH 028/224] Added median query type. --- keen/__init__.py | 28 ++++++++++++++++++++++++++++ keen/client.py | 27 +++++++++++++++++++++++++++ keen/tests/client_tests.py | 5 +++++ 3 files changed, 60 insertions(+) diff --git a/keen/__init__.py b/keen/__init__.py index d6c505a..8fdb44c 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -179,6 +179,34 @@ def average(event_collection, target_property, timeframe=None, timezone=None, in target_property=target_property, max_age=max_age) +def median(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, + group_by=None, max_age=None): + """ Performs a median query + + Finds the median of a target property for events that meet the given criteria. + + :param event_collection: string, the name of the collection to query + :param target_property: string, the name of the event property you would like use + :param timeframe: string or dict, the timeframe in which the events + happened example: "previous_7_days" + :param timezone: int, the timezone you'd like to use for the timeframe + and interval in seconds + :param interval: string, the time interval used for measuring data over + time example: "daily" + :param filters: array of dict, contains the filters you'd like to apply to the data + example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + :param group_by: string or array of strings, the name(s) of the properties you would + like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're + willing to trade for increased query performance, in seconds + + """ + _initialize_client_from_environment() + return _client.median(event_collection=event_collection, timeframe=timeframe, timezone=timezone, + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) + + def percentile(event_collection, target_property, percentile, timeframe=None, timezone=None, interval=None, filters=None, group_by=None, max_age=None): """ Performs a percentile query diff --git a/keen/client.py b/keen/client.py index 994a1d2..a9edb59 100644 --- a/keen/client.py +++ b/keen/client.py @@ -307,6 +307,33 @@ def average(self, event_collection, target_property, timeframe=None, timezone=No target_property=target_property, max_age=max_age) return self.api.query("average", params) + def median(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, + filters=None, group_by=None, max_age=None): + """ Performs a median query + + Finds the median of a target property for events that meet the given criteria. + + :param event_collection: string, the name of the collection to query + :param target_property: string, the name of the event property you would like use + :param timeframe: string or dict, the timeframe in which the events + happened example: "previous_7_days" + :param timezone: int, the timezone you'd like to use for the timeframe + and interval in seconds + :param interval: string, the time interval used for measuring data over + time example: "daily" + :param filters: array of dict, contains the filters you'd like to apply to the data + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] + :param group_by: string or array of strings, the name(s) of the properties you would + like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're + willing to trade for increased query performance, in seconds + + """ + params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) + return self.api.query("median", params) + def percentile(self, event_collection, target_property, percentile, timeframe=None, timezone=None, interval=None, filters=None, group_by=None, max_age=None): """ Performs a percentile query diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index bacb903..9790272 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -265,6 +265,11 @@ def test_average(self, get): resp = keen.average("query test", target_property="number", timeframe="today") self.assertTrue(type(resp) in (int, float), type(resp)) + def test_median(self, get): + get.return_value = self.INT_RESPONSE + resp = keen.median("query test", target_property="number", timeframe="today") + self.assertTrue(type(resp) in (int, float), type(resp)) + def test_percentile(self, get): get.return_value = self.INT_RESPONSE resp = keen.percentile("query test", target_property="number", percentile=80, timeframe="today") From 716ac46dc760888fe5145dfae395cbea7271cd93 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 6 Feb 2015 13:48:42 -0800 Subject: [PATCH 029/224] Removed confusing comment. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8d54fa9..027d7a5 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ import sys try: - import multiprocessing # NOQA + import multiprocessing except ImportError: pass From 9daec2300c2cc8a4b3aa279ff8342c54e1a78b70 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 6 Feb 2015 14:37:03 -0800 Subject: [PATCH 030/224] Bumped up the version. --- README.md | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c5e5cb7..4cfd384 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,11 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.8 ++ Mocked tests. ++ Added ```median``` query method. ++ Added support for `$python setup.py test`. + ##### 0.3.7 + Upgraded to requests==2.5.1 diff --git a/setup.py b/setup.py index 027d7a5..fdd0452 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( name="keen", - version="0.3.7", + version="0.3.8", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From a575d2d99be2c7e4989bd28bc39b9f33d312aac7 Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Sat, 7 Feb 2015 13:26:14 +0100 Subject: [PATCH 031/224] tests take post argument --- keen/tests/client_tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 718e313..87a1354 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -149,7 +149,7 @@ def test_environment_variables(self, post): self.assert_raises(exceptions.KeenApiError, keen.add_event, "python_test", {"hello": "goodbye"}) - def test_new_client_instance(self): + def test_new_client_instance(self, post): exp_project_id = "xxxx1234" exp_write_key = "yyyy4567" exp_read_key = "zzzz8912" @@ -169,7 +169,7 @@ def test_new_client_instance(self): self.assertEquals(exp_read_key, client.api.read_key) self.assertEquals(exp_master_key, client.api.master_key) - def test_set_master_key_env_var(self): + def test_set_master_key_env_var(self, post): exp_master_key = os.environ["KEEN_MASTER_KEY"] = "abcd1234" keen._initialize_client_from_environment() @@ -178,7 +178,7 @@ def test_set_master_key_env_var(self): del os.environ["KEEN_MASTER_KEY"] - def test_set_master_key_package_var(self): + def test_set_master_key_package_var(self, post): exp_master_key = keen.master_key = "abcd4567" keen._initialize_client_from_environment() From 7483461c82800561819156e28c1ef393d6e056dd Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 9 Feb 2015 15:12:54 -0800 Subject: [PATCH 032/224] Bumped up the version. --- README.md | 6 ++++-- setup.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0d0288f..4d0b355 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,9 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.9 ++ Added ```master_key``` parameter. + ##### 0.3.8 + Mocked tests. + Added ```median``` query method. @@ -266,8 +269,7 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Questions & Support If you have any questions, bugs, or suggestions, please -report them via Github Issues. Or, come chat with us anytime -at [users.keen.io](http://users.keen.io). We'd love to hear your feedback and ideas! +report them via Github Issues. We'd love to hear your feedback and ideas! ### Contributing This is an open source project and we love involvement from the community! Hit us up with pull requests and issues. diff --git a/setup.py b/setup.py index fdd0452..ef12dac 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( name="keen", - version="0.3.8", + version="0.3.9", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From 86806a6ea1f772b64119b1f40fe71c661d834280 Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Wed, 11 Feb 2015 11:41:03 +0100 Subject: [PATCH 033/224] Add missing documentation --- keen/__init__.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/keen/__init__.py b/keen/__init__.py index 30db696..db81d72 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -12,6 +12,7 @@ def _initialize_client_from_environment(): + ''' Initialize a KeenCLient instance using environment variables. ''' global _client, project_id, write_key, read_key, master_key if _client is None: @@ -31,16 +32,42 @@ def _initialize_client_from_environment(): def add_event(event_collection, body, timestamp=None): + """ Adds an event. + + Depending on the persistence strategy of the client, + this will either result in the event being uploaded to Keen + immediately or will result in saving the event to some local cache. + + :param event_collection: the name of the collection to insert the + event to + :param body: dict, the body of the event to insert the event to + :param timestamp: datetime, optional, the timestamp of the event + """ _initialize_client_from_environment() _client.add_event(event_collection, body, timestamp=timestamp) def add_events(events): + """ Adds a batch of events. + + Depending on the persistence strategy of the client, + this will either result in the event being uploaded to Keen + immediately or will result in saving the event to some local cache. + + :param events: dictionary of events + """ _initialize_client_from_environment() _client.add_events(events) def generate_image_beacon(event_collection, body, timestamp=None): + """ Generates an image beacon URL. + + :param event_collection: the name of the collection to insert the + event to + :param body: dict, the body of the event to insert the event to + :param timestamp: datetime, optional, the timestamp of the event + """ _initialize_client_from_environment() return _client.generate_image_beacon(event_collection, body, timestamp=timestamp) From 3df4a1bbf320bd2200f558fa3b55c719415b1740 Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Wed, 11 Feb 2015 11:47:31 +0100 Subject: [PATCH 034/224] add missing parameter documentation --- keen/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keen/api.py b/keen/api.py index bcd1dbb..52764f6 100644 --- a/keen/api.py +++ b/keen/api.py @@ -61,8 +61,10 @@ def __init__(self, project_id, write_key=None, read_key=None, :param base_url: optional, set this to override where API requests are sent :param api_version: string, optional, set this to override what API - :param master_key: a Keen IO Master API Key version is used + :param get_timeout: optional, the timeout on GET requests + :param post_timeout: optional, the timeout on POST requests + :param master_key: a Keen IO Master API Key """ # super? recreates the object with values passed into KeenApi super(KeenApi, self).__init__() From 7176dbda9a05151da3eb94f981c3c43e035bbf07 Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Wed, 11 Feb 2015 12:26:46 +0100 Subject: [PATCH 035/224] add some more tests --- keen/tests/client_tests.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 87a1354..7509f31 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -169,20 +169,46 @@ def test_new_client_instance(self, post): self.assertEquals(exp_read_key, client.api.read_key) self.assertEquals(exp_master_key, client.api.master_key) - def test_set_master_key_env_var(self, post): + def test_set_keys_using_env_var(self, post): + exp_project_id = os.environ["KEEN_PROJECT_ID"] = "xxxx5678" + exp_write_key = os.environ["KEEN_WRITE_KEY"] = "yyyy8901" + exp_read_key = os.environ["KEEN_READ_KEY"] = "zzzz2345" exp_master_key = os.environ["KEEN_MASTER_KEY"] = "abcd1234" + keen._initialize_client_from_environment() + # test values + self.assertEquals(exp_project_id, keen.project_id) + self.assertEquals(exp_write_key, keen.write_key) + self.assertEquals(exp_read_key, keen.read_key) self.assertEquals(exp_master_key, keen.master_key) + self.assertEquals(exp_project_id, keen._client.api.project_id) + self.assertEquals(exp_write_key, keen._client.api.write_key) + self.assertEquals(exp_read_key, keen._client.api.read_key) self.assertEquals(exp_master_key, keen._client.api.master_key) + # remove env vars + del os.environ["KEEN_PROJECT_ID"] + del os.environ["KEEN_WRITE_KEY"] + del os.environ["KEEN_READ_KEY"] del os.environ["KEEN_MASTER_KEY"] - def test_set_master_key_package_var(self, post): + def test_set_keys_using_package_var(self, post): + exp_project_id = keen.project_id = "uuuu5678" + exp_write_key = keen.write_key = "vvvv8901" + exp_read_key = keen.read_key = "wwwww2345" exp_master_key = keen.master_key = "abcd4567" + keen._initialize_client_from_environment() + # test values + self.assertEquals(exp_project_id, keen.project_id) + self.assertEquals(exp_write_key, keen.write_key) + self.assertEquals(exp_read_key, keen.read_key) self.assertEquals(exp_master_key, keen.master_key) + self.assertEquals(exp_project_id, keen._client.api.project_id) + self.assertEquals(exp_write_key, keen._client.api.write_key) + self.assertEquals(exp_read_key, keen._client.api.read_key) self.assertEquals(exp_master_key, keen._client.api.master_key) def test_configure_through_code(self, post): From a686784cedda33bac30986035f98b97320cfb8c4 Mon Sep 17 00:00:00 2001 From: Dieter Adriaenssens Date: Wed, 11 Feb 2015 12:37:17 +0100 Subject: [PATCH 036/224] reset Client settings before test --- keen/tests/client_tests.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 7509f31..ad27367 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -170,6 +170,14 @@ def test_new_client_instance(self, post): self.assertEquals(exp_master_key, client.api.master_key) def test_set_keys_using_env_var(self, post): + # reset Client settings + keen._client = None + keen.project_id = None + keen.write_key = None + keen.read_key = None + keen.master_key = None + + # set env vars exp_project_id = os.environ["KEEN_PROJECT_ID"] = "xxxx5678" exp_write_key = os.environ["KEEN_WRITE_KEY"] = "yyyy8901" exp_read_key = os.environ["KEEN_READ_KEY"] = "zzzz2345" From fafdcf0b71aa1528756668b8e2c264265aa6b278 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 13 Feb 2015 11:50:45 -0800 Subject: [PATCH 037/224] Made mocked test inputs more accurate. --- keen/tests/client_tests.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 87a1354..052742c 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -125,8 +125,11 @@ def test_post_timeout_batch(self, post): self.assert_raises(requests.Timeout, keen.add_events, {"python_test": [{"hello": "goodbye"}]}) def test_environment_variables(self, post): - post.return_value = MockedFailedRequest(status_code=401, - json_response={"message": "authorization error", "error_code": 401}) + post.return_value = MockedFailedRequest( + status_code=401, + # "message" is the description, "error_code" is the name of the class. + json_response={"message": "authorization error", "error_code": "AdminOnlyEndpointError"}, + ) # try addEvent w/out having environment variables keen._client = None keen.project_id = None @@ -154,7 +157,7 @@ def test_new_client_instance(self, post): exp_write_key = "yyyy4567" exp_read_key = "zzzz8912" exp_master_key = "abcd3456" - + # create Client instance client = KeenClient( project_id=exp_project_id, @@ -194,7 +197,8 @@ def test_configure_through_code(self, post): client = KeenClient(project_id="123456", read_key=None, write_key="abcdef") with patch("requests.Session.post") as post: post.return_value = MockedFailedRequest( - status_code=401, json_response={"message": "authorization error", "error_code": 401} + status_code=401, + json_response={"message": "authorization error", "error_code": "AdminOnlyEndpointError"}, ) self.assert_raises(exceptions.KeenApiError, client.add_event, "python_test", {"hello": "goodbye"}) From 87e5777cbe5eafa7d14ae7916a8ef01a970d1838 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 20 Mar 2015 13:06:31 -0700 Subject: [PATCH 038/224] Updated setup. --- setup.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ef12dac..2153dba 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ # from distutils.core import setup from setuptools import setup, find_packages +from pip.req import parse_requirements import sys try: @@ -9,8 +10,14 @@ except ImportError: pass +# parse_requirements() returns generator of pip.req.InstallRequirement objects +install_reqs = parse_requirements("./requirements.txt") tests_require = ['nose'] +# reqs is a list of requirement +# e.g. ['django==1.5.1', 'mezzanine==1.4.6'] +reqs = [str(ir.req) for ir in install_reqs] + if sys.version_info < (2, 7): tests_require.append('unittest2') @@ -26,7 +33,7 @@ author_email="team@keen.io", url="https://github.com/keenlabs/KeenClient-Python", packages=["keen"], - install_requires=["requests", "pycrypto", "Padding"], + install_requires=reqs, tests_require=tests_require, test_suite='nose.collector', ) From 07e1cb73eb978073d4533b6447d560c40a5a63c9 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 20 Mar 2015 14:15:07 -0700 Subject: [PATCH 039/224] Bumped version, updated readme. --- README.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d0b355..4d3c4eb 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,10 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.10 ++ Fixed requirements in `setup.py` ++ Updated test inputs and documentation. + ##### 0.3.9 + Added ```master_key``` parameter. diff --git a/setup.py b/setup.py index 2153dba..9fee2c8 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name="keen", - version="0.3.9", + version="0.3.10", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From 921aa02832221677af830570932a752d67bfe5d7 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 20 Mar 2015 19:36:56 -0700 Subject: [PATCH 040/224] Fixed list package. Added manifest to include `requirements.txt` in pypi package. Added session, because latest version of pip requires session to be passed to parse requirements. --- MANIFEST.in | 3 +++ setup.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..ccf2a9b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include requirements.txt +include README.md +include LICENSE.txt diff --git a/setup.py b/setup.py index 9fee2c8..fa6572e 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ # from distutils.core import setup from setuptools import setup, find_packages from pip.req import parse_requirements +import pip import sys try: @@ -11,7 +12,7 @@ pass # parse_requirements() returns generator of pip.req.InstallRequirement objects -install_reqs = parse_requirements("./requirements.txt") +install_reqs = parse_requirements('requirements.txt', session=pip.download.PipSession()) tests_require = ['nose'] # reqs is a list of requirement From b7f2d6c8da9ba955fd2e2f328963f56c6041c927 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Fri, 20 Mar 2015 20:39:41 -0700 Subject: [PATCH 041/224] Bumped version. --- README.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d3c4eb..c26a886 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,9 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.11 ++ Added `requirements.txt` to pypi package. + ##### 0.3.10 + Fixed requirements in `setup.py` + Updated test inputs and documentation. diff --git a/setup.py b/setup.py index fa6572e..8a80ce8 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.10", + version="0.3.11", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From 40a3f33f786119e021e87af58f8c4616cdb70b7a Mon Sep 17 00:00:00 2001 From: Kevin Stone Date: Sun, 22 Mar 2015 16:52:36 -0700 Subject: [PATCH 042/224] Defined more sensible package dependencies. Fixes #54. --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index ae2ee74..164977b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -Padding==0.4 -nose==1.1.2 +Padding>=0.4 +nose>=1.1 pycrypto==2.6 -requests==2.5.1 -mock==1.0.1 +requests>=2.5 +mock From a6b7390ae7f435bd330d856b4ac6a81a999e5845 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Sun, 22 Mar 2015 18:55:18 -0700 Subject: [PATCH 043/224] Removed test requirements from `requirements.txt` --- requirements.txt | 2 -- setup.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 164977b..8a77722 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ Padding>=0.4 -nose>=1.1 pycrypto==2.6 requests>=2.5 -mock diff --git a/setup.py b/setup.py index 8a80ce8..2f641e4 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ # parse_requirements() returns generator of pip.req.InstallRequirement objects install_reqs = parse_requirements('requirements.txt', session=pip.download.PipSession()) -tests_require = ['nose'] +tests_require = ['nose', 'mock'] # reqs is a list of requirement # e.g. ['django==1.5.1', 'mezzanine==1.4.6'] From 7e6dce8a3bbd9334fa4ff0c145f7306c52e06037 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Sun, 22 Mar 2015 19:01:23 -0700 Subject: [PATCH 044/224] Bumped version to 0.3.12. --- README.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c26a886..d7b3142 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,9 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.12 ++ Made requirements more flexible. + ##### 0.3.11 + Added `requirements.txt` to pypi package. diff --git a/setup.py b/setup.py index 2f641e4..13503d2 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.11", + version="0.3.12", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From 4117607664d2e1121f650be5d2c22ab6bc89ce43 Mon Sep 17 00:00:00 2001 From: Taylor Edmiston Date: Thu, 30 Apr 2015 17:35:57 -0400 Subject: [PATCH 045/224] Add example of using unique client instances --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d7b3142..4e25681 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,12 @@ Once you've set `KEEN_PROJECT_ID` and `KEEN_WRITE_KEY`, sending events is simple }) ``` +Or if using unique client instances: + +```python + client.add_event(...) +``` + ##### Send Batch Events to Keen IO You can upload Events in a batch, like so: From d273286add38072c3dbe61b9743d240dba034a0d Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 4 May 2015 10:44:25 -0700 Subject: [PATCH 046/224] Added compatibility for pip < 1.5.6. --- setup.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 13503d2..f10b814 100644 --- a/setup.py +++ b/setup.py @@ -11,8 +11,13 @@ except ImportError: pass -# parse_requirements() returns generator of pip.req.InstallRequirement objects -install_reqs = parse_requirements('requirements.txt', session=pip.download.PipSession()) +try: + # parse_requirements() returns generator of pip.req.InstallRequirement objects + install_reqs = parse_requirements('requirements.txt', session=pip.download.PipSession()) +except AttributeError: + # compatibility for pip < 1.5.6 + install_reqs = parse_requirements('requirements.txt') + tests_require = ['nose', 'mock'] # reqs is a list of requirement From eee64c476d2871b410b53beed6c733f5c9608b0a Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 4 May 2015 11:34:08 -0700 Subject: [PATCH 047/224] Bumped version up to 0.3.13 --- README.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d7b3142..d92a42d 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,9 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.13 ++ Added compatibility for pip < 1.5.6 + ##### 0.3.12 + Made requirements more flexible. diff --git a/setup.py b/setup.py index f10b814..128529c 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ setup( name="keen", - version="0.3.12", + version="0.3.13", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From c715ce27a444b9c4e98aae96bdd92a4dd8ce7e3b Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Thu, 21 May 2015 13:58:55 -0700 Subject: [PATCH 048/224] Issued a fix for #31 which adds a generic check for errors - modified the api.py to have a seperate function for error_handling - added a check to find a generic error (by looking for the error_code in response) - if error_code is found, it raises whatever error is in the response --- keen/api.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/keen/api.py b/keen/api.py index 52764f6..209c620 100644 --- a/keen/api.py +++ b/keen/api.py @@ -113,9 +113,7 @@ def post_event(self, event): headers = {"Content-Type": "application/json", "Authorization": self.write_key} payload = event.to_json() response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) - if response.status_code != 201: - error = response.json() - raise exceptions.KeenApiError(error) + self.error_handling(response) def post_events(self, events): @@ -136,9 +134,7 @@ def post_events(self, events): headers = {"Content-Type": "application/json", "Authorization": self.write_key} payload = json.dumps(events) response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) - if response.status_code != 200: - error = response.json() - raise exceptions.KeenApiError(error) + self.error_handling(response) def query(self, analysis_type, params): """ @@ -158,8 +154,17 @@ def query(self, analysis_type, params): headers = {"Authorization": self.read_key} payload = params response = self.fulfill(HTTPMethods.GET, url, params=payload, headers=headers, timeout=self.get_timeout) - if response.status_code != 200: - error = response.json() - raise exceptions.KeenApiError(error) + self.error_handling(response) return response.json()["result"] + + def error_handling(self, res): + """ + Helper function to do the error handling + + :params res: the response from a request + """ + # making the error handling generic so if an error_code exists, we raise the error + if res.json().get("error_code"): + error = res.json() + raise exceptions.KeenApiError(error) From 7cef1da33fa2687a6889b59d436d64a23f9626e5 Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Fri, 22 May 2015 09:42:12 -0700 Subject: [PATCH 049/224] Updated the README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2a64afe..68bcd80 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,9 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.13 ++ Added better error handling to surface all errors from HTTP API calls + ##### 0.3.13 + Added compatibility for pip < 1.5.6 From dade3efc6e03ed3e813b0ed91f9a133e4493b7c6 Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Tue, 26 May 2015 14:49:37 -0700 Subject: [PATCH 050/224] integrate tests to the travis.yml file --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d63dc32..f7fcd4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,4 @@ python: # command to install dependencies install: "pip install -r requirements.txt --use-mirrors" # command to run tests -script: nosetests \ No newline at end of file +script: "python setup.py test" \ No newline at end of file From db41d77cbdb42d0bb8326e4585d1c2f73ec8c708 Mon Sep 17 00:00:00 2001 From: Jeremy Dunck Date: Tue, 2 Jun 2015 16:44:15 -0700 Subject: [PATCH 051/224] Simplify requirements inclusion for compatibility with old pip --- setup.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/setup.py b/setup.py index 128529c..2b1ac38 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,15 @@ #!/usr/bin/env python -# from distutils.core import setup -from setuptools import setup, find_packages -from pip.req import parse_requirements -import pip -import sys +from setuptools import setup +import os, sys -try: - import multiprocessing -except ImportError: - pass - -try: - # parse_requirements() returns generator of pip.req.InstallRequirement objects - install_reqs = parse_requirements('requirements.txt', session=pip.download.PipSession()) -except AttributeError: - # compatibility for pip < 1.5.6 - install_reqs = parse_requirements('requirements.txt') +setup_path = os.path.dirname(__file__) +reqs_file = open(os.path.join(setup_path, 'requirements.txt'), 'r') +reqs = reqs_file.readlines() +reqs_file.close() tests_require = ['nose', 'mock'] -# reqs is a list of requirement -# e.g. ['django==1.5.1', 'mezzanine==1.4.6'] -reqs = [str(ir.req) for ir in install_reqs] - if sys.version_info < (2, 7): tests_require.append('unittest2') @@ -33,7 +19,7 @@ setup( name="keen", - version="0.3.13", + version="0.3.14", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From b43ca54a73c7f9d5b4733ecf3fd2465e8d8ae9c1 Mon Sep 17 00:00:00 2001 From: Jeremy Dunck Date: Tue, 2 Jun 2015 16:56:34 -0700 Subject: [PATCH 052/224] Update changelog for 0.3.14 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 2a64afe..f713eaf 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,9 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.14 ++ Added compatibility for pip 1.0 + ##### 0.3.13 + Added compatibility for pip < 1.5.6 From 9cef63e1f422005485f9d9820ef3c54a9577a773 Mon Sep 17 00:00:00 2001 From: Jeremy Dunck Date: Wed, 3 Jun 2015 12:12:42 -0700 Subject: [PATCH 053/224] Keep multiprocessing for nose compat Also remove unused setup_requires --- setup.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 2b1ac38..2353ded 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,19 @@ from setuptools import setup import os, sys +try: + # nose uses multiprocessing if available. + # but setup.py atexit fails if it's loaded too late. + # Traceback (most recent call last): + # File "...python2.6/atexit.py", line 24, in _run_exitfuncs + # func(*targs, **kargs) + # File "...python2.6/multiprocessing/util.py", line 258, in _exit_function + # info('process shutting down') + # TypeError: 'NoneType' object is not callable + import multiprocessing # NOQA +except ImportError: + pass + setup_path = os.path.dirname(__file__) reqs_file = open(os.path.join(setup_path, 'requirements.txt'), 'r') reqs = reqs_file.readlines() @@ -13,10 +26,6 @@ if sys.version_info < (2, 7): tests_require.append('unittest2') -setup_requires = [] -if 'nosetests' in sys.argv[1:]: - setup_requires.append('nose') - setup( name="keen", version="0.3.14", From 8d45b0c881159a6cbc9f89b509ec4b02a759fc99 Mon Sep 17 00:00:00 2001 From: Jeremy Dunck Date: Wed, 3 Jun 2015 13:01:01 -0700 Subject: [PATCH 054/224] Add tox config --- .gitignore | 1 + tox.ini | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 3971ce7..d0a087a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # Packages *.egg +*.eggs *.egg-info dist build diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..a96aa14 --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist = py26,py27,py32,py33,pypy + +[testenv] +commands=python setup.py test From 3d3a5ccecd32e63fa35acb493049e50777f81ffc Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Wed, 3 Jun 2015 13:14:26 -0700 Subject: [PATCH 055/224] changed the error checking to look for status code --- keen/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/api.py b/keen/api.py index 209c620..2b7be04 100644 --- a/keen/api.py +++ b/keen/api.py @@ -165,6 +165,6 @@ def error_handling(self, res): :params res: the response from a request """ # making the error handling generic so if an error_code exists, we raise the error - if res.json().get("error_code"): + if res.status_code/100 != 2: error = res.json() raise exceptions.KeenApiError(error) From dd6e5a86dd570304d67152051c7ee8d860311293 Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Wed, 3 Jun 2015 13:16:39 -0700 Subject: [PATCH 056/224] changed the comments to reflect previous commit --- keen/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/api.py b/keen/api.py index 2b7be04..a0d93a4 100644 --- a/keen/api.py +++ b/keen/api.py @@ -164,7 +164,7 @@ def error_handling(self, res): :params res: the response from a request """ - # making the error handling generic so if an error_code exists, we raise the error + # making the error handling generic so if an status_code starting with 2 exists, we raise the error if res.status_code/100 != 2: error = res.json() raise exceptions.KeenApiError(error) From 550d33ee6ab90fdfcbe605301e8306b8b53a7135 Mon Sep 17 00:00:00 2001 From: Jeremy Dunck Date: Thu, 4 Jun 2015 08:37:13 -0700 Subject: [PATCH 057/224] Allow pycrypto to float on 2.6, disallow requests past 2.x Addresses #64. --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8a77722..a1f831f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ Padding>=0.4 -pycrypto==2.6 -requests>=2.5 +pycrypto>=2.6,<2.7 +requests>=2.5,<3.0 From b78ef52cdd9281f6ff36eea1a593df9bc7deba3c Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Thu, 4 Jun 2015 15:36:39 -0700 Subject: [PATCH 058/224] fixed comment to reflect code --- keen/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/api.py b/keen/api.py index a0d93a4..eda7118 100644 --- a/keen/api.py +++ b/keen/api.py @@ -164,7 +164,7 @@ def error_handling(self, res): :params res: the response from a request """ - # making the error handling generic so if an status_code starting with 2 exists, we raise the error + # making the error handling generic so if an status_code starting with 2 doesn't exist, we raise the error if res.status_code/100 != 2: error = res.json() raise exceptions.KeenApiError(error) From 77aef631cdf9114114b3236f7e090dd1a10eb51e Mon Sep 17 00:00:00 2001 From: Jeremy Dunck Date: Thu, 4 Jun 2015 17:39:51 -0700 Subject: [PATCH 059/224] Fix integer division under python3 --- keen/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/api.py b/keen/api.py index eda7118..66b5dae 100644 --- a/keen/api.py +++ b/keen/api.py @@ -165,6 +165,6 @@ def error_handling(self, res): :params res: the response from a request """ # making the error handling generic so if an status_code starting with 2 doesn't exist, we raise the error - if res.status_code/100 != 2: + if res.status_code // 100 != 2: error = res.json() raise exceptions.KeenApiError(error) From 2160b725aadce5f01956a4c790d970c9724efdc0 Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Thu, 11 Jun 2015 11:23:10 -0700 Subject: [PATCH 060/224] bumped version number --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4781fd6..782627a 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,9 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.15 ++ Added better error handling to surface all errors from HTTP API calls + ##### 0.3.14 + Added compatibility for pip 1.0 From ef974d96565b57ee7e63e16717610110ea78ee02 Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Thu, 11 Jun 2015 14:44:33 -0700 Subject: [PATCH 061/224] corrected README and added error catching for malformed JSON --- README.md | 3 --- keen/api.py | 8 +++++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 782627a..fc0169b 100644 --- a/README.md +++ b/README.md @@ -194,9 +194,6 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ##### 0.3.14 + Added compatibility for pip 1.0 -##### 0.3.13 -+ Added better error handling to surface all errors from HTTP API calls - ##### 0.3.13 + Added compatibility for pip < 1.5.6 diff --git a/keen/api.py b/keen/api.py index 66b5dae..de563cc 100644 --- a/keen/api.py +++ b/keen/api.py @@ -10,6 +10,9 @@ # keen exceptions from keen import exceptions +# json +from requests.compat import json + __author__ = 'dkador' @@ -166,5 +169,8 @@ def error_handling(self, res): """ # making the error handling generic so if an status_code starting with 2 doesn't exist, we raise the error if res.status_code // 100 != 2: - error = res.json() + try: + error = res.json() + except json.JSONDecodeError: + error = {'message': 'The API did not respond with JSON, but: "{0}"'.format(res.text[:1000])', "error_code": "InvalidResponseFormat"} raise exceptions.KeenApiError(error) From 6a521ca95eb0f2141da1e6781d18b1c562a7b96b Mon Sep 17 00:00:00 2001 From: Tushar Bhushan Date: Thu, 11 Jun 2015 15:38:30 -0700 Subject: [PATCH 062/224] fixed syntax error --- keen/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/api.py b/keen/api.py index de563cc..89e011c 100644 --- a/keen/api.py +++ b/keen/api.py @@ -172,5 +172,5 @@ def error_handling(self, res): try: error = res.json() except json.JSONDecodeError: - error = {'message': 'The API did not respond with JSON, but: "{0}"'.format(res.text[:1000])', "error_code": "InvalidResponseFormat"} + error = {'message': 'The API did not respond with JSON, but: "{0}"'.format(res.text[:1000]), "error_code": "InvalidResponseFormat"} raise exceptions.KeenApiError(error) From 765fb37e227ade9c4e14410163041fd62a2eb717 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Thu, 11 Jun 2015 17:06:57 -0700 Subject: [PATCH 063/224] Bumped the version to 0.3.15. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2353ded..86c3372 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.14", + version="0.3.15", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From d2baa4b2981d18ad4a757286707cc9c1ef0fbdb5 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Sat, 27 Jun 2015 20:59:12 -0700 Subject: [PATCH 064/224] Added `delete_events` method. --- keen/__init__.py | 18 ++++++++++++++++++ keen/api.py | 33 ++++++++++++++++++++++++++++++--- keen/client.py | 15 +++++++++++++++ keen/tests/client_tests.py | 25 +++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index db81d72..99f9798 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -403,3 +403,21 @@ def multi_analysis(event_collection, analyses, timeframe=None, interval=None, return _client.multi_analysis(event_collection=event_collection, timeframe=timeframe, interval=interval, timezone=timezone, filters=filters, group_by=group_by, analyses=analyses, max_age=max_age) + + +def delete_events(*args, **kwargs): + """ Performs a delete for events. + + Returns true upon success. + + :param event_collection: string, the event collection from which event are being deleted + :param timeframe: string or dict, the timeframe in which the events + happened example: "previous_7_days" + :param timezone: int, the timezone you'd like to use for the timeframe + and interval in seconds + :param filters: array of dict, contains the filters you'd like to apply to the data + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] + + """ + _initialize_client_from_environment() + return _client.delete_events(*args, **kwargs) diff --git a/keen/api.py b/keen/api.py index 89e011c..38418fb 100644 --- a/keen/api.py +++ b/keen/api.py @@ -23,6 +23,7 @@ class HTTPMethods(object): GET = 'get' POST = 'post' + DELETE = 'delete' class KeenAdapter(HTTPAdapter): @@ -67,7 +68,7 @@ def __init__(self, project_id, write_key=None, read_key=None, version is used :param get_timeout: optional, the timeout on GET requests :param post_timeout: optional, the timeout on POST requests - :param master_key: a Keen IO Master API Key + :param master_key: a Keen IO Master API Key, needed for deletes """ # super? recreates the object with values passed into KeenApi super(KeenApi, self).__init__() @@ -161,9 +162,32 @@ def query(self, analysis_type, params): return response.json()["result"] + def delete_events(self, event_collection, params): + """ + Deletes events via the Keen IO API. A master key must be set first. + + :param event_collection: string, the event collection from which event are being deleted + + """ + if not self.master_key: + raise exceptions.InvalidEnvironmentError( + "The Keen IO API requires a master key to run deletes. " + "Please set a 'master_key' when initializing the KeenApi object." + ) + + url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, + self.api_version, + self.project_id, + event_collection) + headers = {"Content-Type": "application/json", "Authorization": self.master_key} + response = self.fulfill(HTTPMethods.DELETE, url, params=params, headers=headers, timeout=self.post_timeout) + + self.error_handling(response) + return True + def error_handling(self, res): """ - Helper function to do the error handling + Helper function to do the error handling :params res: the response from a request """ @@ -172,5 +196,8 @@ def error_handling(self, res): try: error = res.json() except json.JSONDecodeError: - error = {'message': 'The API did not respond with JSON, but: "{0}"'.format(res.text[:1000]), "error_code": "InvalidResponseFormat"} + error = { + 'message': 'The API did not respond with JSON, but: "{0}"'.format(res.text[:1000]), + "error_code": "InvalidResponseFormat" + } raise exceptions.KeenApiError(error) diff --git a/keen/client.py b/keen/client.py index 86b8206..8b7410e 100644 --- a/keen/client.py +++ b/keen/client.py @@ -155,6 +155,21 @@ def generate_image_beacon(self, event_collection, event_body, timestamp=None): self.api.write_key.decode(sys.getdefaultencoding()), self._base64_encode(event_json) ) + def delete_events(self, event_collection, timeframe=None, timezone=None, filters=None): + """ Deletes events. + + :param event_collection: string, the event collection from which event are being deleted + :param timeframe: string or dict, the timeframe in which the events happened + example: "previous_7_days" + :param timezone: int, the timezone you'd like to use for the timeframe + and interval in seconds + :param filters: array of dict, contains the filters you'd like to apply to the data + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] + + """ + params = self.get_params(timeframe=timeframe, timezone=timezone, filters=filters) + return self.api.delete_events(event_collection, params) + def _base64_encode(self, string_to_encode): """ Base64 encodes a string, with either Python 2 or 3. diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index c8cfcbf..dec1896 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -435,6 +435,31 @@ def test_timeout_count(self, get): # Make sure the requests library was called with `timeout`. self.assert_equals(get.call_args[1]["timeout"], 0.0001) + +@patch("requests.Session.delete") +class DeleteTests(BaseTestCase): + + def setUp(self): + super(DeleteTests, self).setUp() + keen._client = None + keen.project_id = "1k4jb23kjbkjkjsd" + keen.master_key = "sdofnasofagaergub" + + def tearDown(self): + keen._client = None + keen.project_id = None + keen.master_key = None + super(DeleteTests, self).tearDown() + + def test_delete_events(self, delete): + delete.return_value = MockedRequest(status_code=204, json_response=[]) + # Assert that the mocked delete function is called the way we expect. + keen.delete_events("foo", filters=[{"property_name": 'username', "operator": 'eq', "property_value": 'Bob'}]) + # Check that the URL is generated correctly. + self.assertEqual("https://api.keen.io/3.0/projects/1k4jb23kjbkjkjsd/events/foo", delete.call_args[0][0]) + # Check that the master_key is in the Authorization header. + self.assertTrue(keen.master_key in delete.call_args[1]["headers"]["Authorization"]) + # only need to test unicode separately in python2 if sys.version_info[0] < 3: From efd9657cb0088c7ecf1a6696303ddf14727a75af Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Mon, 29 Jun 2015 13:06:12 -0700 Subject: [PATCH 065/224] Adds delete_event method --- keen/api.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/keen/api.py b/keen/api.py index 89e011c..800224f 100644 --- a/keen/api.py +++ b/keen/api.py @@ -118,6 +118,27 @@ def post_event(self, event): response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self.error_handling(response) + def delete_event(self, event): + """ + Deletes a single event in the Keen IO API. The master key must be set first + + :param event: an Event to delete + """ + if not self.master_key: + raise exceptions.InvalidEnvironmentError( + "The Keen IO API requires a master key to delete events. " + "Please set a 'master_key' when initializing the " + "KeenApi object." + ) + + url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, + self.project_id, + event.event_collection) + headers = {"Content-Type": "application/json", "Authorization": self.master_key} + payload = event.to_json() + response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) + self.error_handling(response) + def post_events(self, events): """ @@ -163,7 +184,7 @@ def query(self, analysis_type, params): def error_handling(self, res): """ - Helper function to do the error handling + Helper function to do the error handling :params res: the response from a request """ From bf2d0274c73677331ce691d014d058022955e1ed Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Mon, 29 Jun 2015 13:27:26 -0700 Subject: [PATCH 066/224] Adds delete_events method --- keen/api.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/keen/api.py b/keen/api.py index 800224f..ed9d383 100644 --- a/keen/api.py +++ b/keen/api.py @@ -118,9 +118,30 @@ def post_event(self, event): response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self.error_handling(response) + def post_events(self, events): + + """ + Posts a single event to the Keen IO API. The write key must be set first. + + :param events: an Event to upload + """ + if not self.write_key: + raise exceptions.InvalidEnvironmentError( + "The Keen IO API requires a write key to send events. " + "Please set a 'write_key' when initializing the " + "KeenApi object." + ) + + url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, + self.project_id) + headers = {"Content-Type": "application/json", "Authorization": self.write_key} + payload = json.dumps(events) + response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) + self.error_handling(response) + def delete_event(self, event): """ - Deletes a single event in the Keen IO API. The master key must be set first + Deletes a single event in the Keen IO API. The master key must be set first. :param event: an Event to delete """ @@ -139,23 +160,22 @@ def delete_event(self, event): response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self.error_handling(response) - def post_events(self, events): - + def delete_events(self, events): """ - Posts a single event to the Keen IO API. The write key must be set first. + Deletes multiple events in the Keen IO API. The master key must be set first. - :param events: an Event to upload + :param events: Events to delete """ if not self.write_key: raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a write key to send events. " - "Please set a 'write_key' when initializing the " + "The Keen IO API requires a master key to delete events. " + "Please set a 'master_key' when initializing the " "KeenApi object." ) url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, self.project_id) - headers = {"Content-Type": "application/json", "Authorization": self.write_key} + headers = {"Content-Type": "application/json", "Authorization": self.master_key} payload = json.dumps(events) response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self.error_handling(response) From 5cdf1c99171b02239ab03b1261863729ae9503b4 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 29 Jun 2015 14:58:06 -0700 Subject: [PATCH 067/224] Updated Readme. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index fc0169b..d734a94 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,16 @@ Here are some examples of querying. Let's assume you've added some events to th keen.funnel([step1, step2], timeframe="today") # => [2039, 201] ``` +##### Delete Events + +The Keen IO API allows you to [delete events](https://keen.io/docs/api/#delete-events) from event collections, optionally supplying filters, timeframe or timezone to narrow the scope of what you would like to delete. + +You'll need to set your master_key. + +```python + keen.delete_events("event_collection", filters=[{"property_name": 'username', "operator": 'eq', "property_value": 'Bob'}]) +``` + #### Advanced Usage See below for more options. From 8c4b480cc33ba0efe7316388e534db9ebb1cc4c4 Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Mon, 29 Jun 2015 18:03:22 -0700 Subject: [PATCH 068/224] Adds get_collection and get_collections method to the api --- keen/api.py | 43 ++++++++++++++++++++++++++++++++++++++++++- keen/client.py | 18 ++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/keen/api.py b/keen/api.py index 89e011c..c43c07a 100644 --- a/keen/api.py +++ b/keen/api.py @@ -161,9 +161,50 @@ def query(self, analysis_type, params): return response.json()["result"] + def get_collection(self, event_name): + """ + Extracts info about a collection using the Keen IO API. A master key must be set first. + + :param event_name: the name of the collection to retrieve info for + + """ + if not self.master_key: + raise exception.InvalidEnvironmentError( + "The Keen IO API requires a master key to get events. " + "Please set a 'master_key' when initializing the " + "KeenApi object." + ) + url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, + self.project_id, + event_name) + headers = {"Authorization": self.master_key} + response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) + self.error_handling(response) + + return response.json() + + def get_all_collections(self): + """ + Extracts info about collections using the Keen IO API. A master key must be set first. + + """ + if not self.master_key: + raise exception.InvalidEnvironmentError( + "The Keen IO API requires a master key to get events. " + "Please set a 'master_key' when initializing the " + "KeenApi object." + ) + url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, + self.project_id) + headers = {"Authorization": self.master_key} + response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) + self.error_handling(response) + + return response.json() + def error_handling(self, res): """ - Helper function to do the error handling + Helper function to do the error handling :params res: the response from a request """ diff --git a/keen/client.py b/keen/client.py index 86b8206..5528d82 100644 --- a/keen/client.py +++ b/keen/client.py @@ -470,6 +470,24 @@ def funnel(self, steps, timeframe=None, timezone=None, max_age=None): params = self.get_params(steps=steps, timeframe=timeframe, timezone=timezone, max_age=max_age) return self.api.query("funnel", params) + def get_collection(self, event_name): + """ + + Returns a description of a collection + + :param event_name: the name of the event to get the collection info from + """ + + return self.api.get_collection(event_name) + + def get_collections(self): + """ + + Returns a description of all collections + """ + + return self.api.get_all_collections() + def multi_analysis(self, event_collection, analyses, timeframe=None, interval=None, timezone=None, filters=None, group_by=None, max_age=None): """ Performs a multi-analysis query From 539d6ac2714406d605917921f328931aa6b3da29 Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Mon, 29 Jun 2015 19:07:16 -0700 Subject: [PATCH 069/224] Adds delete_collection and delete_collections method to client --- keen/api.py | 31 ++++++------------------------- keen/client.py | 23 +++++++++++++++++++++-- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/keen/api.py b/keen/api.py index 73eaaf8..b1c2be5 100644 --- a/keen/api.py +++ b/keen/api.py @@ -23,6 +23,7 @@ class HTTPMethods(object): GET = 'get' POST = 'post' + DELETE = 'delete' class KeenAdapter(HTTPAdapter): @@ -139,7 +140,7 @@ def post_events(self, events): response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self.error_handling(response) - def delete_event(self, event): + def delete_collection(self, collection): """ Deletes a single event in the Keen IO API. The master key must be set first. @@ -154,30 +155,10 @@ def delete_event(self, event): url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, self.project_id, - event.event_collection) - headers = {"Content-Type": "application/json", "Authorization": self.master_key} - payload = event.to_json() - response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) - self.error_handling(response) - - def delete_events(self, events): - """ - Deletes multiple events in the Keen IO API. The master key must be set first. - - :param events: Events to delete - """ - if not self.write_key: - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a master key to delete events. " - "Please set a 'master_key' when initializing the " - "KeenApi object." - ) - - url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, - self.project_id) + collection['name']) headers = {"Content-Type": "application/json", "Authorization": self.master_key} - payload = json.dumps(events) - response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) + payload = json.dumps(collection) + response = self.fulfill(HTTPMethods.DELETE, url, data=payload, headers=headers, timeout=self.post_timeout) self.error_handling(response) def query(self, analysis_type, params): @@ -202,7 +183,7 @@ def query(self, analysis_type, params): return response.json()["result"] - def get_collection(self, event_name): + def get_collection(self, collection_name): """ Extracts info about a collection using the Keen IO API. A master key must be set first. diff --git a/keen/client.py b/keen/client.py index 5528d82..2d1dfa6 100644 --- a/keen/client.py +++ b/keen/client.py @@ -470,7 +470,7 @@ def funnel(self, steps, timeframe=None, timezone=None, max_age=None): params = self.get_params(steps=steps, timeframe=timeframe, timezone=timezone, max_age=max_age) return self.api.query("funnel", params) - def get_collection(self, event_name): + def get_collection(self, collection_name): """ Returns a description of a collection @@ -478,7 +478,7 @@ def get_collection(self, event_name): :param event_name: the name of the event to get the collection info from """ - return self.api.get_collection(event_name) + return self.api.get_collection(collection_name) def get_collections(self): """ @@ -488,6 +488,25 @@ def get_collections(self): return self.api.get_all_collections() + def delete_collection(self, collection): + """ Performs a deletion of a collection + + + :param collection_name: the name of the collection + """ + + self.api.delete_collection(collection) + + def delete_collections(self, collections): + """ Performs a deletion of multiple collections in the list + + :param collections: a list of collections to be deleted + """ + + for collection in collections: + self.delete_collection(collection) + + def multi_analysis(self, event_collection, analyses, timeframe=None, interval=None, timezone=None, filters=None, group_by=None, max_age=None): """ Performs a multi-analysis query From 5d52f44a39a21a1e26c58a691a8311f92b11f3bf Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Tue, 30 Jun 2015 07:36:55 -0700 Subject: [PATCH 070/224] Updates comments --- keen/api.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/keen/api.py b/keen/api.py index b1c2be5..e90fcfe 100644 --- a/keen/api.py +++ b/keen/api.py @@ -142,9 +142,9 @@ def post_events(self, events): def delete_collection(self, collection): """ - Deletes a single event in the Keen IO API. The master key must be set first. + Deletes a single collection in the Keen IO API. The master key must be set first. - :param event: an Event to delete + :param event: a Collection to delete """ if not self.master_key: raise exceptions.InvalidEnvironmentError( @@ -188,7 +188,6 @@ def get_collection(self, collection_name): Extracts info about a collection using the Keen IO API. A master key must be set first. :param event_name: the name of the collection to retrieve info for - """ if not self.master_key: raise exception.InvalidEnvironmentError( @@ -197,8 +196,7 @@ def get_collection(self, collection_name): "KeenApi object." ) url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, - self.project_id, - event_name) + self.project_id, collection_name) headers = {"Authorization": self.master_key} response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) self.error_handling(response) @@ -207,7 +205,7 @@ def get_collection(self, collection_name): def get_all_collections(self): """ - Extracts info about collections using the Keen IO API. A master key must be set first. + Extracts info about all collections using the Keen IO API. A master key must be set first. """ if not self.master_key: @@ -217,7 +215,7 @@ def get_all_collections(self): "KeenApi object." ) url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, - self.project_id) + self.project_id) headers = {"Authorization": self.master_key} response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) self.error_handling(response) From 01820b1a886b21f173dc97fae1193790101fefdb Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Tue, 30 Jun 2015 07:44:34 -0700 Subject: [PATCH 071/224] Changes delete_collections to use the api delete_collection method rather than the client's --- keen/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/client.py b/keen/client.py index 2d1dfa6..28e0a07 100644 --- a/keen/client.py +++ b/keen/client.py @@ -504,7 +504,7 @@ def delete_collections(self, collections): """ for collection in collections: - self.delete_collection(collection) + self.api.delete_collection(collection) def multi_analysis(self, event_collection, analyses, timeframe=None, interval=None, timezone=None, filters=None, From 72a6e8cd1792dd2184e1763927f20298db6c77f9 Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Tue, 30 Jun 2015 08:57:36 -0700 Subject: [PATCH 072/224] Fixes exception to exceptions when not having a master key --- keen/api.py | 4 ++-- keen/client.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/keen/api.py b/keen/api.py index e90fcfe..32af85b 100644 --- a/keen/api.py +++ b/keen/api.py @@ -190,7 +190,7 @@ def get_collection(self, collection_name): :param event_name: the name of the collection to retrieve info for """ if not self.master_key: - raise exception.InvalidEnvironmentError( + raise exceptions.InvalidEnvironmentError( "The Keen IO API requires a master key to get events. " "Please set a 'master_key' when initializing the " "KeenApi object." @@ -209,7 +209,7 @@ def get_all_collections(self): """ if not self.master_key: - raise exception.InvalidEnvironmentError( + raise exceptions.InvalidEnvironmentError( "The Keen IO API requires a master key to get events. " "Please set a 'master_key' when initializing the " "KeenApi object." diff --git a/keen/client.py b/keen/client.py index 28e0a07..44827d4 100644 --- a/keen/client.py +++ b/keen/client.py @@ -506,7 +506,6 @@ def delete_collections(self, collections): for collection in collections: self.api.delete_collection(collection) - def multi_analysis(self, event_collection, analyses, timeframe=None, interval=None, timezone=None, filters=None, group_by=None, max_age=None): """ Performs a multi-analysis query From 593feb3393fa9578a4d17c8a6c2d2c340f9393af Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Sat, 27 Jun 2015 16:07:42 -0700 Subject: [PATCH 073/224] =?UTF-8?q?Added=20=E2=80=9Call=5Fkey=E2=80=9D=20p?= =?UTF-8?q?aram=20+=20funnel=20test.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keen/__init__.py | 14 +++++------ keen/api.py | 9 +++++-- keen/client.py | 13 +++++++--- keen/tests/client_tests.py | 50 ++++++++++++++++++++++++-------------- 4 files changed, 56 insertions(+), 30 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index 99f9798..e35530e 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -33,11 +33,11 @@ def _initialize_client_from_environment(): def add_event(event_collection, body, timestamp=None): """ Adds an event. - + Depending on the persistence strategy of the client, this will either result in the event being uploaded to Keen immediately or will result in saving the event to some local cache. - + :param event_collection: the name of the collection to insert the event to :param body: dict, the body of the event to insert the event to @@ -49,11 +49,11 @@ def add_event(event_collection, body, timestamp=None): def add_events(events): """ Adds a batch of events. - + Depending on the persistence strategy of the client, this will either result in the event being uploaded to Keen immediately or will result in saving the event to some local cache. - + :param events: dictionary of events """ _initialize_client_from_environment() @@ -62,7 +62,7 @@ def add_events(events): def generate_image_beacon(event_collection, body, timestamp=None): """ Generates an image beacon URL. - + :param event_collection: the name of the collection to insert the event to :param body: dict, the body of the event to insert the event to @@ -355,7 +355,7 @@ def extraction(event_collection, timeframe=None, timezone=None, filters=None, la filters=filters, latest=latest, email=email, property_names=property_names) -def funnel(steps, timeframe=None, timezone=None, max_age=None): +def funnel(*args, **kwargs): """ Performs a Funnel query Returns an object containing the results for each step of the funnel. @@ -372,7 +372,7 @@ def funnel(steps, timeframe=None, timezone=None, max_age=None): """ _initialize_client_from_environment() - return _client.funnel(steps=steps, timeframe=timeframe, timezone=timezone, max_age=max_age) + return _client.funnel(*args, **kwargs) def multi_analysis(event_collection, analyses, timeframe=None, interval=None, diff --git a/keen/api.py b/keen/api.py index 38418fb..958690a 100644 --- a/keen/api.py +++ b/keen/api.py @@ -140,7 +140,7 @@ def post_events(self, events): response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self.error_handling(response) - def query(self, analysis_type, params): + def query(self, analysis_type, params, all_keys=False): """ Performs a query using the Keen IO analysis API. A read key must be set first. @@ -160,7 +160,12 @@ def query(self, analysis_type, params): response = self.fulfill(HTTPMethods.GET, url, params=payload, headers=headers, timeout=self.get_timeout) self.error_handling(response) - return response.json()["result"] + response = response.json() + + if not all_keys: + response = response["result"] + + return response def delete_events(self, event_collection, params): """ diff --git a/keen/client.py b/keen/client.py index 8b7410e..ed37895 100644 --- a/keen/client.py +++ b/keen/client.py @@ -466,7 +466,7 @@ def extraction(self, event_collection, timeframe=None, timezone=None, filters=No filters=filters, latest=latest, email=email, property_names=property_names) return self.api.query("extraction", params) - def funnel(self, steps, timeframe=None, timezone=None, max_age=None): + def funnel(self, steps, timeframe=None, timezone=None, max_age=None, all_keys=False): """ Performs a Funnel query Returns an object containing the results for each step of the funnel. @@ -480,10 +480,17 @@ def funnel(self, steps, timeframe=None, timezone=None, max_age=None): and interval in seconds :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds + :all_keys: set to true to return all keys on response (i.e. "result", "actors", "steps") """ - params = self.get_params(steps=steps, timeframe=timeframe, timezone=timezone, max_age=max_age) - return self.api.query("funnel", params) + params = self.get_params( + steps=steps, + timeframe=timeframe, + timezone=timezone, + max_age=max_age, + ) + + return self.api.query("funnel", params, all_keys=all_keys) def multi_analysis(self, event_collection, analyses, timeframe=None, interval=None, timezone=None, filters=None, group_by=None, max_age=None): diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index dec1896..cedc097 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -14,16 +14,16 @@ __author__ = 'dkador' -class MockedRequest(object): +class MockedResponse(object): def __init__(self, status_code, json_response): self.status_code = status_code self.json_response = json_response def json(self): - return {"result": self.json_response} + return self.json_response -class MockedFailedRequest(MockedRequest): +class MockedFailedResponse(MockedResponse): def json(self): return self.json_response @@ -31,10 +31,9 @@ def json(self): @patch("requests.Session.post") class ClientTests(BaseTestCase): - SINGLE_ADD_RESPONSE = MockedRequest(status_code=201, json_response={"hello": "goodbye"}) - - MULTI_ADD_RESPONSE = MockedRequest(status_code=200, json_response={"hello": "goodbye"}) + SINGLE_ADD_RESPONSE = MockedResponse(status_code=201, json_response={"result": {"hello": "goodbye"}}) + MULTI_ADD_RESPONSE = MockedResponse(status_code=200, json_response={"result": {"hello": "goodbye"}}) def setUp(self): super(ClientTests, self).setUp() @@ -125,7 +124,7 @@ def test_post_timeout_batch(self, post): self.assert_raises(requests.Timeout, keen.add_events, {"python_test": [{"hello": "goodbye"}]}) def test_environment_variables(self, post): - post.return_value = MockedFailedRequest( + post.return_value = MockedFailedResponse( status_code=401, # "message" is the description, "error_code" is the name of the class. json_response={"message": "authorization error", "error_code": "AdminOnlyEndpointError"}, @@ -185,7 +184,7 @@ def test_set_keys_using_env_var(self, post): exp_write_key = os.environ["KEEN_WRITE_KEY"] = "yyyy8901" exp_read_key = os.environ["KEEN_READ_KEY"] = "zzzz2345" exp_master_key = os.environ["KEEN_MASTER_KEY"] = "abcd1234" - + keen._initialize_client_from_environment() # test values @@ -209,7 +208,7 @@ def test_set_keys_using_package_var(self, post): exp_write_key = keen.write_key = "vvvv8901" exp_read_key = keen.read_key = "wwwww2345" exp_master_key = keen.master_key = "abcd4567" - + keen._initialize_client_from_environment() # test values @@ -230,7 +229,7 @@ def test_configure_through_code(self, post): # force client to reinitialize client = KeenClient(project_id="123456", read_key=None, write_key="abcdef") with patch("requests.Session.post") as post: - post.return_value = MockedFailedRequest( + post.return_value = MockedFailedResponse( status_code=401, json_response={"message": "authorization error", "error_code": "AdminOnlyEndpointError"}, ) @@ -291,10 +290,10 @@ def url_escape(self, url): @patch("requests.Session.get") class QueryTests(BaseTestCase): - INT_RESPONSE = MockedRequest(status_code=200, json_response=2) + INT_RESPONSE = MockedResponse(status_code=200, json_response={"result": 2}) - LIST_RESPONSE = MockedRequest( - status_code=200, json_response=[{"value": {"total": 1}}, {"value": {"total": 2}}]) + LIST_RESPONSE = MockedResponse( + status_code=200, json_response={"result": [{"value": {"total": 1}}, {"value": {"total": 2}}]}) def setUp(self): super(QueryTests, self).setUp() @@ -380,19 +379,34 @@ def test_multi_analysis(self, get): def test_funnel(self, get): get.return_value = self.LIST_RESPONSE + step1 = { - "event_collection": "query test", - "actor_property": "number", + "event_collection": "signed up", + "actor_property": "visitor.guid", "timeframe": "today" } step2 = { - "event_collection": "step2", - "actor_property": "number", + "event_collection": "completed profile", + "actor_property": "user.guid", "timeframe": "today" } + resp = keen.funnel([step1, step2]) self.assertEqual(type(resp), list) + def test_funnel_return_all_keys(self, get): + get.return_value = MockedResponse(status_code=200, json_response={ + "result": [], + "actors": [], + "random_key": [], + "steps": [] + }) + + resp = keen.funnel([1, 2], all_keys=True) + self.assertIsInstance(resp, dict) + self.assertIn("actors", resp) + self.assertIn("random_key", resp) + def test_group_by(self, get): get.return_value = self.LIST_RESPONSE resp = keen.count("query test", timeframe="today", group_by="number") @@ -471,7 +485,7 @@ def setUp(self): api_key = unicode("2e79c6ec1d0145be8891bf668599c79a") keen.write_key = unicode(api_key) - @patch("requests.Session.post", MagicMock(return_value=MockedRequest(status_code=201, json_response=[0, 1, 2]))) + @patch("requests.Session.post", MagicMock(return_value=MockedResponse(status_code=201, json_response=[0, 1, 2]))) def test_add_event_with_unicode(self): keen.add_event(unicode("unicode test"), {unicode("number"): 5, "string": unicode("foo")}) From b6490be04c3c28a5daf14ac8d183db3e0cc3dba9 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Sat, 27 Jun 2015 16:12:51 -0700 Subject: [PATCH 074/224] Updated test so it works with python 2.6. --- keen/tests/client_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index cedc097..1e8299e 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -403,7 +403,7 @@ def test_funnel_return_all_keys(self, get): }) resp = keen.funnel([1, 2], all_keys=True) - self.assertIsInstance(resp, dict) + self.assertEquals(type(resp), dict) self.assertIn("actors", resp) self.assertIn("random_key", resp) From 595271ecf4dabd8727f31c38951096155b506130 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Sat, 27 Jun 2015 16:18:59 -0700 Subject: [PATCH 075/224] Update tests again for python 2.6 compatibility. --- keen/tests/client_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 1e8299e..d828950 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -404,8 +404,8 @@ def test_funnel_return_all_keys(self, get): resp = keen.funnel([1, 2], all_keys=True) self.assertEquals(type(resp), dict) - self.assertIn("actors", resp) - self.assertIn("random_key", resp) + self.assertTrue("actors" in resp) + self.assertTrue("random_key" in resp) def test_group_by(self, get): get.return_value = self.LIST_RESPONSE From 5c4bf36830d5efd210fb1de1c45e3b9de118c7cb Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 29 Jun 2015 16:06:28 -0700 Subject: [PATCH 076/224] Updated Readme and bumped the version. --- README.md | 14 +++++++++++--- setup.py | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d734a94..e8fbdf2 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,13 @@ Or if using unique client instances: ##### Send Batch Events to Keen IO You can upload Events in a batch, like so: - + ```python # uploads 4 events total - 2 to the "sign_ups" collection and 2 to the "purchases" collection keen.add_events({ "sign_ups": [ { "username": "nameuser1" }, - { "username": "nameuser2" } + { "username": "nameuser2" } ], "purchases": [ { "price": 5 }, @@ -113,6 +113,10 @@ Here are some examples of querying. Let's assume you've added some events to th keen.funnel([step1, step2], timeframe="today") # => [2039, 201] ``` +To return the full API response (as opposed to the singular "result" key), set `all_keys=True`. + +For example, `keen.funnel([step1, step2], all_keys=True)` would return "result", "actors" and "steps" keys. + ##### Delete Events The Keen IO API allows you to [delete events](https://keen.io/docs/api/#delete-events) from event collections, optionally supplying filters, timeframe or timezone to narrow the scope of what you would like to delete. @@ -198,8 +202,12 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.16 ++ Added `all_keys` parameter which allows users to expose all keys in query response. ++ Added `delete_events` method. + ##### 0.3.15 -+ Added better error handling to surface all errors from HTTP API calls ++ Added better error handling to surface all errors from HTTP API calls. ##### 0.3.14 + Added compatibility for pip 1.0 diff --git a/setup.py b/setup.py index 86c3372..19f2ff6 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.15", + version="0.3.16", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From 80fc5b5a05cc26ecd965bc13541a7b4aa00159df Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Tue, 30 Jun 2015 10:03:14 -0700 Subject: [PATCH 077/224] Fixed test. --- keen/tests/client_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index d828950..b73843d 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -466,7 +466,7 @@ def tearDown(self): super(DeleteTests, self).tearDown() def test_delete_events(self, delete): - delete.return_value = MockedRequest(status_code=204, json_response=[]) + delete.return_value = MockedResponse(status_code=204, json_response=[]) # Assert that the mocked delete function is called the way we expect. keen.delete_events("foo", filters=[{"property_name": 'username', "operator": 'eq', "property_value": 'Bob'}]) # Check that the URL is generated correctly. From 742bcb5192b4606c3dd4c791ae97c524f1d403cc Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Thu, 2 Jul 2015 07:30:23 -0700 Subject: [PATCH 078/224] Adds query changes from original repository --- keen/api.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/keen/api.py b/keen/api.py index 705e5b2..22c3b94 100644 --- a/keen/api.py +++ b/keen/api.py @@ -140,7 +140,6 @@ def post_events(self, events): response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self.error_handling(response) -<<<<<<< HEAD def delete_collection(self, collection): """ Deletes a single collection in the Keen IO API. The master key must be set first. @@ -162,10 +161,7 @@ def delete_collection(self, collection): response = self.fulfill(HTTPMethods.DELETE, url, data=payload, headers=headers, timeout=self.post_timeout) self.error_handling(response) - def query(self, analysis_type, params): -======= def query(self, analysis_type, params, all_keys=False): ->>>>>>> upststream/master """ Performs a query using the Keen IO analysis API. A read key must be set first. From ef0d0244b3e3393afd819b6d254f1c9562778215 Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Fri, 3 Jul 2015 15:39:10 -0700 Subject: [PATCH 079/224] Makes changes recommended in pull request comments --- keen/api.py | 12 ++++++------ keen/client.py | 38 +------------------------------------- 2 files changed, 7 insertions(+), 43 deletions(-) diff --git a/keen/api.py b/keen/api.py index 22c3b94..0eaba73 100644 --- a/keen/api.py +++ b/keen/api.py @@ -142,9 +142,9 @@ def post_events(self, events): def delete_collection(self, collection): """ - Deletes a single collection in the Keen IO API. The master key must be set first. + Delete a collection in the Keen IO API. The master key must be set first. - :param event: a Collection to delete + :param colelction: a Collection to delete """ if not self.master_key: raise exceptions.InvalidEnvironmentError( @@ -211,11 +211,11 @@ def delete_events(self, event_collection, params): self.error_handling(response) return True - def get_collection(self, collection_name): + def get_collection(self, event_collection): """ Extracts info about a collection using the Keen IO API. A master key must be set first. - :param event_name: the name of the collection to retrieve info for + :param event_collection: the name of the collection to retrieve info for """ if not self.master_key: raise exceptions.InvalidEnvironmentError( @@ -224,7 +224,7 @@ def get_collection(self, collection_name): "KeenApi object." ) url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, - self.project_id, collection_name) + self.project_id, event_collection) headers = {"Authorization": self.master_key} response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) self.error_handling(response) @@ -233,7 +233,7 @@ def get_collection(self, collection_name): def get_all_collections(self): """ - Extracts info about all collections using the Keen IO API. A master key must be set first. + Return schema information for all the event collections in a given project. A master key must be set first. """ if not self.master_key: diff --git a/keen/client.py b/keen/client.py index 0ea5266..df7701c 100644 --- a/keen/client.py +++ b/keen/client.py @@ -469,7 +469,7 @@ def extraction(self, event_collection, timeframe=None, timezone=None, filters=No def funnel(self, steps, timeframe=None, timezone=None, max_age=None, all_keys=False): """ Performs a Funnel query - Returns an object containing the results for each step of the funnel. + Returns an object contaExtracts info about all collectionsining the results for each step of the funnel. :param steps: array of dictionaries, one for each step. example: [{"event_collection":"signup","actor_property":"user.id"}, @@ -492,42 +492,6 @@ def funnel(self, steps, timeframe=None, timezone=None, max_age=None, all_keys=Fa return self.api.query("funnel", params, all_keys=all_keys) - def get_collection(self, collection_name): - """ - - Returns a description of a collection - - :param event_name: the name of the event to get the collection info from - """ - - return self.api.get_collection(collection_name) - - def get_collections(self): - """ - - Returns a description of all collections - """ - - return self.api.get_all_collections() - - def delete_collection(self, collection): - """ Performs a deletion of a collection - - - :param collection_name: the name of the collection - """ - - self.api.delete_collection(collection) - - def delete_collections(self, collections): - """ Performs a deletion of multiple collections in the list - - :param collections: a list of collections to be deleted - """ - - for collection in collections: - self.api.delete_collection(collection) - def multi_analysis(self, event_collection, analyses, timeframe=None, interval=None, timezone=None, filters=None, group_by=None, max_age=None): """ Performs a multi-analysis query From 29427f4aad9259a40d27db3d471298e1d5385bc5 Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Tue, 7 Jul 2015 08:53:47 -0700 Subject: [PATCH 080/224] Adds test for deletion of collection. WIP --- keen/tests/client_tests.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index b73843d..3fdd41f 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -7,7 +7,7 @@ from keen import exceptions, persistence_strategies, scoped_keys import keen from keen.client import KeenClient -from keen.tests.base_test_case import BaseTestCase +from base_test_case import BaseTestCase from mock import patch, MagicMock import sys @@ -474,6 +474,12 @@ def test_delete_events(self, delete): # Check that the master_key is in the Authorization header. self.assertTrue(keen.master_key in delete.call_args[1]["headers"]["Authorization"]) + def test_delete_collection(self, delete): + delete.return_value = MockedResponse(status_code=204, json_response=[]) + keen.delete_collection("foo") + self.assertEqual("https://api.keen.io/3.0/projects/" + keen.project_id + "/events/foo", delete.call_ars[0][0]) + self.assertTrue(keen.master_key in delete.call_args[1]["headers"]["Authorization"]) + # only need to test unicode separately in python2 if sys.version_info[0] < 3: @@ -496,3 +502,6 @@ def tearDown(self): keen.master_key = None keen._client = None super(UnicodeTests, self).tearDown() + +if __name__ == '__main__': + From c75325273c4fd21ddc519e1c1c48f140f72e3ef1 Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Tue, 7 Jul 2015 17:18:55 -0700 Subject: [PATCH 081/224] Small fixes --- keen/tests/client_tests.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 3fdd41f..ef60dce 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -502,6 +502,3 @@ def tearDown(self): keen.master_key = None keen._client = None super(UnicodeTests, self).tearDown() - -if __name__ == '__main__': - From 689e35043e54aeaffbbb939105cbc02617c00a5f Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Tue, 7 Jul 2015 17:22:05 -0700 Subject: [PATCH 082/224] Removes unnecessary unit test, small spelling fixes --- keen/api.py | 2 +- keen/client.py | 2 +- keen/tests/client_tests.py | 8 +------- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/keen/api.py b/keen/api.py index 0eaba73..d9b18f6 100644 --- a/keen/api.py +++ b/keen/api.py @@ -144,7 +144,7 @@ def delete_collection(self, collection): """ Delete a collection in the Keen IO API. The master key must be set first. - :param colelction: a Collection to delete + :param collection: a Collection to delete """ if not self.master_key: raise exceptions.InvalidEnvironmentError( diff --git a/keen/client.py b/keen/client.py index df7701c..ed37895 100644 --- a/keen/client.py +++ b/keen/client.py @@ -469,7 +469,7 @@ def extraction(self, event_collection, timeframe=None, timezone=None, filters=No def funnel(self, steps, timeframe=None, timezone=None, max_age=None, all_keys=False): """ Performs a Funnel query - Returns an object contaExtracts info about all collectionsining the results for each step of the funnel. + Returns an object containing the results for each step of the funnel. :param steps: array of dictionaries, one for each step. example: [{"event_collection":"signup","actor_property":"user.id"}, diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index ef60dce..b73843d 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -7,7 +7,7 @@ from keen import exceptions, persistence_strategies, scoped_keys import keen from keen.client import KeenClient -from base_test_case import BaseTestCase +from keen.tests.base_test_case import BaseTestCase from mock import patch, MagicMock import sys @@ -474,12 +474,6 @@ def test_delete_events(self, delete): # Check that the master_key is in the Authorization header. self.assertTrue(keen.master_key in delete.call_args[1]["headers"]["Authorization"]) - def test_delete_collection(self, delete): - delete.return_value = MockedResponse(status_code=204, json_response=[]) - keen.delete_collection("foo") - self.assertEqual("https://api.keen.io/3.0/projects/" + keen.project_id + "/events/foo", delete.call_ars[0][0]) - self.assertTrue(keen.master_key in delete.call_args[1]["headers"]["Authorization"]) - # only need to test unicode separately in python2 if sys.version_info[0] < 3: From 5e717d417f7fd3bb976283ad5ca2a330f505f5a2 Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Mon, 20 Jul 2015 10:50:18 -0700 Subject: [PATCH 083/224] Removes deletion and removes the function to get all collections. Adds unit test for get_collection --- keen/__init__.py | 9 ++++++++ keen/api.py | 42 +------------------------------------- keen/client.py | 9 ++++++++ keen/tests/client_tests.py | 29 +++++++++++++++++++++----- 4 files changed, 43 insertions(+), 46 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index e35530e..bf58721 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -421,3 +421,12 @@ def delete_events(*args, **kwargs): """ _initialize_client_from_environment() return _client.delete_events(*args, **kwargs) + +def get_collection(*args, **kwargs): + """ Returns event collection schema + + :param event_collection: string, the event collection from which schema is to be returned, + if left blank will return schema for all collections + """ + _initialize_client_from_environment() + return _client.get_collection(*args, **kwargs) diff --git a/keen/api.py b/keen/api.py index 0eaba73..83e7a72 100644 --- a/keen/api.py +++ b/keen/api.py @@ -140,27 +140,6 @@ def post_events(self, events): response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self.error_handling(response) - def delete_collection(self, collection): - """ - Delete a collection in the Keen IO API. The master key must be set first. - - :param colelction: a Collection to delete - """ - if not self.master_key: - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a master key to delete events. " - "Please set a 'master_key' when initializing the " - "KeenApi object." - ) - - url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, - self.project_id, - collection['name']) - headers = {"Content-Type": "application/json", "Authorization": self.master_key} - payload = json.dumps(collection) - response = self.fulfill(HTTPMethods.DELETE, url, data=payload, headers=headers, timeout=self.post_timeout) - self.error_handling(response) - def query(self, analysis_type, params, all_keys=False): """ Performs a query using the Keen IO analysis API. A read key must be set first. @@ -219,7 +198,7 @@ def get_collection(self, event_collection): """ if not self.master_key: raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a master key to get events. " + "The Keen IO API requires a master key to get event collection schema. " "Please set a 'master_key' when initializing the " "KeenApi object." ) @@ -231,25 +210,6 @@ def get_collection(self, event_collection): return response.json() - def get_all_collections(self): - """ - Return schema information for all the event collections in a given project. A master key must be set first. - - """ - if not self.master_key: - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a master key to get events. " - "Please set a 'master_key' when initializing the " - "KeenApi object." - ) - url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, - self.project_id) - headers = {"Authorization": self.master_key} - response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) - self.error_handling(response) - - return response.json() - def error_handling(self, res): """ Helper function to do the error handling diff --git a/keen/client.py b/keen/client.py index df7701c..5480f72 100644 --- a/keen/client.py +++ b/keen/client.py @@ -170,6 +170,15 @@ def delete_events(self, event_collection, timeframe=None, timezone=None, filters params = self.get_params(timeframe=timeframe, timezone=timezone, filters=filters) return self.api.delete_events(event_collection, params) + def get_collection(self, event_collection): + """ Returns event collection schema + + :param event_collection: string, the event collection from which schema is to be returned, + if left blank will return schema for all collections + """ + + return self.api.get_collection(event_collection) + def _base64_encode(self, string_to_encode): """ Base64 encodes a string, with either Python 2 or 3. diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index ef60dce..05de0f3 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -474,11 +474,30 @@ def test_delete_events(self, delete): # Check that the master_key is in the Authorization header. self.assertTrue(keen.master_key in delete.call_args[1]["headers"]["Authorization"]) - def test_delete_collection(self, delete): - delete.return_value = MockedResponse(status_code=204, json_response=[]) - keen.delete_collection("foo") - self.assertEqual("https://api.keen.io/3.0/projects/" + keen.project_id + "/events/foo", delete.call_ars[0][0]) - self.assertTrue(keen.master_key in delete.call_args[1]["headers"]["Authorization"]) + +@patch("requests.Session.get") +class GetTests(BaseTestCase): + + def setUp(self): + super(GetTests, self).setUp() + keen._client = None + keen.project_id = "1k4jb23kjbkjkjsd" + keen.master_key = "sdofnasofagaergub" + + def tearDown(self): + keen._client = None + keen.project_id = None + keen.master_key = None + super(GetTests, self).tearDown() + + def test_get_collection(self, get): + get.return_value = MockedResponse(status_code=204, json_response=[]) + # Assert that the mocked get function is called the way we expect. + keen.get_collection("foo") + # Check that the URL is generated correctly + self.assertEqual("https://api.keen.io/3.0/projects/1k4jb23kjbkjkjsd/events/foo", get.call_args[0][0]) + # Check that the master_key is in the Authorization header + self.assertTrue(keen.master_key in get.call_args[1]["headers"]["Authorization"]) # only need to test unicode separately in python2 if sys.version_info[0] < 3: From 97cbdd316f3ca76a67c641c88fd462f13afa05b4 Mon Sep 17 00:00:00 2001 From: Gunnar Holwerda Date: Tue, 21 Jul 2015 08:17:53 -0700 Subject: [PATCH 084/224] Adds back get_all_collections and a unit test for it --- keen/__init__.py | 7 +++++++ keen/api.py | 19 +++++++++++++++++++ keen/client.py | 7 +++++++ keen/tests/client_tests.py | 16 +++++++++++++++- 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/keen/__init__.py b/keen/__init__.py index bf58721..d152d4c 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -430,3 +430,10 @@ def get_collection(*args, **kwargs): """ _initialize_client_from_environment() return _client.get_collection(*args, **kwargs) + +def get_all_collections(): + """ Returns event collection schema for all events + + """ + _initialize_client_from_environment() + return _client.get_all_collections() diff --git a/keen/api.py b/keen/api.py index 83e7a72..e31db8f 100644 --- a/keen/api.py +++ b/keen/api.py @@ -210,6 +210,25 @@ def get_collection(self, event_collection): return response.json() + def get_all_collections(self): + """ + Extracts schema for all collections using the Keen IO API. A master key must be set first. + + """ + if not self.master_key: + raise exceptions.InvalidEnvironmentError( + "The Keen IO API requires a master key to get event collection schema. " + "Please set a 'master_key' when initializing the " + "KeenApi object." + ) + url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, + self.project_id) + headers = {"Authorization": self.master_key} + response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) + self.error_handling(response) + + return response.json() + def error_handling(self, res): """ Helper function to do the error handling diff --git a/keen/client.py b/keen/client.py index 5480f72..9e4b53f 100644 --- a/keen/client.py +++ b/keen/client.py @@ -179,6 +179,13 @@ def get_collection(self, event_collection): return self.api.get_collection(event_collection) + def get_all_collections(self): + """ Returns event collection schema for all events + + """ + + return self.api.get_all_collections() + def _base64_encode(self, string_to_encode): """ Base64 encodes a string, with either Python 2 or 3. diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 05de0f3..01990f4 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -478,6 +478,11 @@ def test_delete_events(self, delete): @patch("requests.Session.get") class GetTests(BaseTestCase): + SINGLE_ADD_RESPONSE = MockedResponse(status_code=201, json_response={"result": {"hello": "goodbye"}}) + + LIST_RESPONSE = MockedResponse( + status_code=200, json_response={"result": [{"value": {"total": 1}}, {"value": {"total": 2}}]}) + def setUp(self): super(GetTests, self).setUp() keen._client = None @@ -491,7 +496,7 @@ def tearDown(self): super(GetTests, self).tearDown() def test_get_collection(self, get): - get.return_value = MockedResponse(status_code=204, json_response=[]) + get.return_value = self.SINGLE_ADD_RESPONSE # Assert that the mocked get function is called the way we expect. keen.get_collection("foo") # Check that the URL is generated correctly @@ -499,6 +504,15 @@ def test_get_collection(self, get): # Check that the master_key is in the Authorization header self.assertTrue(keen.master_key in get.call_args[1]["headers"]["Authorization"]) + def test_get_all_collections(self, get): + get.return_value = self.LIST_RESPONSE + # Assert that the mocked get function is called the way we expect + keen.get_all_collections() + # Check that the URL is generated correctly + self.assertEqual("https://api.keen.io/3.0/projects/1k4jb23kjbkjkjsd/events", get.call_args[0][0]) + # Check that the master_key in the Authorization header + self.assertTrue(keen.master_key in get.call_args[1]["headers"]["Authorization"]) + # only need to test unicode separately in python2 if sys.version_info[0] < 3: From 6ec4a99afac67c59d99bb08d6120892dd10fa918 Mon Sep 17 00:00:00 2001 From: Dustin Larimer Date: Wed, 5 Aug 2015 11:07:30 -0700 Subject: [PATCH 085/224] Add timeframes to query examples in README.md --- README.md | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e8fbdf2..4ea3e55 100644 --- a/README.md +++ b/README.md @@ -87,20 +87,31 @@ That's it! After running your code, check your Keen IO Project to see the event/ Here are some examples of querying. Let's assume you've added some events to the "purchases" collection. ```python - keen.count("purchases") # => 100 - keen.sum("purchases", target_property="price") # => 10000 - keen.minimum("purchases", target_property="price") # => 20 - keen.maximum("purchases", target_property="price") # => 100 - keen.average("purchases", target_property="price") # => 49.2 + keen.count("purchases", timeframe="this_14_days") # => 100 + keen.sum("purchases", target_property="price", timeframe="this_14_days") # => 10000 + keen.minimum("purchases", target_property="price", timeframe="this_14_days") # => 20 + keen.maximum("purchases", target_property="price", timeframe="this_14_days") # => 100 + keen.average("purchases", target_property="price", timeframe="this_14_days") # => 49.2 - keen.sum("purchases", target_property="price", group_by="item.id") # => [{ "item.id": 123, "result": 240 }, { ... }] + keen.sum("purchases", target_property="price", group_by="item.id", timeframe="this_14_days") # => [{ "item.id": 123, "result": 240 }, { ... }] - keen.count_unique("purchases", target_property="user.id") # => 3 - keen.select_unique("purchases", target_property="user.email") # => ["bob@aol.com", "joe@yahoo.biz"] + keen.count_unique("purchases", target_property="user.id", timeframe="this_14_days") # => 3 + keen.select_unique("purchases", target_property="user.email", timeframe="this_14_days") # => ["bob@aol.com", "joe@yahoo.biz"] keen.extraction("purchases", timeframe="today") # => [{ "price" => 20, ... }, { ... }] - keen.multi_analysis("purchases", analyses={"total":{"analysis_type":"sum", "target_property":"price"}, "average":{"analysis_type":"average", "target_property":"price"}) # => {"total":10329.03, "average":933.93} + keen.multi_analysis("purchases", analyses={ + "total":{ + "analysis_type": "sum", + "target_property":"price", + "timeframe": "this_14_days" + }, + "average":{ + "analysis_type": "average", + "target_property":"price", + "timeframe": "this_14_days" + } + ) # => {"total":10329.03, "average":933.93} step1 = { "event_collection": "signup", From 0371c14d840b95ea335e05a05bab6b1b34d2adea Mon Sep 17 00:00:00 2001 From: chethan2k5 Date: Wed, 5 Aug 2015 17:21:54 -0700 Subject: [PATCH 086/224] fixed timestamp overriding keen addons --- keen/client.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/keen/client.py b/keen/client.py index ed37895..f884ad9 100644 --- a/keen/client.py +++ b/keen/client.py @@ -38,6 +38,10 @@ def to_json(self): event_as_dict = copy.deepcopy(self.event_body) if self.timestamp: event_as_dict["keen"] = {"timestamp": self.timestamp.isoformat()} + if "keen" in event_as_dict: + event_as_dict["keen"]["timestamp"] = self.timestamp.isoformat() + else: + event_as_dict["keen"] = {"timestamp": self.timestamp.isoformat()} return json.dumps(event_as_dict) From 508fcf2e7537c08ea876cb6a0f3c029570313af3 Mon Sep 17 00:00:00 2001 From: chethan2k5 Date: Thu, 6 Aug 2015 10:31:32 -0700 Subject: [PATCH 087/224] updated based on review comments and tests --- keen/client.py | 1 - keen/tests/client_tests.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/keen/client.py b/keen/client.py index f884ad9..791aaa2 100644 --- a/keen/client.py +++ b/keen/client.py @@ -37,7 +37,6 @@ def to_json(self): """ event_as_dict = copy.deepcopy(self.event_body) if self.timestamp: - event_as_dict["keen"] = {"timestamp": self.timestamp.isoformat()} if "keen" in event_as_dict: event_as_dict["keen"]["timestamp"] = self.timestamp.isoformat() else: diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index b73843d..8e318c2 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -286,6 +286,21 @@ def url_escape(self, url): import urllib.parse return urllib.parse.quote(url) +@patch("requests.Session.post") +class EventTests(BaseTestCase): + + def test_custom_addon(self): + event = Event( + '', + '', + {'keen': {'addons': {'asdf': 1}}}, + timestamp=datetime.datetime.now(), + ) + as_json = json.loads(event.to_json()) + + self.assertEqual(as_json['keen']['addons']['asdf'], 1) + self.assertTrue('timestamp' in as_json['keen']) + @patch("requests.Session.get") class QueryTests(BaseTestCase): From 85c3138572e741c6f05e736508c8b0e606645508 Mon Sep 17 00:00:00 2001 From: chethan2k5 Date: Mon, 10 Aug 2015 15:41:37 -0700 Subject: [PATCH 088/224] updated test for fixing EventTests --- keen/tests/client_tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 8e318c2..709ecd8 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -286,7 +286,6 @@ def url_escape(self, url): import urllib.parse return urllib.parse.quote(url) -@patch("requests.Session.post") class EventTests(BaseTestCase): def test_custom_addon(self): From a1c3c41b2dafa4001d21bf0fd62e4a91d8dc9608 Mon Sep 17 00:00:00 2001 From: chethan2k5 Date: Wed, 19 Aug 2015 11:34:53 -0700 Subject: [PATCH 089/224] Updated with Client import in tests --- keen/tests/client_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 709ecd8..e14445c 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -6,7 +6,7 @@ import requests from keen import exceptions, persistence_strategies, scoped_keys import keen -from keen.client import KeenClient +from keen.client import KeenClient, Event from keen.tests.base_test_case import BaseTestCase from mock import patch, MagicMock import sys From 425f5b0dcdda3b8c73d09b093008a824c60d0632 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Thu, 20 Aug 2015 10:23:50 -0700 Subject: [PATCH 090/224] Bumped the version to 0.3.17. --- README.md | 14 ++++++++++---- setup.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4ea3e55..addfa0b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,9 @@ Use pip to install! pip install keen -This client is known to work on Python 2.6, 2.7, 3.2 and 3.3 +This client is known to work on Python 2.6, 2.7, 3.2 and 3.3. + +For versions of Python < 2.7.9, you’ll need to install pyasn1, ndg-httpsclient, pyOpenSSL. ### Usage @@ -102,10 +104,10 @@ Here are some examples of querying. Let's assume you've added some events to th keen.multi_analysis("purchases", analyses={ "total":{ - "analysis_type": "sum", - "target_property":"price", + "analysis_type": "sum", + "target_property":"price", "timeframe": "this_14_days" - }, + }, "average":{ "analysis_type": "average", "target_property":"price", @@ -213,6 +215,10 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.17 +- fixed timestamp overriding keen addons +- add `get_collection` and `get_all_collections` methods + ##### 0.3.16 + Added `all_keys` parameter which allows users to expose all keys in query response. + Added `delete_events` method. diff --git a/setup.py b/setup.py index 19f2ff6..8d865f6 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.16", + version="0.3.17", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From ebe8bb19667d8298d8664b3c40e083af464fa10f Mon Sep 17 00:00:00 2001 From: Cass Malloy Date: Fri, 9 Oct 2015 10:32:25 -0700 Subject: [PATCH 091/224] Except ValueError instead of JSONDecodeError --- keen/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/api.py b/keen/api.py index e31db8f..799466c 100644 --- a/keen/api.py +++ b/keen/api.py @@ -239,7 +239,7 @@ def error_handling(self, res): if res.status_code // 100 != 2: try: error = res.json() - except json.JSONDecodeError: + except ValueError: error = { 'message': 'The API did not respond with JSON, but: "{0}"'.format(res.text[:1000]), "error_code": "InvalidResponseFormat" From c42b074f51445ae0ed27f7ecada6f2f742b868b4 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Wed, 14 Oct 2015 13:55:42 -0700 Subject: [PATCH 092/224] Cleaned up doc strings and a few formatting nits. --- keen/__init__.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index d152d4c..2a45ed9 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -85,7 +85,7 @@ def count(event_collection, timeframe=None, timezone=None, interval=None, filter :param interval: string, the time interval used for measuring data over time example: "daily" :param filters: array of dict, contains the filters you'd like to apply to the data - example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're @@ -112,7 +112,7 @@ def sum(event_collection, target_property, timeframe=None, timezone=None, interv :param interval: string, the time interval used for measuring data over time example: "daily" :param filters: array of dict, contains the filters you'd like to apply to the data - example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're @@ -140,7 +140,7 @@ def minimum(event_collection, target_property, timeframe=None, timezone=None, in :param interval: string, the time interval used for measuring data over time example: "daily" :param filters: array of dict, contains the filters you'd like to apply to the data - example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're @@ -168,7 +168,7 @@ def maximum(event_collection, target_property, timeframe=None, timezone=None, in :param interval: string, the time interval used for measuring data over time example: "daily" :param filters: array of dict, contains the filters you'd like to apply to the data - example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're @@ -196,7 +196,7 @@ def average(event_collection, target_property, timeframe=None, timezone=None, in :param interval: string, the time interval used for measuring data over time example: "daily" :param filters: array of dict, contains the filters you'd like to apply to the data - example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're @@ -210,7 +210,7 @@ def average(event_collection, target_property, timeframe=None, timezone=None, in def median(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, max_age=None): + group_by=None, max_age=None): """ Performs a median query Finds the median of a target property for events that meet the given criteria. @@ -224,7 +224,7 @@ def median(event_collection, target_property, timeframe=None, timezone=None, int :param interval: string, the time interval used for measuring data over time example: "daily" :param filters: array of dict, contains the filters you'd like to apply to the data - example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're @@ -233,8 +233,8 @@ def median(event_collection, target_property, timeframe=None, timezone=None, int """ _initialize_client_from_environment() return _client.median(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, - target_property=target_property, max_age=max_age) + interval=interval, filters=filters, group_by=group_by, + target_property=target_property, max_age=max_age) def percentile(event_collection, target_property, percentile, timeframe=None, timezone=None, interval=None, @@ -254,7 +254,7 @@ def percentile(event_collection, target_property, percentile, timeframe=None, ti :param interval: string, the time interval used for measuring data over time example: "daily" :param filters: array of dict, contains the filters you'd like to apply to the data - example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're @@ -290,7 +290,7 @@ def count_unique(event_collection, target_property, timeframe=None, timezone=Non :param interval: string, the time interval used for measuring data over time example: "daily" :param filters: array of dict, contains the filters you'd like to apply to the data - example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're @@ -318,7 +318,7 @@ def select_unique(event_collection, target_property, timeframe=None, timezone=No :param interval: string, the time interval used for measuring data over time example: "daily" :param filters: array of dict, contains the filters you'd like to apply to the data - example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're @@ -344,7 +344,7 @@ def extraction(event_collection, timeframe=None, timezone=None, filters=None, la :param timezone: int, the timezone you'd like to use for the timeframe and interval in seconds :param filters: array of dict, contains the filters you'd like to apply to the data - example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param latest: int, the number of most recent records you'd like to return :param email: string, optional string containing an email address to email results to :param property_names: string or list of strings, used to limit the properties returned @@ -392,7 +392,7 @@ def multi_analysis(event_collection, analyses, timeframe=None, interval=None, :param timezone: int, the timezone you'd like to use for the timeframe and interval in seconds :param filters: array of dict, contains the filters you'd like to apply to the data - example: {["property_name":"device", "operator":"eq", "property_value":"iPhone"}] + example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're @@ -422,6 +422,7 @@ def delete_events(*args, **kwargs): _initialize_client_from_environment() return _client.delete_events(*args, **kwargs) + def get_collection(*args, **kwargs): """ Returns event collection schema @@ -431,6 +432,7 @@ def get_collection(*args, **kwargs): _initialize_client_from_environment() return _client.get_collection(*args, **kwargs) + def get_all_collections(): """ Returns event collection schema for all events From 4959eeb5e9b01202b833910491f632d661c56496 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Wed, 14 Oct 2015 14:37:04 -0700 Subject: [PATCH 093/224] Bumped the version to 0.3.18. --- README.md | 7 +++++-- setup.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index addfa0b..7ae08e5 100644 --- a/README.md +++ b/README.md @@ -215,9 +215,12 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.18 ++ Updated error handling to except `ValueError` + ##### 0.3.17 -- fixed timestamp overriding keen addons -- add `get_collection` and `get_all_collections` methods ++ Fixed timestamp overriding keen addons ++ Added `get_collection` and `get_all_collections` methods ##### 0.3.16 + Added `all_keys` parameter which allows users to expose all keys in query response. diff --git a/setup.py b/setup.py index 8d865f6..8a3ca79 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.17", + version="0.3.18", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From a0b8e5726988009dd2b01ff69b3b0fd10ac9ad67 Mon Sep 17 00:00:00 2001 From: Dennis Yu Date: Thu, 19 Nov 2015 17:13:20 -0800 Subject: [PATCH 094/224] add base_url as possible env varaible --- keen/__init__.py | 7 +++++-- keen/client.py | 4 ++-- keen/tests/client_tests.py | 16 +++++++++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index 2a45ed9..35cba07 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -9,11 +9,12 @@ write_key = None read_key = None master_key = None +base_url = None def _initialize_client_from_environment(): ''' Initialize a KeenCLient instance using environment variables. ''' - global _client, project_id, write_key, read_key, master_key + global _client, project_id, write_key, read_key, master_key, base_url if _client is None: # check environment for project ID and keys @@ -21,6 +22,7 @@ def _initialize_client_from_environment(): write_key = write_key or os.environ.get("KEEN_WRITE_KEY") read_key = read_key or os.environ.get("KEEN_READ_KEY") master_key = master_key or os.environ.get("KEEN_MASTER_KEY") + base_url = base_url or os.environ.get("KEEN_BASE_URL") if not project_id: raise InvalidEnvironmentError("Please set the KEEN_PROJECT_ID environment variable or set keen.project_id!") @@ -28,7 +30,8 @@ def _initialize_client_from_environment(): _client = KeenClient(project_id, write_key=write_key, read_key=read_key, - master_key=master_key) + master_key=master_key, + base_url=base_url) def add_event(event_collection, body, timestamp=None): diff --git a/keen/client.py b/keen/client.py index 2d5a506..d56e3f3 100644 --- a/keen/client.py +++ b/keen/client.py @@ -61,7 +61,7 @@ class KeenClient(object): def __init__(self, project_id, write_key=None, read_key=None, persistence_strategy=None, api_class=KeenApi, get_timeout=305, post_timeout=305, - master_key=None): + master_key=None, base_url=None): """ Initializes a KeenClient object. :param project_id: the Keen IO project ID @@ -82,7 +82,7 @@ def __init__(self, project_id, write_key=None, read_key=None, # into a default persistence strategy. self.api = api_class(project_id, write_key=write_key, read_key=read_key, get_timeout=get_timeout, post_timeout=post_timeout, - master_key=master_key) + master_key=master_key, base_url=base_url) if persistence_strategy: # validate the given persistence strategy diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index e690299..d7a059e 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -135,6 +135,7 @@ def test_environment_variables(self, post): keen.write_key = None keen.read_key = None keen.master_key = None + keen.base_url = None self.assert_raises(exceptions.InvalidEnvironmentError, keen.add_event, "python_test", {"hello": "goodbye"}) @@ -156,13 +157,15 @@ def test_new_client_instance(self, post): exp_write_key = "yyyy4567" exp_read_key = "zzzz8912" exp_master_key = "abcd3456" + exp_base_url = "keen.base.url" # create Client instance client = KeenClient( project_id=exp_project_id, write_key=exp_write_key, read_key=exp_read_key, - master_key=exp_master_key + master_key=exp_master_key, + base_url=exp_base_url ) # assert values @@ -170,6 +173,7 @@ def test_new_client_instance(self, post): self.assertEquals(exp_write_key, client.api.write_key) self.assertEquals(exp_read_key, client.api.read_key) self.assertEquals(exp_master_key, client.api.master_key) + self.assertEquals(exp_base_url, client.api.base_url) def test_set_keys_using_env_var(self, post): # reset Client settings @@ -178,12 +182,14 @@ def test_set_keys_using_env_var(self, post): keen.write_key = None keen.read_key = None keen.master_key = None + keen.base_url = None # set env vars exp_project_id = os.environ["KEEN_PROJECT_ID"] = "xxxx5678" exp_write_key = os.environ["KEEN_WRITE_KEY"] = "yyyy8901" exp_read_key = os.environ["KEEN_READ_KEY"] = "zzzz2345" exp_master_key = os.environ["KEEN_MASTER_KEY"] = "abcd1234" + exp_base_url = os.environ["KEEN_BASE_URL"] = "keen.base.url" keen._initialize_client_from_environment() @@ -192,22 +198,26 @@ def test_set_keys_using_env_var(self, post): self.assertEquals(exp_write_key, keen.write_key) self.assertEquals(exp_read_key, keen.read_key) self.assertEquals(exp_master_key, keen.master_key) + self.assertEquals(exp_base_url, keen.base_url) self.assertEquals(exp_project_id, keen._client.api.project_id) self.assertEquals(exp_write_key, keen._client.api.write_key) self.assertEquals(exp_read_key, keen._client.api.read_key) self.assertEquals(exp_master_key, keen._client.api.master_key) + self.assertEquals(exp_base_url, keen._client.api.base_url) # remove env vars del os.environ["KEEN_PROJECT_ID"] del os.environ["KEEN_WRITE_KEY"] del os.environ["KEEN_READ_KEY"] del os.environ["KEEN_MASTER_KEY"] + del os.environ["KEEN_BASE_URL"] def test_set_keys_using_package_var(self, post): exp_project_id = keen.project_id = "uuuu5678" exp_write_key = keen.write_key = "vvvv8901" exp_read_key = keen.read_key = "wwwww2345" exp_master_key = keen.master_key = "abcd4567" + exp_base_url = keen.base_url = "keen.base.url" keen._initialize_client_from_environment() @@ -220,6 +230,7 @@ def test_set_keys_using_package_var(self, post): self.assertEquals(exp_write_key, keen._client.api.write_key) self.assertEquals(exp_read_key, keen._client.api.read_key) self.assertEquals(exp_master_key, keen._client.api.master_key) + self.assertEquals(exp_base_url, keen._client.api.base_url) def test_configure_through_code(self, post): client = KeenClient(project_id="123456", read_key=None, write_key=None) @@ -324,6 +335,7 @@ def tearDown(self): keen.write_key = None keen.read_key = None keen.master_key = None + keen.base_url = None keen._client = None super(QueryTests, self).tearDown() @@ -472,6 +484,7 @@ def setUp(self): keen._client = None keen.project_id = "1k4jb23kjbkjkjsd" keen.master_key = "sdofnasofagaergub" + keen.base_url = None def tearDown(self): keen._client = None @@ -502,6 +515,7 @@ def setUp(self): keen._client = None keen.project_id = "1k4jb23kjbkjkjsd" keen.master_key = "sdofnasofagaergub" + keen.base_url = None def tearDown(self): keen._client = None From ffad02682657b8aaafac74c89fc97c5e7d5cb1e9 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Tue, 24 Nov 2015 16:57:19 -0800 Subject: [PATCH 095/224] Bumped the version to 0.3.19. --- README.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ae08e5..ee7a6cd 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,9 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur ### Changelog +##### 0.3.19 ++ Added `base_url` as a possible env variable + ##### 0.3.18 + Updated error handling to except `ValueError` diff --git a/setup.py b/setup.py index 8a3ca79..818f6f6 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.18", + version="0.3.19", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From 06a2ddeb9069fb29f7cc2fa2249b7634359cb775 Mon Sep 17 00:00:00 2001 From: Joanne Cheng Date: Fri, 13 Nov 2015 15:57:37 -0700 Subject: [PATCH 096/224] Saved Query support in python SDK * allow user to get, create, update, and delete saved queries from keen client * use `Responses` library for http mocking * move all private methods in `keen/api.py` to bottom of file * minor refactoring in `keen/api.py` * Update README with testing instructions * Add python3.4 to travis.yml * Remove 'use-mirrors' option from travis.yml since it's depreciated --- .travis.yml | 5 +- README.md | 31 +++++++ keen/api.py | 58 ++++++------ keen/client.py | 3 +- keen/saved_queries.py | 114 ++++++++++++++++++++++++ keen/tests/saved_query_tests.py | 150 ++++++++++++++++++++++++++++++++ setup.py | 2 +- 7 files changed, 327 insertions(+), 36 deletions(-) create mode 100644 keen/saved_queries.py create mode 100644 keen/tests/saved_query_tests.py diff --git a/.travis.yml b/.travis.yml index f7fcd4b..029191e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,8 @@ python: - "2.7" - "3.2" - "3.3" + - "3.4" # command to install dependencies -install: "pip install -r requirements.txt --use-mirrors" +install: "pip install -r requirements.txt" # command to run tests -script: "python setup.py test" \ No newline at end of file +script: "python setup.py test" diff --git a/README.md b/README.md index ee7a6cd..88ab0a9 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,30 @@ You'll need to set your master_key. See below for more options. +##### Saved Queries +You can manage your saved queries from the Keen python client. + +```python +# Create a saved query +keen.saved_queries.create("name", saved_query_attributes) + +# Get all saved queries +keen.saved_queries.all + +# Get one saved query +keen.saved_queries.get("saved-query-slug") + +# Get saved query with results +keen.saved_queries.results("saved-query-slug") + +# Update a saved query +saved_query_attributes = { refresh_rate: 14400 } +keen.saved_queries.update("saved-query-slug", saved_query_attributes) + +# Delete a saved query +keen.saved_queries.delete("saved-query-slug") +``` + ##### Overwriting event timestamps Two time-related properties are included in your event automatically. The properties “keen.timestamp” and “keen.created_at” are set at the time your event is recorded. You have the ability to overwrite the keen.timestamp property. This could be useful, for example, if you are backfilling historical data. Be sure to use [ISO-8601 Format](https://keen.io/docs/event-data-modeling/event-data-intro/#iso-8601-format). @@ -213,6 +237,13 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur `write_key` and `read_key` now contain scoped keys based on your master API key. +### Testing + +To run tests: +``` +python setup.py tests +``` + ### Changelog ##### 0.3.19 diff --git a/keen/api.py b/keen/api.py index 799466c..e8656a1 100644 --- a/keen/api.py +++ b/keen/api.py @@ -84,14 +84,6 @@ def __init__(self, project_id, write_key=None, read_key=None, self.post_timeout = post_timeout self.session = self._create_session() - def _create_session(self): - - """ Build a session that uses KeenAdapter for SSL """ - - s = requests.Session() - s.mount('https://', KeenAdapter()) - return s - def fulfill(self, method, *args, **kwargs): """ Fulfill an HTTP request to Keen's API. """ @@ -117,7 +109,7 @@ def post_event(self, event): headers = {"Content-Type": "application/json", "Authorization": self.write_key} payload = event.to_json() response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) - self.error_handling(response) + self._error_handling(response) def post_events(self, events): @@ -138,7 +130,7 @@ def post_events(self, events): headers = {"Content-Type": "application/json", "Authorization": self.write_key} payload = json.dumps(events) response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) - self.error_handling(response) + self._error_handling(response) def query(self, analysis_type, params, all_keys=False): """ @@ -158,7 +150,7 @@ def query(self, analysis_type, params, all_keys=False): headers = {"Authorization": self.read_key} payload = params response = self.fulfill(HTTPMethods.GET, url, params=payload, headers=headers, timeout=self.get_timeout) - self.error_handling(response) + self._error_handling(response) response = response.json() @@ -174,11 +166,7 @@ def delete_events(self, event_collection, params): :param event_collection: string, the event collection from which event are being deleted """ - if not self.master_key: - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a master key to run deletes. " - "Please set a 'master_key' when initializing the KeenApi object." - ) + self._check_for_master_key() url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, @@ -187,7 +175,7 @@ def delete_events(self, event_collection, params): headers = {"Content-Type": "application/json", "Authorization": self.master_key} response = self.fulfill(HTTPMethods.DELETE, url, params=params, headers=headers, timeout=self.post_timeout) - self.error_handling(response) + self._error_handling(response) return True def get_collection(self, event_collection): @@ -196,17 +184,12 @@ def get_collection(self, event_collection): :param event_collection: the name of the collection to retrieve info for """ - if not self.master_key: - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a master key to get event collection schema. " - "Please set a 'master_key' when initializing the " - "KeenApi object." - ) + self._check_for_master_key() url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, self.project_id, event_collection) headers = {"Authorization": self.master_key} response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) - self.error_handling(response) + self._error_handling(response) return response.json() @@ -215,21 +198,16 @@ def get_all_collections(self): Extracts schema for all collections using the Keen IO API. A master key must be set first. """ - if not self.master_key: - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a master key to get event collection schema. " - "Please set a 'master_key' when initializing the " - "KeenApi object." - ) + self._check_for_master_key() url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, self.project_id) headers = {"Authorization": self.master_key} response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) - self.error_handling(response) + self._error_handling(response) return response.json() - def error_handling(self, res): + def _error_handling(self, res): """ Helper function to do the error handling @@ -245,3 +223,19 @@ def error_handling(self, res): "error_code": "InvalidResponseFormat" } raise exceptions.KeenApiError(error) + + def _create_session(self): + + """ Build a session that uses KeenAdapter for SSL """ + + s = requests.Session() + s.mount('https://', KeenAdapter()) + return s + + def _check_for_master_key(self): + if not self.master_key: + raise exceptions.InvalidEnvironmentError( + "The Keen IO API requires a master key to perform this operation. " + "Please set a 'master_key' when initializing the " + "KeenApi object." + ) diff --git a/keen/client.py b/keen/client.py index d56e3f3..bba952f 100644 --- a/keen/client.py +++ b/keen/client.py @@ -2,7 +2,7 @@ import copy import json import sys -from keen import persistence_strategies, exceptions +from keen import persistence_strategies, exceptions, saved_queries from keen.api import KeenApi from keen.persistence_strategies import BasePersistenceStrategy @@ -97,6 +97,7 @@ def __init__(self, project_id, write_key=None, read_key=None, self.persistence_strategy = persistence_strategy self.get_timeout = get_timeout self.post_timeout = post_timeout + self.saved_queries = saved_queries.SavedQueriesInterface(project_id, master_key, read_key) if sys.version_info[0] < 3: @staticmethod diff --git a/keen/saved_queries.py b/keen/saved_queries.py new file mode 100644 index 0000000..a343ad0 --- /dev/null +++ b/keen/saved_queries.py @@ -0,0 +1,114 @@ +from keen.api import KeenApi +from keen import exceptions + +class SavedQueriesInterface: + + def __init__(self, project_id, master_key, read_key): + self.project_id = project_id + self.master_key = master_key + self.read_key = read_key + + def all(self): + """ + Gets all saved queries for a project from the Keen IO API. + Master key must be set. + """ + keen_api = KeenApi(self.project_id, master_key=self.master_key) + self._check_for_master_key() + url = "{0}/{1}/projects/{2}/queries/saved".format( + keen_api.base_url, keen_api.api_version, self.project_id + ) + response = keen_api.fulfill("get", url, headers=self._headers()) + + return response.json() + + def get(self, query_name): + """ + Gets a single saved query for a project from the Keen IO API given a + query name. + Master key must be set. + """ + keen_api = KeenApi(self.project_id, master_key=self.master_key) + self._check_for_master_key() + url = "{0}/{1}/projects/{2}/queries/saved/{3}".format( + keen_api.base_url, keen_api.api_version, self.project_id, query_name + ) + response = keen_api.fulfill("get", url, headers=self._headers()) + keen_api._error_handling(response) + + return response.json() + + def results(self, query_name): + """ + Gets a single saved query with a 'result' object for a project from thei + Keen IO API given a query name. + Read or Master key must be set. + """ + keen_api = KeenApi(self.project_id, master_key=self.master_key) + self._check_for_master_or_read_key() + url = "{0}/{1}/projects/{2}/queries/saved/{3}/result".format( + keen_api.base_url, keen_api.api_version, self.project_id, query_name + ) + key = self.master_key if self.master_key else self.read_key + response = keen_api.fulfill("get", url, headers={"Authorization": key }) + keen_api._error_handling(response) + + return response.json() + + def create(self, query_name, saved_query): + """ + Creates the saved query via a PUT request to Keen IO Saved Query endpoint. Master key must be set. + """ + keen_api = KeenApi(self.project_id, master_key=self.master_key) + self._check_for_master_key() + url = "{0}/{1}/projects/{2}/queries/saved/{3}".format( + keen_api.base_url, keen_api.api_version, self.project_id, query_name + ) + response = keen_api.fulfill( + "put", url, headers=self._headers(), data=saved_query + ) + keen_api._error_handling(response) + + return response.json() + + def update(self, query_name, saved_query): + """ + Updates the saved query via a PUT request to Keen IO Saved Query + endpoint. + Master key must be set. + """ + return self.create(query_name, saved_query) + + def delete(self, query_name): + """ + Deletes a saved query from a project with a query name. + Master key must be set. + """ + keen_api = KeenApi(self.project_id, master_key=self.master_key) + self._check_for_master_key() + url = "{0}/{1}/projects/{2}/queries/saved/{3}".format( + keen_api.base_url, keen_api.api_version, self.project_id, query_name + ) + response = keen_api.fulfill("delete", url, headers=self._headers()) + keen_api._error_handling(response) + + return True + + def _headers(self): + return {"Authorization": self.master_key} + + def _check_for_master_key(self): + if not self.master_key: + raise exceptions.InvalidEnvironmentError( + "The Keen IO API requires a master key to perform this operation on saved queries. " + "Please set a 'master_key' when initializing the " + "KeenApi object." + ) + + def _check_for_master_or_read_key(self): + if not (self.read_key or self.master_key): + raise exceptions.InvalidEnvironmentError( + "The Keen IO API requires a read key or master key to perform this operation on saved queries. " + "Please set a 'read_key' or 'master_key' when initializing the " + "KeenApi object." + ) diff --git a/keen/tests/saved_query_tests.py b/keen/tests/saved_query_tests.py new file mode 100644 index 0000000..3d97663 --- /dev/null +++ b/keen/tests/saved_query_tests.py @@ -0,0 +1,150 @@ +from keen.tests.base_test_case import BaseTestCase +from keen.client import KeenClient +from keen import exceptions + +import responses + +class SavedQueryTests(BaseTestCase): + + def setUp(self): + super(SavedQueryTests, self).setUp() + self.exp_project_id = "xxxx1234" + exp_master_key = "abcd3456" + self.client = KeenClient( + project_id=self.exp_project_id, + master_key=exp_master_key + ) + + def test_get_all_saved_queries_keys(self): + client = KeenClient(project_id="123123") + self.assertRaises( + exceptions.InvalidEnvironmentError, client.saved_queries.all + ) + + @responses.activate + def test_get_all_saved_queries(self): + saved_queries_response = [ + { "query_name": "first-saved-query", "query": {} }, + { "query_name": "second-saved-query", "query": {} } + ] + url = "{0}/{1}/projects/{2}/queries/saved".format( + self.client.api.base_url, + self.client.api.api_version, + self.exp_project_id + ) + responses.add( + responses.GET, url, status=200, json=saved_queries_response + ) + + all_saved_queries = self.client.saved_queries.all() + + self.assertEquals(all_saved_queries, saved_queries_response) + + def test_get_one_saved_query_keys(self): + client = KeenClient(project_id="123123") + self.assertRaises( + exceptions.InvalidEnvironmentError, + lambda: client.saved_queries.get("saved-query-name") + ) + + @responses.activate + def test_get_one_saved_query(self): + saved_queries_response = { + "query_name": "saved-query-name" + } + url = "{0}/{1}/projects/{2}/queries/saved/saved-query-name".format( + self.client.api.base_url, + self.client.api.api_version, + self.exp_project_id + ) + responses.add( + responses.GET, url, status=200, json=saved_queries_response + ) + + saved_query = self.client.saved_queries.get("saved-query-name") + + self.assertEquals(saved_query, saved_queries_response) + + def test_create_saved_query_master_key(self): + client = KeenClient(project_id="123123") + self.assertRaises( + exceptions.InvalidEnvironmentError, + lambda: client.saved_queries.create("saved-query-name", {}) + ) + + @responses.activate + def test_create_saved_query(self): + saved_queries_response = { + "query_name": "saved-query-name" + } + url = "{0}/{1}/projects/{2}/queries/saved/saved-query-name".format( + self.client.api.base_url, + self.client.api.api_version, + self.exp_project_id + ) + responses.add( + responses.PUT, url, status=201, json=saved_queries_response + ) + + saved_query = self.client.saved_queries.create("saved-query-name", saved_queries_response) + + self.assertEquals(saved_query, saved_queries_response) + + @responses.activate + def test_update_saved_query(self): + saved_queries_response = { + "query_name": "saved-query-name" + } + url = "{0}/{1}/projects/{2}/queries/saved/saved-query-name".format( + self.client.api.base_url, + self.client.api.api_version, + self.exp_project_id + ) + responses.add( + responses.PUT, url, status=200, json=saved_queries_response + ) + + saved_query = self.client.saved_queries.update("saved-query-name", saved_queries_response) + + self.assertEquals(saved_query, saved_queries_response) + + def test_delete_saved_query_master_key(self): + client = KeenClient(project_id="123123", read_key="123123") + self.assertRaises( + exceptions.InvalidEnvironmentError, + lambda: client.saved_queries.delete("saved-query-name") + ) + + @responses.activate + def test_delete_saved_query(self): + url = "{0}/{1}/projects/{2}/queries/saved/saved-query-name".format( + self.client.api.base_url, + self.client.api.api_version, + self.exp_project_id + ) + responses.add( + responses.DELETE, url, status=204, json="" + ) + + response = self.client.saved_queries.delete("saved-query-name") + + self.assertEquals(response, True) + + @responses.activate + def test_saved_query_results(self): + saved_queries_response = { + "query_name": "saved-query-name", + "results": {} + } + url = "{0}/{1}/projects/{2}/queries/saved/saved-query-name/result".format( + self.client.api.base_url, + self.client.api.api_version, + self.exp_project_id + ) + responses.add( + responses.GET, url, status=209, json=saved_queries_response + ) + + response = self.client.saved_queries.results("saved-query-name") + + self.assertEquals(response, saved_queries_response) diff --git a/setup.py b/setup.py index 818f6f6..82deb00 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ reqs = reqs_file.readlines() reqs_file.close() -tests_require = ['nose', 'mock'] +tests_require = ['nose', 'mock', 'responses'] if sys.version_info < (2, 7): tests_require.append('unittest2') From b515db8c623650cb66d4271d9c13ec7f104cea48 Mon Sep 17 00:00:00 2001 From: Joanne Cheng Date: Wed, 25 Nov 2015 18:25:13 -0500 Subject: [PATCH 097/224] Bump version to 3.2 --- README.md | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 88ab0a9..f3a2d71 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,10 @@ python setup.py tests ### Changelog +##### 0.3.2 ++ Add `saved_queries` support ++ Add Python 3.4 support + ##### 0.3.19 + Added `base_url` as a possible env variable diff --git a/setup.py b/setup.py index 82deb00..952fa77 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.19", + version="0.3.2", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From 78c1a6ceecb3fdde35dfdaf3ba00a49a335e7fc2 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Wed, 25 Nov 2015 15:34:05 -0800 Subject: [PATCH 098/224] Bumped to version 0.3.20 --- README.md | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f3a2d71..fce5b52 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ python setup.py tests ### Changelog -##### 0.3.2 +##### 0.3.20 + Add `saved_queries` support + Add Python 3.4 support diff --git a/setup.py b/setup.py index 952fa77..1aa6a03 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.2", + version="0.3.20", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From b533c13b5b78a6779f65733e101fe6575eab8d03 Mon Sep 17 00:00:00 2001 From: Joanne Date: Wed, 2 Dec 2015 12:17:31 -0700 Subject: [PATCH 099/224] Fix minor error in saved query section of README keen.saved_queries.all -> keen.saved_queries.all() --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fce5b52..cea8f68 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ You can manage your saved queries from the Keen python client. keen.saved_queries.create("name", saved_query_attributes) # Get all saved queries -keen.saved_queries.all +keen.saved_queries.all() # Get one saved query keen.saved_queries.get("saved-query-slug") From a88d7f1a8087e840f8b1ea3b07ee56f993bd4297 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Tue, 8 Mar 2016 12:55:45 -0800 Subject: [PATCH 100/224] =?UTF-8?q?Created=20=E2=80=9CConfigure=20Unique?= =?UTF-8?q?=20Client=20Instances=E2=80=9D=20section.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 61 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index fce5b52..3ed1661 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Use pip to install! pip install keen -This client is known to work on Python 2.6, 2.7, 3.2 and 3.3. +This client is known to work on Python 2.6, 2.7, 3.2, 3.3 and 3.4. For versions of Python < 2.7.9, you’ll need to install pyasn1, ndg-httpsclient, pyOpenSSL. @@ -33,18 +33,7 @@ If you don't want to use environment variables for some reason, you can directly keen.master_key = "abcd" ``` -You can also configure unique client instances as follows: - -```python - from keen.client import KeenClient - - client = KeenClient( - project_id="xxxx", - write_key="yyyy", - read_key="zzzz", - master_key="abcd" - ) -``` +For information on how to configure unique client instances, take a look at the [Advanced Usage](https://github.com/keenlabs/KeenClient-Python#advanced-usage) section below. ##### Send Events to Keen IO @@ -57,12 +46,6 @@ Once you've set `KEEN_PROJECT_ID` and `KEEN_WRITE_KEY`, sending events is simple }) ``` -Or if using unique client instances: - -```python - client.add_event(...) -``` - ##### Send Batch Events to Keen IO You can upload Events in a batch, like so: @@ -86,7 +69,7 @@ That's it! After running your code, check your Keen IO Project to see the event/ ##### Do analysis with Keen IO -Here are some examples of querying. Let's assume you've added some events to the "purchases" collection. +Here are some examples of querying. Let's assume you've added some events to the "purchases" collection. For more code samples, take a look at Keen's [docs](https://keen.io/docs/api/?python#) ```python keen.count("purchases", timeframe="this_14_days") # => 100 @@ -144,6 +127,44 @@ You'll need to set your master_key. See below for more options. +##### Configure Unique Client Instances + +If you intend to send events or query from different projects within the same python file, you'll need to set up unique client instances (one per project). You can do this by assigning an instance of KeenClient to a variable like so: + +```python + from keen.client import KeenClient + + client = KeenClient( + project_id="xxxx", # your project ID for collecting cycling data + write_key="yyyy", + read_key="zzzz", + master_key="abcd" + ) + + client_hike = KeenClient( + project_id="xxxx", # your project ID for collecting hiking data (different from the one above) + write_key="yyyy", + read_key="zzzz", + master_key="abcd" + ) +``` + +You can send events like this: + +```python + # add an event to an event collection in your cycling project + client.add_event(...) + + # or add an event to an event collection in your hiking project + client_hike.add_event(...) +``` + +Similarly, you can query events like this: + +```python + client.count(...) +``` + ##### Saved Queries You can manage your saved queries from the Keen python client. From 5c923a23f8ed40b18a759a03b3ef5afca8de7e4e Mon Sep 17 00:00:00 2001 From: Daniel Kador Date: Mon, 14 Mar 2016 16:31:27 -0700 Subject: [PATCH 101/224] Fix bug with scoped key generation not working with newer Keen projects. --- README.md | 3 + keen/scoped_keys.py | 120 ++++++++++++++++++++++++++++----- keen/tests/scoped_key_tests.py | 42 ++++++++++++ setup.py | 2 +- 4 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 keen/tests/scoped_key_tests.py diff --git a/README.md b/README.md index 47f5fcb..25f9da7 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,9 @@ python setup.py tests ### Changelog +##### 0.3.21 ++ Fix bug with scoped key generation not working with newer Keen projects. + ##### 0.3.20 + Add `saved_queries` support + Add Python 3.4 support diff --git a/keen/scoped_keys.py b/keen/scoped_keys.py index 4697c3b..74d976b 100644 --- a/keen/scoped_keys.py +++ b/keen/scoped_keys.py @@ -8,22 +8,22 @@ __author__ = 'dkador' # the block size for the cipher object; must be 16, 24, or 32 for AES -BLOCK_SIZE = 32 +OLD_BLOCK_SIZE = 32 -def _pad(s): +def pad_aes256(s): """ Pads an input string to a given block size. :param s: string :returns: The padded string. """ - if len(s) % BLOCK_SIZE == 0: + if len(s) % AES.block_size == 0: return s - return Padding.appendPadding(s, blocksize=BLOCK_SIZE) + return Padding.appendPadding(s, blocksize=AES.block_size) -def _unpad(s): +def unpad_aes256(s): """ Removes padding from an input string based on a given block size. :param s: string @@ -33,14 +33,96 @@ def _unpad(s): return s try: - return Padding.removePadding(s, blocksize=BLOCK_SIZE) + return Padding.removePadding(s, blocksize=AES.block_size) except AssertionError: # if there's an error while removing padding, just return s. return s -# encrypt with AES, encode with hex -def _encode_aes(key, plaintext): +def old_pad(s): + """ + Pads an input string to a given block size. + :param s: string + :returns: The padded string. + """ + if len(s) % OLD_BLOCK_SIZE == 0: + return s + + return Padding.appendPadding(s, blocksize=OLD_BLOCK_SIZE) + + +def old_unpad(s): + """ + Removes padding from an input string based on a given block size. + :param s: string + :returns: The unpadded string. + """ + if not s: + return s + + try: + return Padding.removePadding(s, blocksize=OLD_BLOCK_SIZE) + except AssertionError: + # if there's an error while removing padding, just return s. + return s + + +# encrypt with AES-256-CBC, encode with hex +def encode_aes256(key, plaintext): + """ + Utility method to encode some given plaintext with the given key. Important thing to note: + + This is not a general purpose encryption method - it has specific semantics (see below for + details). + + Takes the given hex string key and converts it to a 256 bit binary blob. Then pads the given + plaintext to AES block size which is always 16 bytes, regardless of AES key size. Then + encrypts using AES-256-CBC using a random IV. Then converts both the IV and the ciphertext + to hex. Finally returns the IV appended by the ciphertext. + + :param key: string, 64 hex chars long + :param plaintext: string, any amount of data + """ + if len(key) != 64: + raise TypeError("encode_aes256() expects a 256 bit key encoded as a 64 hex character string") + + # generate AES.block_size cryptographically secure random bytes for our IV (initial value) + iv = os.urandom(AES.block_size) + # set up an AES cipher object + cipher = AES.new(binascii.unhexlify(key), mode=AES.MODE_CBC, IV=iv) + # encrypt the plaintext after padding it + ciphertext = cipher.encrypt(pad_aes256(plaintext)) + # append the hexed IV and the hexed ciphertext + iv_plus_encrypted = binascii.hexlify(iv) + binascii.hexlify(ciphertext) + # return that + return iv_plus_encrypted + + +def decode_aes256(key, iv_plus_encrypted): + """ + Utility method to decode a payload consisting of the hexed IV + the hexed ciphertext using + the given key. See above for more details. + + :param key: string, 64 hex characters long + :param iv_plus_encrypted: string, a hexed IV + hexed ciphertext + """ + # grab first AES.block_size bytes (aka 2 * AES.block_size characters of hex) - that's the IV + iv_size = 2 * AES.block_size + hexed_iv = iv_plus_encrypted[:iv_size] + # grab everything else - that's the ciphertext (aka encrypted message) + hexed_ciphertext = iv_plus_encrypted[iv_size:] + # unhex the iv and ciphertext + iv = binascii.unhexlify(hexed_iv) + ciphertext = binascii.unhexlify(hexed_ciphertext) + # set up the correct AES cipher object + cipher = AES.new(binascii.unhexlify(key), mode=AES.MODE_CBC, IV=iv) + # decrypt! + plaintext = cipher.decrypt(ciphertext) + # return the unpadded version of this + return unpad_aes256(plaintext) + + +def old_encode_aes(key, plaintext): """ Utility method to encode some given plaintext with the given key. Important thing to note: @@ -57,16 +139,16 @@ def _encode_aes(key, plaintext): # generate 16 cryptographically secure random bytes for our IV (initial value) iv = os.urandom(16) # set up an AES cipher object - cipher = AES.new(_pad(key), mode=AES.MODE_CBC, IV=iv) + cipher = AES.new(old_pad(key), mode=AES.MODE_CBC, IV=iv) # encrypte the plaintext after padding it - ciphertext = cipher.encrypt(_pad(plaintext)) + ciphertext = cipher.encrypt(old_pad(plaintext)) # append the hexed IV and the hexed ciphertext iv_plus_encrypted = binascii.hexlify(iv) + binascii.hexlify(ciphertext) # return that return iv_plus_encrypted -def _decode_aes(key, iv_plus_encrypted): +def old_decode_aes(key, iv_plus_encrypted): """ Utility method to decode a payload consisting of the hexed IV + the hexed ciphertext using the given key. See above for more details. @@ -82,18 +164,24 @@ def _decode_aes(key, iv_plus_encrypted): iv = binascii.unhexlify(hexed_iv) ciphertext = binascii.unhexlify(hexed_ciphertext) # set up the correct AES cipher object - cipher = AES.new(_pad(key), mode=AES.MODE_CBC, IV=iv) + cipher = AES.new(old_pad(key), mode=AES.MODE_CBC, IV=iv) # decrypt! plaintext = cipher.decrypt(ciphertext) # return the unpadded version of this - return _unpad(plaintext) + return old_unpad(plaintext) def encrypt(api_key, options): options_string = json.dumps(options) - return _encode_aes(api_key, options_string) + if len(api_key) == 64: + return encode_aes256(api_key, options_string) + else: + return old_encode_aes(api_key, options_string) def decrypt(api_key, scoped_key): - json_string = _decode_aes(api_key, scoped_key) - return json.loads(json_string) \ No newline at end of file + if len(api_key) == 64: + json_string = decode_aes256(api_key, scoped_key) + else: + json_string = old_decode_aes(api_key, scoped_key) + return json.loads(json_string) diff --git a/keen/tests/scoped_key_tests.py b/keen/tests/scoped_key_tests.py new file mode 100644 index 0000000..6882778 --- /dev/null +++ b/keen/tests/scoped_key_tests.py @@ -0,0 +1,42 @@ +from keen import scoped_keys +from keen.tests.base_test_case import BaseTestCase + + +class ScopedKeyTests(BaseTestCase): + api_key = "24077ACBCB198BAAA2110EDDB673282F8E34909FD823A15C55A6253A664BE368" + bad_api_key = "24077ACBCB198BAAA2110EDDB673282F8E34909FD823A15C55A6253A664BE369" + old_api_key = "ab428324dbdbcfe744" + old_bad_api_key = "badbadbadbad" + options = { + "filters": [{ + "property_name": "accountId", + "operator": "eq", + "property_value": "123456" + }] + } + + def test_scoped_key_encrypts_and_decrypts(self): + encrypted = scoped_keys.encrypt(self.api_key, self.options) + decrypted = scoped_keys.decrypt(self.api_key, encrypted) + self.assert_equal(decrypted, self.options) + + def test_scoped_key_fails_decryption_bad_key(self): + encrypted = scoped_keys.encrypt(self.api_key, self.options) + try: + scoped_keys.decrypt(self.bad_api_key, encrypted) + self.fail("shouldn't get here") + except ValueError as e: + self.assert_is_not_none(e) + + def test_old_scoped_key_encrypts_and_decrypts(self): + encrypted = scoped_keys.encrypt(self.old_api_key, self.options) + decrypted = scoped_keys.decrypt(self.old_api_key, encrypted) + self.assert_equal(decrypted, self.options) + + def test_old_scoped_key_fails_decryption_on_bad_key(self): + encrypted = scoped_keys.encrypt(self.old_api_key, self.options) + try: + scoped_keys.decrypt(self.old_bad_api_key, encrypted) + self.fail("shouldn't get here") + except ValueError as e: + self.assert_is_not_none(e) diff --git a/setup.py b/setup.py index 1aa6a03..79eeb52 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.20", + version="0.3.21", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From 8d7269c512fb3ba1ddd1a3fbeb0587f44a5ca549 Mon Sep 17 00:00:00 2001 From: Daniel Kador Date: Mon, 14 Mar 2016 17:29:47 -0700 Subject: [PATCH 102/224] fix up py2 vs py3 incompatibilities --- keen/Padding.py | 10 ++++++++-- keen/scoped_keys.py | 9 ++++++--- keen/tests/scoped_key_tests.py | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/keen/Padding.py b/keen/Padding.py index 5615340..b213370 100644 --- a/keen/Padding.py +++ b/keen/Padding.py @@ -103,7 +103,10 @@ def appendCMSPadding(str, blocksize=AES_blocksize): def removeCMSPadding(str, blocksize=AES_blocksize): '''CMS padding: Remove padding with bytes containing the number of padding bytes ''' - pad_len = ord(str[-1]) # last byte contains number of padding bytes + try: + pad_len = ord(str[-1]) # last byte contains number of padding bytes + except TypeError: + pad_len = str[-1] assert pad_len <= blocksize, 'padding error' assert pad_len <= len(str), 'padding error' @@ -152,7 +155,10 @@ def appendZeroLenPadding(str, blocksize=AES_blocksize): def removeZeroLenPadding(str, blocksize=AES_blocksize): 'Remove Padding with zeroes + last byte equal to the number of padding bytes' - pad_len = ord(str[-1]) # last byte contains number of padding bytes + try: + pad_len = ord(str[-1]) # last byte contains number of padding bytes + except TypeError: + pad_len = str[-1] assert pad_len < blocksize, 'padding error' assert pad_len < len(str), 'padding error' diff --git a/keen/scoped_keys.py b/keen/scoped_keys.py index 74d976b..e064c9b 100644 --- a/keen/scoped_keys.py +++ b/keen/scoped_keys.py @@ -89,7 +89,7 @@ def encode_aes256(key, plaintext): # generate AES.block_size cryptographically secure random bytes for our IV (initial value) iv = os.urandom(AES.block_size) # set up an AES cipher object - cipher = AES.new(binascii.unhexlify(key), mode=AES.MODE_CBC, IV=iv) + cipher = AES.new(binascii.unhexlify(key.encode('ascii')), mode=AES.MODE_CBC, IV=iv) # encrypt the plaintext after padding it ciphertext = cipher.encrypt(pad_aes256(plaintext)) # append the hexed IV and the hexed ciphertext @@ -115,7 +115,7 @@ def decode_aes256(key, iv_plus_encrypted): iv = binascii.unhexlify(hexed_iv) ciphertext = binascii.unhexlify(hexed_ciphertext) # set up the correct AES cipher object - cipher = AES.new(binascii.unhexlify(key), mode=AES.MODE_CBC, IV=iv) + cipher = AES.new(binascii.unhexlify(key.encode('ascii')), mode=AES.MODE_CBC, IV=iv) # decrypt! plaintext = cipher.decrypt(ciphertext) # return the unpadded version of this @@ -184,4 +184,7 @@ def decrypt(api_key, scoped_key): json_string = decode_aes256(api_key, scoped_key) else: json_string = old_decode_aes(api_key, scoped_key) - return json.loads(json_string) + try: + return json.loads(json_string) + except TypeError: + return json.loads(json_string.decode()) diff --git a/keen/tests/scoped_key_tests.py b/keen/tests/scoped_key_tests.py index 6882778..ff65c31 100644 --- a/keen/tests/scoped_key_tests.py +++ b/keen/tests/scoped_key_tests.py @@ -26,7 +26,7 @@ def test_scoped_key_fails_decryption_bad_key(self): scoped_keys.decrypt(self.bad_api_key, encrypted) self.fail("shouldn't get here") except ValueError as e: - self.assert_is_not_none(e) + self.assert_not_equal(e, None) def test_old_scoped_key_encrypts_and_decrypts(self): encrypted = scoped_keys.encrypt(self.old_api_key, self.options) @@ -39,4 +39,4 @@ def test_old_scoped_key_fails_decryption_on_bad_key(self): scoped_keys.decrypt(self.old_bad_api_key, encrypted) self.fail("shouldn't get here") except ValueError as e: - self.assert_is_not_none(e) + self.assert_not_equal(e, None) From 7dc25f8397e691ecc7fecd5a685dd4fe08b4b993 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Tue, 26 Apr 2016 14:51:51 -0700 Subject: [PATCH 103/224] Added support for python 3.5. --- .travis.yml | 1 + README.md | 9 ++++++--- setup.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 029191e..2455552 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "3.2" - "3.3" - "3.4" + - "3.5" # command to install dependencies install: "pip install -r requirements.txt" # command to run tests diff --git a/README.md b/README.md index 25f9da7..ea007bf 100644 --- a/README.md +++ b/README.md @@ -267,12 +267,15 @@ python setup.py tests ### Changelog +##### 0.3.22 ++ Added support for python 3.5 + ##### 0.3.21 -+ Fix bug with scoped key generation not working with newer Keen projects. ++ Fixed bug with scoped key generation not working with newer Keen projects. ##### 0.3.20 -+ Add `saved_queries` support -+ Add Python 3.4 support ++ Added `saved_queries` support ++ Added Python 3.4 support ##### 0.3.19 + Added `base_url` as a possible env variable diff --git a/setup.py b/setup.py index 79eeb52..661d1de 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.21", + version="0.3.22", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From 9689c56f6c9c9973a5403449963ea89893cafe4d Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Tue, 26 Apr 2016 15:47:37 -0700 Subject: [PATCH 104/224] Updated README to reflect python 3.5 support. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ea007bf..8cac9d2 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Use pip to install! pip install keen -This client is known to work on Python 2.6, 2.7, 3.2, 3.3 and 3.4. +This client is known to work on Python 2.6, 2.7, 3.2, 3.3, 3.4 and 3.5. For versions of Python < 2.7.9, you’ll need to install pyasn1, ndg-httpsclient, pyOpenSSL. From f4c2502a3072c87c0aabf4f217e294699c9ce0b1 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 9 May 2016 14:08:12 -0700 Subject: [PATCH 105/224] Added status code to JSON parse error response. --- keen/api.py | 7 +++---- keen/tests/client_tests.py | 22 +++++++++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/keen/api.py b/keen/api.py index e8656a1..5b495c7 100644 --- a/keen/api.py +++ b/keen/api.py @@ -199,8 +199,7 @@ def get_all_collections(self): """ self._check_for_master_key() - url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, - self.project_id) + url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, self.project_id) headers = {"Authorization": self.master_key} response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) self._error_handling(response) @@ -219,8 +218,8 @@ def _error_handling(self, res): error = res.json() except ValueError: error = { - 'message': 'The API did not respond with JSON, but: "{0}"'.format(res.text[:1000]), - "error_code": "InvalidResponseFormat" + "message": "The API did not respond with JSON, but: {}".format(res.text[:1000]), + "error_code": "{}".format(res.status_code) } raise exceptions.KeenApiError(error) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index d7a059e..d54745e 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -15,9 +15,10 @@ class MockedResponse(object): - def __init__(self, status_code, json_response): + def __init__(self, status_code, json_response, text=None): self.status_code = status_code self.json_response = json_response + self.text = text def json(self): return self.json_response @@ -28,6 +29,11 @@ def json(self): return self.json_response +class MockedMalformedJsonResponse(MockedResponse): + def json(self): + raise ValueError + + @patch("requests.Session.post") class ClientTests(BaseTestCase): @@ -123,6 +129,19 @@ def test_post_timeout_batch(self, post): post.side_effect = requests.Timeout self.assert_raises(requests.Timeout, keen.add_events, {"python_test": [{"hello": "goodbye"}]}) + def test_malformed_json_response(self, post): + post.return_value = MockedMalformedJsonResponse( + status_code=401, + json_response={}, + text="test error text" + ) + + with self.assert_raises(exceptions.KeenApiError) as cm: + keen.add_event("python_test", {"hello": "goodbye"}) + + self.assertIn(post.return_value.text, str(cm.exception)) + self.assertIn(str(post.return_value.status_code), str(cm.exception)) + def test_environment_variables(self, post): post.return_value = MockedFailedResponse( status_code=401, @@ -297,6 +316,7 @@ def url_escape(self, url): import urllib.parse return urllib.parse.quote(url) + class EventTests(BaseTestCase): def test_custom_addon(self): From 5da69eec57d95709abbde50cad67eba1a7a7e115 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 9 May 2016 14:37:16 -0700 Subject: [PATCH 106/224] Reformatted test wrt to python 2.6. --- keen/tests/client_tests.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index d54745e..83e9bfa 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -136,11 +136,16 @@ def test_malformed_json_response(self, post): text="test error text" ) - with self.assert_raises(exceptions.KeenApiError) as cm: + exception = None + + try: keen.add_event("python_test", {"hello": "goodbye"}) + except exceptions.KeenApiError as e: + + exception = e - self.assertIn(post.return_value.text, str(cm.exception)) - self.assertIn(str(post.return_value.status_code), str(cm.exception)) + self.assertIn(post.return_value.text, str(exception)) + self.assertIn(str(post.return_value.status_code), str(exception)) def test_environment_variables(self, post): post.return_value = MockedFailedResponse( From d1902457cbd62a58ffa55bfacafd4704257c1d5f Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 9 May 2016 15:09:23 -0700 Subject: [PATCH 107/224] One more try at fixing py26 error. --- keen/tests/client_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 83e9bfa..e89c301 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -132,7 +132,7 @@ def test_post_timeout_batch(self, post): def test_malformed_json_response(self, post): post.return_value = MockedMalformedJsonResponse( status_code=401, - json_response={}, + json_response=" ", text="test error text" ) From e57256f8a6bbfc1eb9aef89e72f823cce4f81cbb Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 9 May 2016 15:16:20 -0700 Subject: [PATCH 108/224] Added indices back to the error formatting. --- keen/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keen/api.py b/keen/api.py index 5b495c7..1f24473 100644 --- a/keen/api.py +++ b/keen/api.py @@ -218,8 +218,8 @@ def _error_handling(self, res): error = res.json() except ValueError: error = { - "message": "The API did not respond with JSON, but: {}".format(res.text[:1000]), - "error_code": "{}".format(res.status_code) + "message": "The API did not respond with JSON, but: {0}".format(res.text[:1000]), + "error_code": "{0}".format(res.status_code) } raise exceptions.KeenApiError(error) From 390e718ae75008ba9c69d75b03b349af1ff933e3 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Mon, 9 May 2016 15:25:12 -0700 Subject: [PATCH 109/224] Used `assertTrue` in place of `assertIn`. --- keen/tests/client_tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index e89c301..5d7ab32 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -144,8 +144,8 @@ def test_malformed_json_response(self, post): exception = e - self.assertIn(post.return_value.text, str(exception)) - self.assertIn(str(post.return_value.status_code), str(exception)) + self.assertTrue(post.return_value.text in str(exception)) + self.assertTrue(str(post.return_value.status_code) in str(exception)) def test_environment_variables(self, post): post.return_value = MockedFailedResponse( From d1be5e0023f2cf573a1342d232a4539ea81485f3 Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Tue, 10 May 2016 12:16:28 -0700 Subject: [PATCH 110/224] Bumped the version to 0.3.23. --- README.md | 3 +++ setup.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8cac9d2..dd599e7 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,9 @@ python setup.py tests ### Changelog +##### 0.3.23 ++ Added status code to JSON parse error response + ##### 0.3.22 + Added support for python 3.5 diff --git a/setup.py b/setup.py index 661d1de..dd70a78 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.22", + version="0.3.23", description="Python Client for Keen IO", author="Keen IO", author_email="team@keen.io", From 2d46038a9b93e79868aa03c02523a5a84b35116b Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Sun, 29 May 2016 15:28:24 +0100 Subject: [PATCH 111/224] Few improvements for Pypi - Add classifiers - Readme in RST format so that it shows better on pypi --- README.md => README.rst | 319 +++++++++++++++++++++++++++------------- setup.py | 17 +++ 2 files changed, 230 insertions(+), 106 deletions(-) rename README.md => README.rst (66%) diff --git a/README.md b/README.rst similarity index 66% rename from README.md rename to README.rst index dd599e7..3e84063 100644 --- a/README.md +++ b/README.rst @@ -1,56 +1,69 @@ Keen IO Official Python Client Library ====================================== -[![Build Status](https://secure.travis-ci.org/keenlabs/KeenClient-Python.png)](http://travis-ci.org/keenlabs/KeenClient-Python) +|build-status| -This is the official Python Client for the [Keen IO](https://keen.io/) API. The +This is the official Python Client for the `Keen IO `_ API. The Keen IO API lets developers build analytics features directly into their apps. This is still under active development. Stay tuned for improvements! -### Installation +Installation +------------ Use pip to install! +:: + pip install keen This client is known to work on Python 2.6, 2.7, 3.2, 3.3, 3.4 and 3.5. For versions of Python < 2.7.9, you’ll need to install pyasn1, ndg-httpsclient, pyOpenSSL. -### Usage +Usage +----- -To use this client with the Keen IO API, you have to configure your Keen IO Project ID and its access keys (if you need an account, [sign up here](https://keen.io/) - it's free). +To use this client with the Keen IO API, you have to configure your Keen IO Project ID and its access +keys (if you need an account, `sign up here `_ - it's free). -Setting a write key is required for publishing events. Setting a read key is required for running queries. The recommended way to set this configuration information is via the environment. The keys you can set are `KEEN_PROJECT_ID`, `KEEN_WRITE_KEY`, `KEEN_READ_KEY`, and `KEEN_MASTER_KEY`. +Setting a write key is required for publishing events. Setting a read key is required for +running queries. The recommended way to set this configuration information is via the environment. +The keys you can set are `KEEN_PROJECT_ID`, `KEEN_WRITE_KEY`, `KEEN_READ_KEY`, and `KEEN_MASTER_KEY`. If you don't want to use environment variables for some reason, you can directly set values as follows: -```python +.. code-block:: python + keen.project_id = "xxxx" keen.write_key = "yyyy" keen.read_key = "zzzz" keen.master_key = "abcd" -``` -For information on how to configure unique client instances, take a look at the [Advanced Usage](https://github.com/keenlabs/KeenClient-Python#advanced-usage) section below. -##### Send Events to Keen IO +For information on how to configure unique client instances, take a look at the +`Advanced Usage <#advanced-usage>`_ section below. + +Send Events to Keen IO +`````````````````````` Once you've set `KEEN_PROJECT_ID` and `KEEN_WRITE_KEY`, sending events is simple: -```python +.. code-block:: python + keen.add_event("sign_ups", { "username": "lloyd", "referred_by": "harry" }) -``` -##### Send Batch Events to Keen IO + +Send Batch Events to Keen IO +```````````````````````````` You can upload Events in a batch, like so: -```python +.. code-block:: python + # uploads 4 events total - 2 to the "sign_ups" collection and 2 to the "purchases" collection keen.add_events({ "sign_ups": [ @@ -62,16 +75,18 @@ You can upload Events in a batch, like so: { "price": 6 } ] }) -``` That's it! After running your code, check your Keen IO Project to see the event/events has been added. -##### Do analysis with Keen IO +Do analysis with Keen IO +```````````````````````` + +Here are some examples of querying. Let's assume you've added some events to the "purchases" collection. +For more code samples, take a look at Keen's `docs `_ -Here are some examples of querying. Let's assume you've added some events to the "purchases" collection. For more code samples, take a look at Keen's [docs](https://keen.io/docs/api/?python#) +.. code-block:: python -```python keen.count("purchases", timeframe="this_14_days") # => 100 keen.sum("purchases", target_property="price", timeframe="this_14_days") # => 10000 keen.minimum("purchases", target_property="price", timeframe="this_14_days") # => 20 @@ -107,31 +122,36 @@ Here are some examples of querying. Let's assume you've added some events to the "actor_property": "user.email" } keen.funnel([step1, step2], timeframe="today") # => [2039, 201] -``` + To return the full API response (as opposed to the singular "result" key), set `all_keys=True`. For example, `keen.funnel([step1, step2], all_keys=True)` would return "result", "actors" and "steps" keys. -##### Delete Events +Delete Events +````````````` -The Keen IO API allows you to [delete events](https://keen.io/docs/api/#delete-events) from event collections, optionally supplying filters, timeframe or timezone to narrow the scope of what you would like to delete. +The Keen IO API allows you to `delete events `_ from event collections, optionally supplying filters, timeframe or timezone to narrow the scope of what you would like to delete. You'll need to set your master_key. -```python +.. code-block:: python + keen.delete_events("event_collection", filters=[{"property_name": 'username', "operator": 'eq', "property_value": 'Bob'}]) -``` -#### Advanced Usage +Advanced Usage +`````````````` See below for more options. -##### Configure Unique Client Instances +Configure Unique Client Instances +''''''''''''''''''''''''''''''''' + +If you intend to send events or query from different projects within the same python file, you'll need to set up +unique client instances (one per project). You can do this by assigning an instance of KeenClient to a variable like so: -If you intend to send events or query from different projects within the same python file, you'll need to set up unique client instances (one per project). You can do this by assigning an instance of KeenClient to a variable like so: +.. code-block:: python -```python from keen.client import KeenClient client = KeenClient( @@ -147,55 +167,65 @@ If you intend to send events or query from different projects within the same py read_key="zzzz", master_key="abcd" ) -``` + You can send events like this: -```python +.. code-block:: python + # add an event to an event collection in your cycling project client.add_event(...) # or add an event to an event collection in your hiking project client_hike.add_event(...) -``` + Similarly, you can query events like this: -```python +.. code-block:: python + client.count(...) -``` -##### Saved Queries + +Saved Queries +''''''''''''' + You can manage your saved queries from the Keen python client. -```python -# Create a saved query -keen.saved_queries.create("name", saved_query_attributes) +.. code-block:: python + + # Create a saved query + keen.saved_queries.create("name", saved_query_attributes) -# Get all saved queries -keen.saved_queries.all() + # Get all saved queries + keen.saved_queries.all() -# Get one saved query -keen.saved_queries.get("saved-query-slug") + # Get one saved query + keen.saved_queries.get("saved-query-slug") -# Get saved query with results -keen.saved_queries.results("saved-query-slug") + # Get saved query with results + keen.saved_queries.results("saved-query-slug") -# Update a saved query -saved_query_attributes = { refresh_rate: 14400 } -keen.saved_queries.update("saved-query-slug", saved_query_attributes) + # Update a saved query + saved_query_attributes = { refresh_rate: 14400 } + keen.saved_queries.update("saved-query-slug", saved_query_attributes) -# Delete a saved query -keen.saved_queries.delete("saved-query-slug") -``` + # Delete a saved query + keen.saved_queries.delete("saved-query-slug") -##### Overwriting event timestamps -Two time-related properties are included in your event automatically. The properties “keen.timestamp” and “keen.created_at” are set at the time your event is recorded. You have the ability to overwrite the keen.timestamp property. This could be useful, for example, if you are backfilling historical data. Be sure to use [ISO-8601 Format](https://keen.io/docs/event-data-modeling/event-data-intro/#iso-8601-format). +Overwriting event timestamps +'''''''''''''''''''''''''''' + +Two time-related properties are included in your event automatically. The properties “keen.timestamp” +and “keen.created_at” are set at the time your event is recorded. You have the ability to overwrite the +keen.timestamp property. This could be useful, for example, if you are backfilling historical data. Be +sure to use `ISO-8601 Format `_. Keen stores all date and time information in UTC! -```python +.. code-block:: python + keen.add_event("sign_ups", { "keen": { "timestamp": "2012-07-06T02:09:10.141Z" @@ -203,13 +233,17 @@ Keen stores all date and time information in UTC! "username": "lloyd", "referred_by": "harry" }) -``` -##### Get from Keen IO with a Timeout -By default, GET requests will timeout after 305 seconds. If you want to manually override this, you can create a KeenClient with the "get_timeout" parameter. This client will fail GETs if no bytes have been returned by the server in the specified time. For example: +Get from Keen IO with a Timeout +''''''''''''''''''''''''''''''' + +By default, GET requests will timeout after 305 seconds. If you want to manually override this, you can +create a KeenClient with the "get_timeout" parameter. This client will fail GETs if no bytes have been +returned by the server in the specified time. For example: + +.. code-block:: python -```python from keen.client import KeenClient client = KeenClient( @@ -219,15 +253,21 @@ By default, GET requests will timeout after 305 seconds. If you want to manually get_timeout=100 ) -``` -This will cause queries such as count(), sum(), and average() to timeout after 100 seconds. If this timeout limit is hit, a requests.Timeout will be raised. Due to a bug in the requests library, you might also see an SSLError (https://github.com/kennethreitz/requests/issues/1294) -##### Send to Keen IO with a Timeout +This will cause queries such as count(), sum(), and average() to timeout after 100 seconds. If this timeout +limit is hit, a requests.Timeout will be raised. Due to a bug in the requests library, you might also see an +SSLError (`#1294 `_) + +Send to Keen IO with a Timeout +'''''''''''''''''''''''''''''' + +By default, POST requests will timeout after 305 seconds. If you want to manually override this, you can +create a KeenClient with the "post_timeout" parameter. This client will fail POSTs if no bytes have been +returned by the server in the specified time. For example: -By default, POST requests will timeout after 305 seconds. If you want to manually override this, you can create a KeenClient with the "post_timeout" parameter. This client will fail POSTs if no bytes have been returned by the server in the specified time. For example: +.. code-block:: python -```python from keen.client import KeenClient client = KeenClient( @@ -238,15 +278,17 @@ By default, POST requests will timeout after 305 seconds. If you want to manuall post_timeout=100 ) -``` + This will cause both add_event() and add_events() to timeout after 100 seconds. If this timeout limit is hit, a requests.Timeout will be raised. Due to a bug in the requests library, you might also see an SSLError (https://github.com/kennethreitz/requests/issues/1294) -##### Create Scoped Keys +Create Scoped Keys +'''''''''''''''''' + +The Python client enables you to create `Scoped Keys `_ easily. For example: -The Python client enables you to create [Scoped Keys](https://keen.io/docs/security/#scoped-key) easily. For example: +.. code-block:: python -```python from keen.client import KeenClient from keen import scoped_keys @@ -254,153 +296,218 @@ The Python client enables you to create [Scoped Keys](https://keen.io/docs/secur write_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}) read_key = scoped_keys.encrypt(api_key, {"allowed_operations": ["read"]}) -``` + `write_key` and `read_key` now contain scoped keys based on your master API key. -### Testing +Testing +------- To run tests: -``` -python setup.py tests -``` -### Changelog +:: + + python setup.py tests + + +Changelog +--------- + +0.3.23 +`````` -##### 0.3.23 + Added status code to JSON parse error response -##### 0.3.22 +0.3.22 +`````` + + Added support for python 3.5 -##### 0.3.21 +0.3.21 +`````` + + Fixed bug with scoped key generation not working with newer Keen projects. -##### 0.3.20 +0.3.20 +`````` + + Added `saved_queries` support + Added Python 3.4 support -##### 0.3.19 +0.3.19 +`````` + + Added `base_url` as a possible env variable -##### 0.3.18 +0.3.18 +`````` + + Updated error handling to except `ValueError` -##### 0.3.17 +0.3.17 +`````` + + Fixed timestamp overriding keen addons + Added `get_collection` and `get_all_collections` methods -##### 0.3.16 +0.3.16 +`````` + + Added `all_keys` parameter which allows users to expose all keys in query response. + Added `delete_events` method. -##### 0.3.15 +0.3.15 +`````` + + Added better error handling to surface all errors from HTTP API calls. -##### 0.3.14 +0.3.14 +`````` + + Added compatibility for pip 1.0 -##### 0.3.13 +0.3.13 +`````` + + Added compatibility for pip < 1.5.6 -##### 0.3.12 +0.3.12 +`````` + + Made requirements more flexible. -##### 0.3.11 +0.3.11 +`````` + + Added `requirements.txt` to pypi package. -##### 0.3.10 +0.3.10 +`````` + + Fixed requirements in `setup.py` + Updated test inputs and documentation. -##### 0.3.9 +0.3.9 +````` + + Added ```master_key``` parameter. -##### 0.3.8 +0.3.8 +````` + + Mocked tests. + Added ```median``` query method. + Added support for `$python setup.py test`. -##### 0.3.7 +0.3.7 +````` + + Upgraded to requests==2.5.1 -##### 0.3.6 +0.3.6 +````` + + Added ```max_age``` parameter for caching. -##### 0.3.5 +0.3.5 +````` + + Added client configurable timeout to gets. -##### 0.3.4 +0.3.4 +````` + + Added ```percentile``` query method. -##### 0.3.3 +0.3.3 +````` + Support ```interval``` parameter for multi analyses on the keen module. -##### 0.3.2 +0.3.2 +````` + Reuse internal requests' session inside an instance of KeenApi. -##### 0.3.1 +0.3.1 +````` + Support ```property_names``` parameter for extractions. -##### 0.3.0 +0.3.0 +````` + Added client configurable timeout to posts. + Upgraded to requests==2.2.1. -##### 0.2.3 +0.2.3 +````` + Fixed sys.version_info issue with Python 2.6. -##### 0.2.2 +0.2.2 +````` + Added interval to multi_analysis. -##### 0.2.1 +0.2.1 +````` + Added stacktrace_id and unique_id to Keen API errors. -##### 0.2.0 +0.2.0 +````` + Added add_events method to keen/__init__.py so it can be used at a module level. + Added method to generate image beacon URLs. -##### 0.1.9 +0.1.9 +````` + Added support for publishing events in batches + Added support for configuring client automatically from environment + Added methods on keen module directly -##### 0.1.8 +0.1.8 +````` + Added querying support -##### 0.1.7 +0.1.7 +````` + Bugfix to use write key when sending events - do not use 0.1.6! -##### 0.1.6 +0.1.6 +````` + Changed project token -> project ID. + Added support for read and write scoped keys. + Added support for generating scoped keys yourself. + Added support for python 2.6, 3.2, and 3.3 -##### 0.1.5 +0.1.5 +````` + Added documentation. -### To Do +To Do +----- * Asynchronous insert * Scoped keys -### Questions & Support +Questions & Support +------------------- If you have any questions, bugs, or suggestions, please report them via Github Issues. We'd love to hear your feedback and ideas! -### Contributing +Contributing +------------ + This is an open source project and we love involvement from the community! Hit us up with pull requests and issues. + +.. |build-status| image:: https://secure.travis-ci.org/keenlabs/KeenClient-Python.png + :target: http://travis-ci.org/keenlabs/KeenClient-Python + :alt: Build status diff --git a/setup.py b/setup.py index dd70a78..446d1b4 100644 --- a/setup.py +++ b/setup.py @@ -30,6 +30,7 @@ name="keen", version="0.3.23", description="Python Client for Keen IO", + long_description=open(os.path.join('README.rst'), 'r').read(), author="Keen IO", author_email="team@keen.io", url="https://github.com/keenlabs/KeenClient-Python", @@ -37,4 +38,20 @@ install_requires=reqs, tests_require=tests_require, test_suite='nose.collector', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Topic :: Software Development :: Libraries :: Python Modules', + ] ) From 8f51becbc3dd96bb482f6b5a5e274a3551d4890e Mon Sep 17 00:00:00 2001 From: Fitz Elliott Date: Wed, 8 Jun 2016 15:19:04 -0400 Subject: [PATCH 112/224] Only funnel takes an all_keys param --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dd599e7..b17b62f 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ Here are some examples of querying. Let's assume you've added some events to the keen.funnel([step1, step2], timeframe="today") # => [2039, 201] ``` -To return the full API response (as opposed to the singular "result" key), set `all_keys=True`. +To return the full API response from a funnel analysis (as opposed to the singular "result" key), set `all_keys=True`. For example, `keen.funnel([step1, step2], all_keys=True)` would return "result", "actors" and "steps" keys. From ca98288456f22a1fcc24a482962485945b07430d Mon Sep 17 00:00:00 2001 From: Stephanie Stroud Date: Wed, 29 Jun 2016 16:35:36 -0700 Subject: [PATCH 113/224] Bumped version to 0.3.24. --- README.rst | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5c70d79..1c42e88 100644 --- a/README.rst +++ b/README.rst @@ -313,6 +313,11 @@ To run tests: Changelog --------- +0.3.24 +`````` + ++ Updated documentation + 0.3.23 `````` diff --git a/setup.py b/setup.py index 446d1b4..d6d46a2 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.23", + version="0.3.24", description="Python Client for Keen IO", long_description=open(os.path.join('README.rst'), 'r').read(), author="Keen IO", From fb260c0359b890b9d483b3d0c4797e5cb70ecb43 Mon Sep 17 00:00:00 2001 From: Dan Stace Date: Mon, 6 Jun 2016 12:41:11 -0400 Subject: [PATCH 114/224] use pycryptodome instead of defunct pycrypto --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a1f831f..7a8d681 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ Padding>=0.4 -pycrypto>=2.6,<2.7 +pycryptodome>=3.4 requests>=2.5,<3.0 From ec162d20b6c72818d7076f39186c4d12f6225264 Mon Sep 17 00:00:00 2001 From: Dan Stace Date: Mon, 25 Jul 2016 12:12:10 -0400 Subject: [PATCH 115/224] ensure bytes are used for encryption keys --- keen/scoped_keys.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/keen/scoped_keys.py b/keen/scoped_keys.py index e064c9b..417e384 100644 --- a/keen/scoped_keys.py +++ b/keen/scoped_keys.py @@ -1,6 +1,7 @@ import binascii import json import os +import six from Crypto.Cipher import AES from keen import Padding @@ -10,6 +11,14 @@ # the block size for the cipher object; must be 16, 24, or 32 for AES OLD_BLOCK_SIZE = 32 +DEFAULT_ENCODING = 'UTF-8' + + +def ensure_bytes(s, encoding=None): + if isinstance(s, six.text_type): + return s.encode(encoding or DEFAULT_ENCODING) + return s + def pad_aes256(s): """ @@ -91,7 +100,7 @@ def encode_aes256(key, plaintext): # set up an AES cipher object cipher = AES.new(binascii.unhexlify(key.encode('ascii')), mode=AES.MODE_CBC, IV=iv) # encrypt the plaintext after padding it - ciphertext = cipher.encrypt(pad_aes256(plaintext)) + ciphertext = cipher.encrypt(ensure_bytes(pad_aes256(plaintext))) # append the hexed IV and the hexed ciphertext iv_plus_encrypted = binascii.hexlify(iv) + binascii.hexlify(ciphertext) # return that @@ -139,9 +148,9 @@ def old_encode_aes(key, plaintext): # generate 16 cryptographically secure random bytes for our IV (initial value) iv = os.urandom(16) # set up an AES cipher object - cipher = AES.new(old_pad(key), mode=AES.MODE_CBC, IV=iv) + cipher = AES.new(ensure_bytes(old_pad(key)), mode=AES.MODE_CBC, IV=iv) # encrypte the plaintext after padding it - ciphertext = cipher.encrypt(old_pad(plaintext)) + ciphertext = cipher.encrypt(ensure_bytes(old_pad(plaintext))) # append the hexed IV and the hexed ciphertext iv_plus_encrypted = binascii.hexlify(iv) + binascii.hexlify(ciphertext) # return that @@ -164,7 +173,7 @@ def old_decode_aes(key, iv_plus_encrypted): iv = binascii.unhexlify(hexed_iv) ciphertext = binascii.unhexlify(hexed_ciphertext) # set up the correct AES cipher object - cipher = AES.new(old_pad(key), mode=AES.MODE_CBC, IV=iv) + cipher = AES.new(ensure_bytes(old_pad(key)), mode=AES.MODE_CBC, IV=iv) # decrypt! plaintext = cipher.decrypt(ciphertext) # return the unpadded version of this From 5706ab31e0deac093c7ce0512cebaef2f9f193e2 Mon Sep 17 00:00:00 2001 From: Dan Stace Date: Thu, 11 Aug 2016 12:58:28 -0400 Subject: [PATCH 116/224] require requests < 2.11.0 for py3.2 support --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7a8d681..10fd324 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ Padding>=0.4 pycryptodome>=3.4 -requests>=2.5,<3.0 +requests>=2.5,<2.11.0 From 00fb7f708f51b8ae4ee6ac8fb8e875bf961fa00b Mon Sep 17 00:00:00 2001 From: Dan Stace Date: Thu, 11 Aug 2016 16:19:46 -0400 Subject: [PATCH 117/224] Fix UnicodeDecodeError under PY3. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d6d46a2..4121bb3 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python from setuptools import setup -import os, sys +import os, sys, codecs try: # nose uses multiprocessing if available. @@ -30,7 +30,7 @@ name="keen", version="0.3.24", description="Python Client for Keen IO", - long_description=open(os.path.join('README.rst'), 'r').read(), + long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", author_email="team@keen.io", url="https://github.com/keenlabs/KeenClient-Python", From 7f04b020a97ee3d6736af7ea529e265e24e8df27 Mon Sep 17 00:00:00 2001 From: Daniel Kador Date: Mon, 15 Aug 2016 10:46:38 -0700 Subject: [PATCH 118/224] version 0.3.25 --- README.rst | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1c42e88..14753b5 100644 --- a/README.rst +++ b/README.rst @@ -313,6 +313,12 @@ To run tests: Changelog --------- +0.3.25 +`````` + ++ Replaced defunct `pycrypto` library with `cryptodome`. ++ Fixed UnicodeDecodeError under PY3 while installing in Windows. + 0.3.24 `````` diff --git a/setup.py b/setup.py index 4121bb3..6cf75ed 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.24", + version="0.3.25", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", From a01accba5cbe8b26f0f7aa8902fab61680da4b54 Mon Sep 17 00:00:00 2001 From: Ivo Rothschild Date: Tue, 30 Aug 2016 12:30:01 -0400 Subject: [PATCH 119/224] Removed Padding from requirements.txt. --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 10fd324..54ff87c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -Padding>=0.4 pycryptodome>=3.4 requests>=2.5,<2.11.0 From 6d73571ea5252c8814b9a43a9bdbaf7f8837bbba Mon Sep 17 00:00:00 2001 From: Daniel Kador Date: Wed, 31 Aug 2016 15:04:41 -0700 Subject: [PATCH 120/224] version 0.3.26 --- README.rst | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 14753b5..93a7854 100644 --- a/README.rst +++ b/README.rst @@ -313,6 +313,11 @@ To run tests: Changelog --------- +0.3.26 +`````` + ++ Removed unused `Padding` from requirements.txt to make python 3.x installs cleaner. + 0.3.25 `````` diff --git a/setup.py b/setup.py index 6cf75ed..bfb1e0c 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.25", + version="0.3.26", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", From 9d301751c9506e17e8d40d0fcaaaaf75cce7156f Mon Sep 17 00:00:00 2001 From: Daniel Kador Date: Mon, 28 Nov 2016 15:54:32 -0800 Subject: [PATCH 121/224] return JSON from event batch upload --- README.rst | 5 +++++ keen/__init__.py | 2 +- keen/api.py | 25 ++++++++++++++++++------- keen/client.py | 2 +- keen/persistence_strategies.py | 2 +- keen/tests/client_tests.py | 23 +++++++++++++++++++++-- setup.py | 2 +- 7 files changed, 48 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index 93a7854..445768e 100644 --- a/README.rst +++ b/README.rst @@ -313,6 +313,11 @@ To run tests: Changelog --------- +0.3.27 +`````` + ++ Return JSON response when uploading events in a batch. + 0.3.26 `````` diff --git a/keen/__init__.py b/keen/__init__.py index 35cba07..c487f5e 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -60,7 +60,7 @@ def add_events(events): :param events: dictionary of events """ _initialize_client_from_environment() - _client.add_events(events) + return _client.add_events(events) def generate_image_beacon(event_collection, body, timestamp=None): diff --git a/keen/api.py b/keen/api.py index 1f24473..106486b 100644 --- a/keen/api.py +++ b/keen/api.py @@ -131,6 +131,7 @@ def post_events(self, events): payload = json.dumps(events) response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self._error_handling(response) + return self._get_response_json(response) def query(self, analysis_type, params, all_keys=False): """ @@ -214,15 +215,25 @@ def _error_handling(self, res): """ # making the error handling generic so if an status_code starting with 2 doesn't exist, we raise the error if res.status_code // 100 != 2: - try: - error = res.json() - except ValueError: - error = { - "message": "The API did not respond with JSON, but: {0}".format(res.text[:1000]), - "error_code": "{0}".format(res.status_code) - } + error = self._get_response_json(res) raise exceptions.KeenApiError(error) + def _get_response_json(self, res): + """ + Helper function to extract the JSON body out of a response OR throw an exception. + + :param res: the response from a request + :return: the JSON body OR throws an exception + """ + try: + error = res.json() + except ValueError: + error = { + "message": "The API did not respond with JSON, but: {0}".format(res.text[:1000]), + "error_code": "{0}".format(res.status_code) + } + return error + def _create_session(self): """ Build a session that uses KeenAdapter for SSL """ diff --git a/keen/client.py b/keen/client.py index bba952f..cda21b0 100644 --- a/keen/client.py +++ b/keen/client.py @@ -141,7 +141,7 @@ def add_events(self, events): :param events: dictionary of events """ - self.persistence_strategy.batch_persist(events) + return self.persistence_strategy.batch_persist(events) def generate_image_beacon(self, event_collection, event_body, timestamp=None): """ Generates an image beacon URL. diff --git a/keen/persistence_strategies.py b/keen/persistence_strategies.py index d46cbbc..48f2aca 100644 --- a/keen/persistence_strategies.py +++ b/keen/persistence_strategies.py @@ -41,7 +41,7 @@ def batch_persist(self, events): :param events: a batch of events to persist """ - self.api.post_events(events) + return self.api.post_events(events) class RedisPersistenceStrategy(BasePersistenceStrategy): diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 5d7ab32..c2562b3 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -39,7 +39,9 @@ class ClientTests(BaseTestCase): SINGLE_ADD_RESPONSE = MockedResponse(status_code=201, json_response={"result": {"hello": "goodbye"}}) - MULTI_ADD_RESPONSE = MockedResponse(status_code=200, json_response={"result": {"hello": "goodbye"}}) + MULTI_ADD_RESPONSE = MockedResponse(status_code=200, json_response={"collection_a": {"success": True}}) + MULTI_ADD_RESPONSE_FAILURE = MockedResponse(status_code=200, json_response={"collection_a": {"success": False, + "error": "message"}}) def setUp(self): super(ClientTests, self).setUp() @@ -99,7 +101,7 @@ def test_direct_persistence_strategy(self, post): keen.add_event("python_test", {"hello": "goodbye"}) post.return_value = self.MULTI_ADD_RESPONSE - keen.add_events( + response = keen.add_events( { "sign_ups": [{ "username": "timmy", @@ -112,6 +114,23 @@ def test_direct_persistence_strategy(self, post): {"price": 7} ]} ) + self.assertEqual(self.MULTI_ADD_RESPONSE.json_response, response) + + post.return_value = self.MULTI_ADD_RESPONSE_FAILURE + response = keen.add_events( + { + "sign_ups": [{ + "username": "timmy", + "referred_by": "steve", + "son_of": "my_mom" + }], + "purchases": [ + {"price": 5}, + {"price": 6}, + {"price": 7} + ]} + ) + self.assertEqual(self.MULTI_ADD_RESPONSE_FAILURE.json_response, response) def test_module_level_add_event(self, post): post.return_value = self.SINGLE_ADD_RESPONSE diff --git a/setup.py b/setup.py index bfb1e0c..55701f2 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.26", + version="0.3.27", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", From 6e5996d1bb76f913dd51d74024b5cc1a2e2e98cd Mon Sep 17 00:00:00 2001 From: Daniel Kador Date: Mon, 28 Nov 2016 16:10:07 -0800 Subject: [PATCH 122/224] update README to include how to handle batch upload errors --- README.rst | 56 ++++++++++++++++++++++++++++++++++++++ keen/tests/client_tests.py | 6 ++-- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 445768e..384c44e 100644 --- a/README.rst +++ b/README.rst @@ -144,6 +144,62 @@ Advanced Usage See below for more options. +Check Batch Upload Response For Errors +'''''''''''''''''''''''''''''''''''''' + +When you upload events in a batch, some of them may succeed and some of them may have errors. The Keen API returns information on each. Here's an example: + +Upload code (remember, Keen IO doesn't allow periods in property names): + +.. code-block:: python + response = keen.add_events({ + "sign_ups": [ + { "username": "nameuser1" }, + { "username": "nameuser2", "an.invalid.property.name": 1 } + ], + "purchases": [ + { "price": 5 }, + { "price": 6 } + ] + }) + +That code would result in the following API JSON response: + +.. code-block:: javascript + { + "sign_ups": [ + {"success": true}, + {"success": false, "error": {"name": "some_error_name", "description": "some longer description"}} + ], + "purchases": [ + {"success": true}, + {"success": true} + ] + } + +So in python, to check on the results of your batch, you'd have code like so: + +.. code-block:: python + batch = { + "sign_ups": [ + { "username": "nameuser1" }, + { "username": "nameuser2", "an.invalid.property.name": 1 } + ], + "purchases": [ + { "price": 5 }, + { "price": 6 } + ] + } + response = keen.add_events(batch) + + for collection in response: + collection_result = response[collection] + event_count = 0 + for individual_result in collection_result: + if not individual_result["success"]: + print("Event had error! Collection: '{}'. Event body: '{}'.".format(collection, batch[collection][event_count])) + event_count += 1 + Configure Unique Client Instances ''''''''''''''''''''''''''''''''' diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index c2562b3..88ce746 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -39,9 +39,9 @@ class ClientTests(BaseTestCase): SINGLE_ADD_RESPONSE = MockedResponse(status_code=201, json_response={"result": {"hello": "goodbye"}}) - MULTI_ADD_RESPONSE = MockedResponse(status_code=200, json_response={"collection_a": {"success": True}}) - MULTI_ADD_RESPONSE_FAILURE = MockedResponse(status_code=200, json_response={"collection_a": {"success": False, - "error": "message"}}) + MULTI_ADD_RESPONSE = MockedResponse(status_code=200, json_response={"collection_a": [{"success": True}]}) + MULTI_ADD_RESPONSE_FAILURE = MockedResponse(status_code=200, json_response={"collection_a": [{"success": False, + "error": "message"}]}) def setUp(self): super(ClientTests, self).setUp() From f795f845da92e4fd7cb0fdbc0dd31d55f5cd27f1 Mon Sep 17 00:00:00 2001 From: Daniel Kador Date: Tue, 29 Nov 2016 09:41:37 -0800 Subject: [PATCH 123/224] fix bad code blocks in README --- README.rst | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 384c44e..d0a67cb 100644 --- a/README.rst +++ b/README.rst @@ -152,6 +152,7 @@ When you upload events in a batch, some of them may succeed and some of them may Upload code (remember, Keen IO doesn't allow periods in property names): .. code-block:: python + response = keen.add_events({ "sign_ups": [ { "username": "nameuser1" }, @@ -166,6 +167,7 @@ Upload code (remember, Keen IO doesn't allow periods in property names): That code would result in the following API JSON response: .. code-block:: javascript + { "sign_ups": [ {"success": true}, @@ -180,6 +182,7 @@ That code would result in the following API JSON response: So in python, to check on the results of your batch, you'd have code like so: .. code-block:: python + batch = { "sign_ups": [ { "username": "nameuser1" }, @@ -369,6 +372,11 @@ To run tests: Changelog --------- +0.3.28 +`````` + ++ Fix incorrect README. + 0.3.27 `````` diff --git a/setup.py b/setup.py index 55701f2..5a2437a 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ setup( name="keen", - version="0.3.27", + version="0.3.28", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", From 16fe2863fc41d782676d60031bef57dc14c8557b Mon Sep 17 00:00:00 2001 From: Joe Wegner Date: Wed, 15 Feb 2017 10:55:17 -0600 Subject: [PATCH 124/224] move the version to a separate file, so we can put it in the header --- VERSION | 1 + keen/__init__.py | 1 - keen/api.py | 16 ++++++++-------- keen/saved_queries.py | 15 ++++++--------- keen/utilities.py | 25 +++++++++++++++++++++++++ setup.py | 5 ++++- 6 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 VERSION create mode 100644 keen/utilities.py diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..647b7a1 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.3.29 \ No newline at end of file diff --git a/keen/__init__.py b/keen/__init__.py index c487f5e..3f269c5 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -11,7 +11,6 @@ master_key = None base_url = None - def _initialize_client_from_environment(): ''' Initialize a KeenCLient instance using environment variables. ''' global _client, project_id, write_key, read_key, master_key, base_url diff --git a/keen/api.py b/keen/api.py index 106486b..d1672ad 100644 --- a/keen/api.py +++ b/keen/api.py @@ -7,8 +7,8 @@ from requests.adapters import HTTPAdapter from requests.packages.urllib3.poolmanager import PoolManager -# keen exceptions -from keen import exceptions +# keen +from keen import exceptions, utilities # json from requests.compat import json @@ -106,7 +106,7 @@ def post_event(self, event): url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, self.project_id, event.event_collection) - headers = {"Content-Type": "application/json", "Authorization": self.write_key} + headers = utilities.headers(self.write_key) payload = event.to_json() response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self._error_handling(response) @@ -127,7 +127,7 @@ def post_events(self, events): url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, self.project_id) - headers = {"Content-Type": "application/json", "Authorization": self.write_key} + headers = utilities.headers(self.write_key) payload = json.dumps(events) response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self._error_handling(response) @@ -148,7 +148,7 @@ def query(self, analysis_type, params, all_keys=False): url = "{0}/{1}/projects/{2}/queries/{3}".format(self.base_url, self.api_version, self.project_id, analysis_type) - headers = {"Authorization": self.read_key} + headers = utilities.headers(self.read_key) payload = params response = self.fulfill(HTTPMethods.GET, url, params=payload, headers=headers, timeout=self.get_timeout) self._error_handling(response) @@ -173,7 +173,7 @@ def delete_events(self, event_collection, params): self.api_version, self.project_id, event_collection) - headers = {"Content-Type": "application/json", "Authorization": self.master_key} + headers = utilities.headers(self.master_key) response = self.fulfill(HTTPMethods.DELETE, url, params=params, headers=headers, timeout=self.post_timeout) self._error_handling(response) @@ -188,7 +188,7 @@ def get_collection(self, event_collection): self._check_for_master_key() url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, self.project_id, event_collection) - headers = {"Authorization": self.master_key} + headers = utilities.headers(self.master_key) response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) self._error_handling(response) @@ -201,7 +201,7 @@ def get_all_collections(self): """ self._check_for_master_key() url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, self.project_id) - headers = {"Authorization": self.master_key} + headers = utilities.headers(self.master_key) response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) self._error_handling(response) diff --git a/keen/saved_queries.py b/keen/saved_queries.py index a343ad0..5885e49 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -1,5 +1,5 @@ from keen.api import KeenApi -from keen import exceptions +from keen import exceptions, utilities class SavedQueriesInterface: @@ -18,7 +18,7 @@ def all(self): url = "{0}/{1}/projects/{2}/queries/saved".format( keen_api.base_url, keen_api.api_version, self.project_id ) - response = keen_api.fulfill("get", url, headers=self._headers()) + response = keen_api.fulfill("get", url, headers=utilities.headers(self.master_key)) return response.json() @@ -33,7 +33,7 @@ def get(self, query_name): url = "{0}/{1}/projects/{2}/queries/saved/{3}".format( keen_api.base_url, keen_api.api_version, self.project_id, query_name ) - response = keen_api.fulfill("get", url, headers=self._headers()) + response = keen_api.fulfill("get", url, headers=utilities.headers(self.master_key)) keen_api._error_handling(response) return response.json() @@ -50,7 +50,7 @@ def results(self, query_name): keen_api.base_url, keen_api.api_version, self.project_id, query_name ) key = self.master_key if self.master_key else self.read_key - response = keen_api.fulfill("get", url, headers={"Authorization": key }) + response = keen_api.fulfill("get", url, headers=utilities.headers(key)) keen_api._error_handling(response) return response.json() @@ -65,7 +65,7 @@ def create(self, query_name, saved_query): keen_api.base_url, keen_api.api_version, self.project_id, query_name ) response = keen_api.fulfill( - "put", url, headers=self._headers(), data=saved_query + "put", url, headers=utilities.headers(self.master_key), data=saved_query ) keen_api._error_handling(response) @@ -89,14 +89,11 @@ def delete(self, query_name): url = "{0}/{1}/projects/{2}/queries/saved/{3}".format( keen_api.base_url, keen_api.api_version, self.project_id, query_name ) - response = keen_api.fulfill("delete", url, headers=self._headers()) + response = keen_api.fulfill("delete", url, headers=utilities.headers(self.master_key)) keen_api._error_handling(response) return True - def _headers(self): - return {"Authorization": self.master_key} - def _check_for_master_key(self): if not self.master_key: raise exceptions.InvalidEnvironmentError( diff --git a/keen/utilities.py b/keen/utilities.py new file mode 100644 index 0000000..076aee2 --- /dev/null +++ b/keen/utilities.py @@ -0,0 +1,25 @@ +VERSION = None + +def version(): + """ + Retrives the current version of the SDK + """ + + if VERSION is None: + version_file = open(os.path.join('.', 'VERSION')) + VERSION = version_file.read().strip() + + return VERSION + +def headers(api_key): + """ + Helper function to easily get the correct headers for an endpoint + + :params api_key: The appropriate API key for the request being made + """ + + return { + "Content-Type": "application/json", + "Authorization": api_key, + "X-Keensdkversion-X": "python-#{0}".format(version) + } \ No newline at end of file diff --git a/setup.py b/setup.py index 5a2437a..d8ccfcc 100644 --- a/setup.py +++ b/setup.py @@ -26,9 +26,12 @@ if sys.version_info < (2, 7): tests_require.append('unittest2') +version_file = open(os.path.join('.', 'VERSION')) +version = version_file.read().strip() + setup( name="keen", - version="0.3.28", + version=version, description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", From 97da9681149fa1f51954f754b0771afc02239174 Mon Sep 17 00:00:00 2001 From: Joe Wegner Date: Wed, 15 Feb 2017 11:21:37 -0600 Subject: [PATCH 125/224] fix some issues with the version utilities --- keen/utilities.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/keen/utilities.py b/keen/utilities.py index 076aee2..07392b0 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -1,3 +1,5 @@ +import os + VERSION = None def version(): @@ -5,6 +7,7 @@ def version(): Retrives the current version of the SDK """ + global VERSION if VERSION is None: version_file = open(os.path.join('.', 'VERSION')) VERSION = version_file.read().strip() @@ -21,5 +24,5 @@ def headers(api_key): return { "Content-Type": "application/json", "Authorization": api_key, - "X-Keensdkversion-X": "python-#{0}".format(version) + "X-Keensdkversion-X": "python-{0}".format(version()) } \ No newline at end of file From 52f5f9b79e0bcfceb0ce2cad5951e5d781b5f26f Mon Sep 17 00:00:00 2001 From: Joe Wegner Date: Fri, 17 Feb 2017 08:35:51 -0600 Subject: [PATCH 126/224] change header name --- keen/utilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keen/utilities.py b/keen/utilities.py index 07392b0..d80ffbb 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -24,5 +24,5 @@ def headers(api_key): return { "Content-Type": "application/json", "Authorization": api_key, - "X-Keensdkversion-X": "python-{0}".format(version()) - } \ No newline at end of file + "Keen-Sdk": "python-{0}".format(version()) + } From 431562eb46f3449103addc7d996a98727d465eb3 Mon Sep 17 00:00:00 2001 From: Joe Wegner Date: Wed, 22 Feb 2017 14:42:16 -0600 Subject: [PATCH 127/224] spell check, darnit --- keen/utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/utilities.py b/keen/utilities.py index d80ffbb..c2f4ab0 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -4,7 +4,7 @@ def version(): """ - Retrives the current version of the SDK + Retrieves the current version of the SDK """ global VERSION From 14a25c0de7585d928b5504267441de3fbc9ecffc Mon Sep 17 00:00:00 2001 From: Joe Wegner Date: Wed, 22 Feb 2017 14:43:11 -0600 Subject: [PATCH 128/224] spacing --- keen/utilities.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/keen/utilities.py b/keen/utilities.py index c2f4ab0..6c2e5f2 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -3,26 +3,26 @@ VERSION = None def version(): - """ - Retrieves the current version of the SDK - """ + """ + Retrieves the current version of the SDK + """ - global VERSION - if VERSION is None: + global VERSION + if VERSION is None: version_file = open(os.path.join('.', 'VERSION')) VERSION = version_file.read().strip() - return VERSION + return VERSION def headers(api_key): - """ - Helper function to easily get the correct headers for an endpoint + """ + Helper function to easily get the correct headers for an endpoint - :params api_key: The appropriate API key for the request being made - """ + :params api_key: The appropriate API key for the request being made + """ - return { + return { "Content-Type": "application/json", "Authorization": api_key, "Keen-Sdk": "python-{0}".format(version()) - } + } From 9a7849f9a1400d10907ccac3fa072c5d60452321 Mon Sep 17 00:00:00 2001 From: Joe Wegner Date: Wed, 22 Feb 2017 14:44:14 -0600 Subject: [PATCH 129/224] update changelog --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index d0a67cb..bad253e 100644 --- a/README.rst +++ b/README.rst @@ -372,6 +372,11 @@ To run tests: Changelog --------- +0.3.29 +`````` + ++ Add Keen-Sdk header to all requests, containing the SDK version + 0.3.28 `````` From 3e9e0d72f19b26f919428786abf2e157d23bcee5 Mon Sep 17 00:00:00 2001 From: Joe Wegner Date: Wed, 22 Feb 2017 15:46:51 -0600 Subject: [PATCH 130/224] Fix indentnation on block --- keen/utilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keen/utilities.py b/keen/utilities.py index 6c2e5f2..05c9b7d 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -9,8 +9,8 @@ def version(): global VERSION if VERSION is None: - version_file = open(os.path.join('.', 'VERSION')) - VERSION = version_file.read().strip() + version_file = open(os.path.join('.', 'VERSION')) + VERSION = version_file.read().strip() return VERSION From aeb10c5cff0056b0cc0ac6bea1a73ba59caef227 Mon Sep 17 00:00:00 2001 From: Bartek Ogryczak Date: Thu, 23 Feb 2017 10:49:25 -0800 Subject: [PATCH 131/224] Adding missing `VERSION` file to sdist This fixes following error when building from source distribution ``` Traceback (most recent call last): File "", line 1, in File "/private/var/folders/ch/72dmd7t14cq6nr3gp86qync40000gp/T/pip-build-jncV6o/keen/setup.py", line 29, in version_file = open(os.path.join('.', 'VERSION')) IOError: [Errno 2] No such file or directory: './VERSION' ``` --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index ccf2a9b..51999c7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ include requirements.txt include README.md include LICENSE.txt +include VERSION From 471a470cc446d3482d949ef884b52f4bfdd8376c Mon Sep 17 00:00:00 2001 From: Daniel Kador Date: Thu, 23 Feb 2017 11:38:22 -0800 Subject: [PATCH 132/224] release 0.3.30 to fix 0.3.29 build problems --- LICENSE.txt | 2 +- README.rst | 7 ++++++- VERSION | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index c47a4c0..a656355 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2013 Keen Labs +Copyright (c) 2017 Keen Labs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.rst b/README.rst index bad253e..e2fa3b9 100644 --- a/README.rst +++ b/README.rst @@ -372,10 +372,15 @@ To run tests: Changelog --------- +0.3.30 +`````` + ++ Fix broken 0.3.29 release. + 0.3.29 `````` -+ Add Keen-Sdk header to all requests, containing the SDK version ++ Add Keen-Sdk header to all requests, containing the SDK version. 0.3.28 `````` diff --git a/VERSION b/VERSION index 647b7a1..d8c672f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.29 \ No newline at end of file +0.3.30 \ No newline at end of file From 2acfdc2fa66548535bcef79be3b200ea8105d2ab Mon Sep 17 00:00:00 2001 From: Keen IO Build User Date: Mon, 6 Mar 2017 15:30:44 -0800 Subject: [PATCH 133/224] bump to 0.3.31 and remove VERSION file --- MANIFEST.in | 1 - README.rst | 4 ++-- keen/utilities.py | 16 +++++----------- setup.py | 5 +---- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 51999c7..ccf2a9b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,3 @@ include requirements.txt include README.md include LICENSE.txt -include VERSION diff --git a/README.rst b/README.rst index e2fa3b9..068be9e 100644 --- a/README.rst +++ b/README.rst @@ -372,10 +372,10 @@ To run tests: Changelog --------- -0.3.30 +0.3.31 `````` -+ Fix broken 0.3.29 release. ++ Fix broken releases. 0.3.29 `````` diff --git a/keen/utilities.py b/keen/utilities.py index 05c9b7d..2221886 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -1,19 +1,13 @@ -import os +VERSION = "0.3.31" -VERSION = None def version(): """ Retrieves the current version of the SDK """ - - global VERSION - if VERSION is None: - version_file = open(os.path.join('.', 'VERSION')) - VERSION = version_file.read().strip() - return VERSION + def headers(api_key): """ Helper function to easily get the correct headers for an endpoint @@ -22,7 +16,7 @@ def headers(api_key): """ return { - "Content-Type": "application/json", - "Authorization": api_key, - "Keen-Sdk": "python-{0}".format(version()) + "Content-Type": "application/json", + "Authorization": api_key, + "Keen-Sdk": "python-{0}".format(version()) } diff --git a/setup.py b/setup.py index d8ccfcc..d6cb442 100644 --- a/setup.py +++ b/setup.py @@ -26,12 +26,9 @@ if sys.version_info < (2, 7): tests_require.append('unittest2') -version_file = open(os.path.join('.', 'VERSION')) -version = version_file.read().strip() - setup( name="keen", - version=version, + version="0.3.31", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", From d273e30c264bc831f9410bdb51b269a6f815ff8c Mon Sep 17 00:00:00 2001 From: Keen IO Build User Date: Mon, 6 Mar 2017 15:38:15 -0800 Subject: [PATCH 134/224] remove VERSION file as it is extraneous --- VERSION | 1 - 1 file changed, 1 deletion(-) delete mode 100644 VERSION diff --git a/VERSION b/VERSION deleted file mode 100644 index d8c672f..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.3.30 \ No newline at end of file From a8cad5d618884874652a8096503bce8634f62f85 Mon Sep 17 00:00:00 2001 From: Justin Date: Sat, 11 Mar 2017 23:43:00 -0500 Subject: [PATCH 135/224] Accept a dict for the saved_query properties, and convert to a json-formatted str to send as the PUT payload. --- keen/saved_queries.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/keen/saved_queries.py b/keen/saved_queries.py index 5885e49..040abb5 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -1,3 +1,6 @@ + +import json + from keen.api import KeenApi from keen import exceptions, utilities @@ -40,7 +43,7 @@ def get(self, query_name): def results(self, query_name): """ - Gets a single saved query with a 'result' object for a project from thei + Gets a single saved query with a 'result' object for a project from the Keen IO API given a query name. Read or Master key must be set. """ @@ -57,15 +60,18 @@ def results(self, query_name): def create(self, query_name, saved_query): """ - Creates the saved query via a PUT request to Keen IO Saved Query endpoint. Master key must be set. + Creates the saved query via a PUT request to Keen IO Saved Query endpoint. + Master key must be set. """ keen_api = KeenApi(self.project_id, master_key=self.master_key) self._check_for_master_key() url = "{0}/{1}/projects/{2}/queries/saved/{3}".format( keen_api.base_url, keen_api.api_version, self.project_id, query_name ) + + payload = json.dumps(saved_query) response = keen_api.fulfill( - "put", url, headers=utilities.headers(self.master_key), data=saved_query + "put", url, headers=utilities.headers(self.master_key), data=payload ) keen_api._error_handling(response) From 5c132ab23b051949b1ed05270a83c32a0e0b7a24 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 14 Mar 2017 00:42:43 -0500 Subject: [PATCH 136/224] Whitespace as per Codacy. --- keen/saved_queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/saved_queries.py b/keen/saved_queries.py index 040abb5..ad558f3 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -68,7 +68,7 @@ def create(self, query_name, saved_query): url = "{0}/{1}/projects/{2}/queries/saved/{3}".format( keen_api.base_url, keen_api.api_version, self.project_id, query_name ) - + payload = json.dumps(saved_query) response = keen_api.fulfill( "put", url, headers=utilities.headers(self.master_key), data=payload From 4a5e51f57f2d04db789d202bb729d335826345ca Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 14 Mar 2017 02:42:22 -0500 Subject: [PATCH 137/224] Clean up saved_queries code a bit. - Don't create a KeenApi instance for each method call--just use the one the KeenClient already created since this SavedQueriesInterface is bound to that KeenClient instance anyway. - Consolidate the dispatch of the actual HTTP request, error handling and response gathering. - Construct and work off of the base url for Saved Queries. --- keen/api.py | 1 + keen/client.py | 2 +- keen/saved_queries.py | 75 ++++++++++++++++++------------------------- 3 files changed, 34 insertions(+), 44 deletions(-) diff --git a/keen/api.py b/keen/api.py index d1672ad..2f69a18 100644 --- a/keen/api.py +++ b/keen/api.py @@ -24,6 +24,7 @@ class HTTPMethods(object): GET = 'get' POST = 'post' DELETE = 'delete' + PUT = 'put' class KeenAdapter(HTTPAdapter): diff --git a/keen/client.py b/keen/client.py index cda21b0..24fddad 100644 --- a/keen/client.py +++ b/keen/client.py @@ -97,7 +97,7 @@ def __init__(self, project_id, write_key=None, read_key=None, self.persistence_strategy = persistence_strategy self.get_timeout = get_timeout self.post_timeout = post_timeout - self.saved_queries = saved_queries.SavedQueriesInterface(project_id, master_key, read_key) + self.saved_queries = saved_queries.SavedQueriesInterface(self.api) if sys.version_info[0] < 3: @staticmethod diff --git a/keen/saved_queries.py b/keen/saved_queries.py index ad558f3..c857789 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -1,29 +1,27 @@ import json -from keen.api import KeenApi +from keen.api import KeenApi, HTTPMethods from keen import exceptions, utilities class SavedQueriesInterface: - def __init__(self, project_id, master_key, read_key): - self.project_id = project_id - self.master_key = master_key - self.read_key = read_key + def __init__(self, api): + self.api = api + self.saved_query_url = "{0}/{1}/projects/{2}/queries/saved".format( + self.api.base_url, self.api.api_version, self.api.project_id + ) def all(self): """ Gets all saved queries for a project from the Keen IO API. Master key must be set. """ - keen_api = KeenApi(self.project_id, master_key=self.master_key) self._check_for_master_key() - url = "{0}/{1}/projects/{2}/queries/saved".format( - keen_api.base_url, keen_api.api_version, self.project_id - ) - response = keen_api.fulfill("get", url, headers=utilities.headers(self.master_key)) - return response.json() + response = self._get_json(HTTPMethods.GET, self.saved_query_url, self.api.master_key) + + return response def get(self, query_name): """ @@ -31,15 +29,12 @@ def get(self, query_name): query name. Master key must be set. """ - keen_api = KeenApi(self.project_id, master_key=self.master_key) self._check_for_master_key() - url = "{0}/{1}/projects/{2}/queries/saved/{3}".format( - keen_api.base_url, keen_api.api_version, self.project_id, query_name - ) - response = keen_api.fulfill("get", url, headers=utilities.headers(self.master_key)) - keen_api._error_handling(response) - return response.json() + url = "{0}/{1}".format(self.saved_query_url, query_name) + response = self._get_json(HTTPMethods.GET, url, self.api.master_key) + + return response def results(self, query_name): """ @@ -47,35 +42,26 @@ def results(self, query_name): Keen IO API given a query name. Read or Master key must be set. """ - keen_api = KeenApi(self.project_id, master_key=self.master_key) self._check_for_master_or_read_key() - url = "{0}/{1}/projects/{2}/queries/saved/{3}/result".format( - keen_api.base_url, keen_api.api_version, self.project_id, query_name - ) - key = self.master_key if self.master_key else self.read_key - response = keen_api.fulfill("get", url, headers=utilities.headers(key)) - keen_api._error_handling(response) - return response.json() + url = "{0}/{1}/result".format(self.saved_query_url, query_name) + key = self.api.master_key if self.api.master_key else self.api.read_key + response = self._get_json(HTTPMethods.GET, url, key) + + return response def create(self, query_name, saved_query): """ Creates the saved query via a PUT request to Keen IO Saved Query endpoint. Master key must be set. """ - keen_api = KeenApi(self.project_id, master_key=self.master_key) self._check_for_master_key() - url = "{0}/{1}/projects/{2}/queries/saved/{3}".format( - keen_api.base_url, keen_api.api_version, self.project_id, query_name - ) + url = "{0}/{1}".format(self.saved_query_url, query_name) payload = json.dumps(saved_query) - response = keen_api.fulfill( - "put", url, headers=utilities.headers(self.master_key), data=payload - ) - keen_api._error_handling(response) + response = self._get_json(HTTPMethods.PUT, url, self.api.master_key, data=payload) - return response.json() + return response def update(self, query_name, saved_query): """ @@ -90,18 +76,21 @@ def delete(self, query_name): Deletes a saved query from a project with a query name. Master key must be set. """ - keen_api = KeenApi(self.project_id, master_key=self.master_key) self._check_for_master_key() - url = "{0}/{1}/projects/{2}/queries/saved/{3}".format( - keen_api.base_url, keen_api.api_version, self.project_id, query_name - ) - response = keen_api.fulfill("delete", url, headers=utilities.headers(self.master_key)) - keen_api._error_handling(response) + + url = "{0}/{1}".format(self.saved_query_url, query_name) + response = self._get_json(HTTPMethods.DELETE, url, self.api.master_key) return True + def _get_json(self, http_method, url, key, *args, **kwargs): + response = self.api.fulfill(http_method, url, headers=utilities.headers(key), *args, **kwargs) + self.api._error_handling(response) + + return response.json() + def _check_for_master_key(self): - if not self.master_key: + if not self.api.master_key: raise exceptions.InvalidEnvironmentError( "The Keen IO API requires a master key to perform this operation on saved queries. " "Please set a 'master_key' when initializing the " @@ -109,7 +98,7 @@ def _check_for_master_key(self): ) def _check_for_master_or_read_key(self): - if not (self.read_key or self.master_key): + if not (self.api.read_key or self.api.master_key): raise exceptions.InvalidEnvironmentError( "The Keen IO API requires a read key or master key to perform this operation on saved queries. " "Please set a 'read_key' or 'master_key' when initializing the " From 5499531c3952013432bc06f3b5d1b4012800f24a Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 14 Mar 2017 05:34:53 -0500 Subject: [PATCH 138/224] Clean up Saved Queries API and share some code with the rest of the SDK too. - There's more that can be done here, but I didn't want to mess more with the core api.py without more test coverage. --- keen/api.py | 48 +++++++++------------ keen/saved_queries.py | 54 ++++++++++++------------ keen/tests/client_tests.py | 8 +++- keen/tests/saved_query_tests.py | 1 + keen/utilities.py | 74 ++++++++++++++++++++++++++++++++- 5 files changed, 127 insertions(+), 58 deletions(-) diff --git a/keen/api.py b/keen/api.py index 2f69a18..235ac50 100644 --- a/keen/api.py +++ b/keen/api.py @@ -9,6 +9,7 @@ # keen from keen import exceptions, utilities +from keen.utilities import KeenKeys, requires_key # json from requests.compat import json @@ -49,6 +50,7 @@ class KeenApi(object): # the default base URL of the Keen API base_url = "https://api.keen.io" + # the default version of the Keen API api_version = "3.0" @@ -91,18 +93,13 @@ def fulfill(self, method, *args, **kwargs): return getattr(self.session, method)(*args, **kwargs) + @requires_key(KeenKeys.WRITE) def post_event(self, event): """ Posts a single event to the Keen IO API. The write key must be set first. :param event: an Event to upload """ - if not self.write_key: - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a write key to send events. " - "Please set a 'write_key' when initializing the " - "KeenApi object." - ) url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, self.project_id, @@ -112,6 +109,7 @@ def post_event(self, event): response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.post_timeout) self._error_handling(response) + @requires_key(KeenKeys.WRITE) def post_events(self, events): """ @@ -119,12 +117,6 @@ def post_events(self, events): :param events: an Event to upload """ - if not self.write_key: - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a write key to send events. " - "Please set a 'write_key' when initializing the " - "KeenApi object." - ) url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, self.project_id) @@ -134,17 +126,12 @@ def post_events(self, events): self._error_handling(response) return self._get_response_json(response) + @requires_key(KeenKeys.READ) def query(self, analysis_type, params, all_keys=False): """ Performs a query using the Keen IO analysis API. A read key must be set first. """ - if not self.read_key: - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a read key to perform queries. " - "Please set a 'read_key' when initializing the " - "KeenApi object." - ) url = "{0}/{1}/projects/{2}/queries/{3}".format(self.base_url, self.api_version, self.project_id, analysis_type) @@ -161,6 +148,7 @@ def query(self, analysis_type, params, all_keys=False): return response + @requires_key(KeenKeys.MASTER) def delete_events(self, event_collection, params): """ Deletes events via the Keen IO API. A master key must be set first. @@ -168,7 +156,6 @@ def delete_events(self, event_collection, params): :param event_collection: string, the event collection from which event are being deleted """ - self._check_for_master_key() url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, @@ -180,13 +167,14 @@ def delete_events(self, event_collection, params): self._error_handling(response) return True + @requires_key(KeenKeys.READ) def get_collection(self, event_collection): """ Extracts info about a collection using the Keen IO API. A master key must be set first. :param event_collection: the name of the collection to retrieve info for """ - self._check_for_master_key() + url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, self.project_id, event_collection) headers = utilities.headers(self.master_key) @@ -195,12 +183,13 @@ def get_collection(self, event_collection): return response.json() + @requires_key(KeenKeys.READ) def get_all_collections(self): """ Extracts schema for all collections using the Keen IO API. A master key must be set first. """ - self._check_for_master_key() + url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, self.project_id) headers = utilities.headers(self.master_key) response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) @@ -214,6 +203,7 @@ def _error_handling(self, res): :params res: the response from a request """ + # making the error handling generic so if an status_code starting with 2 doesn't exist, we raise the error if res.status_code // 100 != 2: error = self._get_response_json(res) @@ -226,6 +216,7 @@ def _get_response_json(self, res): :param res: the response from a request :return: the JSON body OR throws an exception """ + try: error = res.json() except ValueError: @@ -243,10 +234,11 @@ def _create_session(self): s.mount('https://', KeenAdapter()) return s - def _check_for_master_key(self): - if not self.master_key: - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a master key to perform this operation. " - "Please set a 'master_key' when initializing the " - "KeenApi object." - ) + def _get_read_key(self): + return self.read_key + + def _get_write_key(self): + return self.write_key + + def _get_master_key(self): + return self.master_key \ No newline at end of file diff --git a/keen/saved_queries.py b/keen/saved_queries.py index c857789..50c86f7 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -3,6 +3,8 @@ from keen.api import KeenApi, HTTPMethods from keen import exceptions, utilities +from keen.utilities import KeenKeys, requires_key + class SavedQueriesInterface: @@ -12,74 +14,75 @@ def __init__(self, api): self.api.base_url, self.api.api_version, self.api.project_id ) + @requires_key(KeenKeys.MASTER) def all(self): """ Gets all saved queries for a project from the Keen IO API. Master key must be set. """ - self._check_for_master_key() - response = self._get_json(HTTPMethods.GET, self.saved_query_url, self.api.master_key) + response = self._get_json(HTTPMethods.GET, self.saved_query_url, self._get_master_key()) return response + @requires_key(KeenKeys.MASTER) def get(self, query_name): """ Gets a single saved query for a project from the Keen IO API given a query name. Master key must be set. """ - self._check_for_master_key() url = "{0}/{1}".format(self.saved_query_url, query_name) - response = self._get_json(HTTPMethods.GET, url, self.api.master_key) + response = self._get_json(HTTPMethods.GET, url, self._get_master_key()) return response + @requires_key(KeenKeys.READ) def results(self, query_name): """ Gets a single saved query with a 'result' object for a project from the Keen IO API given a query name. Read or Master key must be set. """ - self._check_for_master_or_read_key() url = "{0}/{1}/result".format(self.saved_query_url, query_name) - key = self.api.master_key if self.api.master_key else self.api.read_key - response = self._get_json(HTTPMethods.GET, url, key) + response = self._get_json(HTTPMethods.GET, url, self._get_read_key()) return response + @requires_key(KeenKeys.MASTER) def create(self, query_name, saved_query): """ Creates the saved query via a PUT request to Keen IO Saved Query endpoint. Master key must be set. """ - self._check_for_master_key() url = "{0}/{1}".format(self.saved_query_url, query_name) payload = json.dumps(saved_query) - response = self._get_json(HTTPMethods.PUT, url, self.api.master_key, data=payload) + response = self._get_json(HTTPMethods.PUT, url, self._get_master_key(), data=payload) return response + @requires_key(KeenKeys.MASTER) def update(self, query_name, saved_query): """ Updates the saved query via a PUT request to Keen IO Saved Query endpoint. Master key must be set. """ + return self.create(query_name, saved_query) + @requires_key(KeenKeys.MASTER) def delete(self, query_name): """ Deletes a saved query from a project with a query name. Master key must be set. """ - self._check_for_master_key() url = "{0}/{1}".format(self.saved_query_url, query_name) - response = self._get_json(HTTPMethods.DELETE, url, self.api.master_key) + response = self._get_json(HTTPMethods.DELETE, url, self._get_master_key()) return True @@ -87,20 +90,15 @@ def _get_json(self, http_method, url, key, *args, **kwargs): response = self.api.fulfill(http_method, url, headers=utilities.headers(key), *args, **kwargs) self.api._error_handling(response) - return response.json() - - def _check_for_master_key(self): - if not self.api.master_key: - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a master key to perform this operation on saved queries. " - "Please set a 'master_key' when initializing the " - "KeenApi object." - ) - - def _check_for_master_or_read_key(self): - if not (self.api.read_key or self.api.master_key): - raise exceptions.InvalidEnvironmentError( - "The Keen IO API requires a read key or master key to perform this operation on saved queries. " - "Please set a 'read_key' or 'master_key' when initializing the " - "KeenApi object." - ) + try: + response = response.json() + except ValueError: + response = "No JSON available." + + return response + + def _get_read_key(self): + return self.api.read_key + + def _get_master_key(self): + return self.api.master_key diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 88ce746..18fa8b1 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -494,6 +494,8 @@ def test_interval(self, get): def test_passing_invalid_custom_api_client(self, get): class CustomApiClient(object): + api_version = "3.0" + def __init__(self, project_id, write_key=None, read_key=None, base_url=None, api_version=None, **kwargs): super(CustomApiClient, self).__init__() @@ -506,7 +508,11 @@ def __init__(self, project_id, write_key=None, read_key=None, self.api_version = api_version api_key = "2e79c6ec1d0145be8891bf668599c79a" - client = KeenClient("5004ded1163d66114f000000", write_key=scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}), read_key=scoped_keys.encrypt(api_key, {"allowed_operations": ["read"]}), api_class=CustomApiClient) + client = KeenClient("5004ded1163d66114f000000", + write_key=scoped_keys.encrypt(api_key, {"allowed_operations": ["write"]}), + read_key=scoped_keys.encrypt(api_key, {"allowed_operations": ["read"]}), + api_class=CustomApiClient, + base_url="keen.base.url") # Should raise an error, we never added this method on our class # But it shows it is actually using our class diff --git a/keen/tests/saved_query_tests.py b/keen/tests/saved_query_tests.py index 3d97663..ce125f2 100644 --- a/keen/tests/saved_query_tests.py +++ b/keen/tests/saved_query_tests.py @@ -12,6 +12,7 @@ def setUp(self): exp_master_key = "abcd3456" self.client = KeenClient( project_id=self.exp_project_id, + read_key="efgh7890", master_key=exp_master_key ) diff --git a/keen/utilities.py b/keen/utilities.py index 2221886..972554a 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -1,5 +1,11 @@ -VERSION = "0.3.31" +from functools import wraps + +# keen +from keen import exceptions + + +VERSION = "0.3.31" def version(): """ @@ -20,3 +26,69 @@ def headers(api_key): "Authorization": api_key, "Keen-Sdk": "python-{0}".format(version()) } + + +class KeenKeys(object): + """ Keen API key types. """ + + READ = 'read' + WRITE = 'write' + MASTER = 'master' + + +class switch(object): + """ Python switch recipe. """ + + def __init__(self, value): + self.value = value + self.fall = False + + def __iter__(self): + yield self.match + raise StopIteration + + def match(self, *args): + """Whether or not to enter a given case statement""" + + self.fall = self.fall or not args + self.fall = self.fall or (self.value in args) + + return self.fall + + +def _throw_key_missing(key, relying_on_master): + message = ("The Keen IO API requires a {0} key to perform queries. " + "Please set a '{0}_key' when initializing the " + "KeenApi object.") + + if relying_on_master: + message += ' The "master_key" is set, but one should prefer the key with least privilege.' + + raise exceptions.InvalidEnvironmentError(message.format(key)) + + +def requires_key(key_type): + def requires_key_decorator(func): + + @wraps(func) + def method_wrapper(self, *args, **kwargs): + for case in switch(key_type): + if case(KeenKeys.READ): + if not self._get_read_key(): + _throw_key_missing(KeenKeys.READ, bool(self._get_master_key())) + break + + if case(KeenKeys.WRITE): + if not self._get_write_key(): + _throw_key_missing(KeenKeys.WRITE, bool(self._get_master_key())) + break + + if case(KeenKeys.MASTER): + if not self._get_master_key(): + _throw_key_missing(KeenKeys.MASTER, False) + break + + return func(self, *args, **kwargs) + + return method_wrapper + return requires_key_decorator From 858bce2d356d0dec3996500ec2b7dc5c53cb5197 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 15 Mar 2017 15:38:13 -0500 Subject: [PATCH 139/224] Fix up some documentation bugs I've had jotted down for a while. - Also fixes Issue #106. --- README.rst | 90 ++++++++++++++++++++++++++++++++---------------- keen/__init__.py | 2 +- keen/client.py | 20 +++++------ 3 files changed, 71 insertions(+), 41 deletions(-) diff --git a/README.rst b/README.rst index 068be9e..adfc2d6 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,9 @@ keys (if you need an account, `sign up here `_ - it's free). Setting a write key is required for publishing events. Setting a read key is required for running queries. The recommended way to set this configuration information is via the environment. The keys you can set are `KEEN_PROJECT_ID`, `KEEN_WRITE_KEY`, `KEEN_READ_KEY`, and `KEEN_MASTER_KEY`. +As per the Principle of Least Privilege, it's recommended that you not use the master_key if not +necessary. This SDK will expect and use the precise key for a given operation, and throw an +exception in cases of misuse. If you don't want to use environment variables for some reason, you can directly set values as follows: @@ -38,7 +41,7 @@ If you don't want to use environment variables for some reason, you can directly keen.project_id = "xxxx" keen.write_key = "yyyy" keen.read_key = "zzzz" - keen.master_key = "abcd" + keen.master_key = "abcd" # not required for typical usage For information on how to configure unique client instances, take a look at the @@ -100,25 +103,27 @@ For more code samples, take a look at Keen's `docs [{ "price" => 20, ... }, { ... }] - keen.multi_analysis("purchases", analyses={ - "total":{ - "analysis_type": "sum", - "target_property":"price", - "timeframe": "this_14_days" + keen.multi_analysis( + "purchases", + analyses={ + "total":{ + "analysis_type": "sum", + "target_property":"price" + }, + "average":{ + "analysis_type": "average", + "target_property":"price", + } }, - "average":{ - "analysis_type": "average", - "target_property":"price", - "timeframe": "this_14_days" - } + timeframe='this_14_days' ) # => {"total":10329.03, "average":933.93} step1 = { - "event_collection": "signup", + "event_collection": "sign_ups", "actor_property": "user.email" } step2 = { - "event_collection": "purchase", + "event_collection": "purchases", "actor_property": "user.email" } keen.funnel([step1, step2], timeframe="today") # => [2039, 201] @@ -126,7 +131,7 @@ For more code samples, take a look at Keen's `docs `_ from the Keen python client. .. code-block:: python + # Create your KeenClient + from keen.client import KeenClient + + client = KeenClient( + project_id="xxxx", # your project ID + read_key="zzzz", + master_key="abcd" # Most Saved Query functionality requires master_key + ) + # Create a saved query - keen.saved_queries.create("name", saved_query_attributes) + saved_query_attributes = { + # NOTE : For now, refresh_rate must explicitly be set to 0 unless you + # intend to create a Cached Query. + "refresh_rate": 0, + "query": { + "analysis_type": "count", + "event_collection": "purchases", + "timeframe": "this_2_weeks", + "filters": [{ + "property_name": "price", + "operator": "gte", + "property_value": 1.00 + }] + } + } + client.saved_queries.create("name", saved_query_attributes) # Get all saved queries - keen.saved_queries.all() + client.saved_queries.all() # Get one saved query - keen.saved_queries.get("saved-query-slug") + client.saved_queries.get("saved-query-slug") # Get saved query with results - keen.saved_queries.results("saved-query-slug") + client.saved_queries.results("saved-query-slug") - # Update a saved query - saved_query_attributes = { refresh_rate: 14400 } - keen.saved_queries.update("saved-query-slug", saved_query_attributes) + # Update a saved query to now be a cached query with the minimum refresh rate of 4 hrs + saved_query_attributes = { "refresh_rate": 14400 } + client.saved_queries.update("saved-query-slug", saved_query_attributes) - # Delete a saved query - keen.saved_queries.delete("saved-query-slug") + # Update a saved query to a new resource name + saved_query_attributes = { "query_name": "cached-query-slug" } + client.saved_queries.update("saved-query-slug", saved_query_attributes) + + # Delete a saved query (use the new resource name since we just changed it) + client.saved_queries.delete("cached-query-slug") Overwriting event timestamps @@ -310,7 +343,6 @@ returned by the server in the specified time. For example: write_key="yyyy", read_key="zzzz", get_timeout=100 - ) @@ -332,8 +364,6 @@ returned by the server in the specified time. For example: client = KeenClient( project_id="xxxx", write_key="yyyy", - read_key="zzzz", - master_key="abcd", post_timeout=100 ) @@ -366,7 +396,7 @@ To run tests: :: - python setup.py tests + python setup.py test Changelog diff --git a/keen/__init__.py b/keen/__init__.py index 3f269c5..b608d72 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -12,7 +12,7 @@ base_url = None def _initialize_client_from_environment(): - ''' Initialize a KeenCLient instance using environment variables. ''' + ''' Initialize a KeenClient instance using environment variables. ''' global _client, project_id, write_key, read_key, master_key, base_url if _client is None: diff --git a/keen/client.py b/keen/client.py index cda21b0..e8c3f99 100644 --- a/keen/client.py +++ b/keen/client.py @@ -228,7 +228,7 @@ def count(self, event_collection, timeframe=None, timezone=None, interval=None, :param filters: array of dict, contains the filters you'd like to apply to the data example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would - like to group you results by. example: "customer.id" or ["browser","operating_system"] + like to group your results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -254,7 +254,7 @@ def sum(self, event_collection, target_property, timeframe=None, timezone=None, :param filters: array of dict, contains the filters you'd like to apply to the data example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would - like to group you results by. example: "customer.id" or ["browser","operating_system"] + like to group your results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -281,7 +281,7 @@ def minimum(self, event_collection, target_property, timeframe=None, timezone=No :param filters: array of dict, contains the filters you'd like to apply to the data example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would - like to group you results by. example: "customer.id" or ["browser","operating_system"] + like to group your results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -308,7 +308,7 @@ def maximum(self, event_collection, target_property, timeframe=None, timezone=No :param filters: array of dict, contains the filters you'd like to apply to the data example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would - like to group you results by. example: "customer.id" or ["browser","operating_system"] + like to group your results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -335,7 +335,7 @@ def average(self, event_collection, target_property, timeframe=None, timezone=No :param filters: array of dict, contains the filters you'd like to apply to the data example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would - like to group you results by. example: "customer.id" or ["browser","operating_system"] + like to group your results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -362,7 +362,7 @@ def median(self, event_collection, target_property, timeframe=None, timezone=Non :param filters: array of dict, contains the filters you'd like to apply to the data example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would - like to group you results by. example: "customer.id" or ["browser","operating_system"] + like to group your results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -391,7 +391,7 @@ def percentile(self, event_collection, target_property, percentile, timeframe=No :param filters: array of dict, contains the filters you'd like to apply to the data example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would - like to group you results by. example: "customer.id" or ["browser","operating_system"] + like to group your results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -426,7 +426,7 @@ def count_unique(self, event_collection, target_property, timeframe=None, timezo :param filters: array of dict, contains the filters you'd like to apply to the data example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would - like to group you results by. example: "customer.id" or ["browser","operating_system"] + like to group your results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -453,7 +453,7 @@ def select_unique(self, event_collection, target_property, timeframe=None, timez :param filters: array of dict, contains the filters you'd like to apply to the data example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would - like to group you results by. example: "customer.id" or ["browser","operating_system"] + like to group your results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -531,7 +531,7 @@ def multi_analysis(self, event_collection, analyses, timeframe=None, interval=No :param filters: array of dict, contains the filters you'd like to apply to the data example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would - like to group you results by. example: "customer.id" or ["browser","operating_system"] + like to group your results by. example: "customer.id" or ["browser","operating_system"] :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds From 2a00c869de9aba2bc80d653e8d1006af17694e77 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 15 Mar 2017 18:49:35 -0500 Subject: [PATCH 140/224] A few minor PR feedback changes. --- README.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index adfc2d6..cce6a7e 100644 --- a/README.rst +++ b/README.rst @@ -108,11 +108,11 @@ For more code samples, take a look at Keen's `docs Date: Sat, 18 Mar 2017 00:42:40 -0500 Subject: [PATCH 141/224] Explain that query update requires sending the entire query definition. --- README.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index cce6a7e..f3aae8e 100644 --- a/README.rst +++ b/README.rst @@ -283,7 +283,7 @@ You can manage your `saved queries Date: Sat, 18 Mar 2017 03:15:22 -0500 Subject: [PATCH 142/224] Example of what would need to happen to support updating partial query definitions, like only { 'query_name': 'new_name' }. --- keen/saved_queries.py | 81 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/keen/saved_queries.py b/keen/saved_queries.py index 50c86f7..b7cd69f 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -65,14 +65,65 @@ def create(self, query_name, saved_query): return response @requires_key(KeenKeys.MASTER) - def update(self, query_name, saved_query): + def update(self, query_name, saved_query_full_definition): """ Updates the saved query via a PUT request to Keen IO Saved Query - endpoint. + endpoint. The entire query definition must be provided--anything + excluded will be considered an explicit removal of that property. Master key must be set. """ - return self.create(query_name, saved_query) + return self.create(query_name, saved_query_full_definition) + + @requires_key(KeenKeys.MASTER) + def update_partial(self, query_name, saved_query_attributes): + """ + Given a dict of attributes to be updated, update only those attributes + in the Saved Query at the resource given by 'query_name'. This will + perform two HTTP requests--one to fetch the query definition, and one + to set the new attributes. This method will intend to preserve any + other properties on the query. + + Master key must be set. + """ + + query_name_attr_name = "query_name" + refresh_rate_attr_name = "refresh_rate" + query_attr_name = "query" + metadata_attr_name = "metadata" + + old_saved_query = self.get(query_name) + + # Create a new query def to send back. We cannot send values for attributes like 'urls', + # 'last_modified_date', 'run_information', etc. + new_saved_query = { + query_name_attr_name: old_saved_query[query_name_attr_name], # expected + refresh_rate_attr_name: old_saved_query[refresh_rate_attr_name], # expected + query_attr_name: {} + } + + # If metadata was set, preserve it. The Explorer UI currently stores information here. + old_metadata = (old_saved_query[metadata_attr_name] + if hasattr(old_saved_query, metadata_attr_name) + else None) + + if old_metadata: + new_saved_query[metadata_attr_name] = old_metadata + + # Preserve any non-empty properties of the existing query. We get back values like None + # for 'group_by', 'interval' or 'timezone', but those aren't accepted values when updating. + old_query = old_saved_query[query_attr_name] # expected + + # Using dict.items() in both Python 2.x and Python 3.x. iteritems() might be better in 2.7. + # Shallow copy since we want the entire object heirarchy to start with. + for (key, value) in old_query.items(): + if value: + new_saved_query[query_attr_name][key] = value + + # Now, recursively overwrite any attributes passed in. + _deep_update(new_saved_query, saved_query_attributes) + + return self.create(query_name, new_saved_query) @requires_key(KeenKeys.MASTER) def delete(self, query_name): @@ -86,8 +137,30 @@ def delete(self, query_name): return True + def _deep_update(mapping, updates): + # NOTE : Careful with items()/iteritems()/viewitems() on Python 2 vs 3. + for (key, value) in updates.items(): + # NOTE : should really check collections/collections.abc Mapping instead of dict. + if isinstance(mapping, dict): + if isinstance(value, dict): + next_level_value = _deep_update(mapping.get(key, {}), value) + mapping[key] = next_level_value + else: + mapping[key] = value + else: + # Turn the original key into a mapping if it wasn't already + mapping = { key: value } + + return mapping + def _get_json(self, http_method, url, key, *args, **kwargs): - response = self.api.fulfill(http_method, url, headers=utilities.headers(key), *args, **kwargs) + response = self.api.fulfill( + http_method, + url, + headers=utilities.headers(key), + *args, + **kwargs) + self.api._error_handling(response) try: From 10c2bcff1ea17a1f2a784b87861c4b202afedd14 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 21 Mar 2017 22:47:11 -0500 Subject: [PATCH 143/224] Tiny change to support existing clients that already work around create() not accepting a dict. --- keen/saved_queries.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/keen/saved_queries.py b/keen/saved_queries.py index ad558f3..c52ffc9 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -69,7 +69,15 @@ def create(self, query_name, saved_query): keen_api.base_url, keen_api.api_version, self.project_id, query_name ) - payload = json.dumps(saved_query) + payload = saved_query + + # To support clients that may have already called dumps() to work around how this used to + # work, make sure it's not a str. Hopefully it's some sort of mapping. When we actually + # try to send the request, client code will get an InvalidJSONError if payload isn't + # a json-formatted string. + if not isinstance(payload, str): + payload = json.dumps(saved_query) + response = keen_api.fulfill( "put", url, headers=utilities.headers(self.master_key), data=payload ) From a60c9934908442cc225ef80cb41fac091186c74b Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 21 Mar 2017 23:17:24 -0500 Subject: [PATCH 144/224] Forgot to actually use the `read_key` instead of just demanding it in get_collection()/get_collections(). --- keen/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keen/api.py b/keen/api.py index 235ac50..ebdc411 100644 --- a/keen/api.py +++ b/keen/api.py @@ -177,7 +177,7 @@ def get_collection(self, event_collection): url = "{0}/{1}/projects/{2}/events/{3}".format(self.base_url, self.api_version, self.project_id, event_collection) - headers = utilities.headers(self.master_key) + headers = utilities.headers(self.read_key) response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) self._error_handling(response) @@ -191,7 +191,7 @@ def get_all_collections(self): """ url = "{0}/{1}/projects/{2}/events".format(self.base_url, self.api_version, self.project_id) - headers = utilities.headers(self.master_key) + headers = utilities.headers(self.read_key) response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) self._error_handling(response) From 7836c96677a4e680c327aca7d7e6e6139ce46c51 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 21 Mar 2017 23:35:42 -0500 Subject: [PATCH 145/224] Fix unit tests now that get_collection()/get_collections() don't need the `master_key`. --- keen/tests/client_tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 18fa8b1..5af0a5c 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -564,13 +564,13 @@ def setUp(self): super(GetTests, self).setUp() keen._client = None keen.project_id = "1k4jb23kjbkjkjsd" - keen.master_key = "sdofnasofagaergub" + keen.read_key = "sdofnasofagaergub" keen.base_url = None def tearDown(self): keen._client = None keen.project_id = None - keen.master_key = None + keen.read_key = None super(GetTests, self).tearDown() def test_get_collection(self, get): @@ -580,7 +580,7 @@ def test_get_collection(self, get): # Check that the URL is generated correctly self.assertEqual("https://api.keen.io/3.0/projects/1k4jb23kjbkjkjsd/events/foo", get.call_args[0][0]) # Check that the master_key is in the Authorization header - self.assertTrue(keen.master_key in get.call_args[1]["headers"]["Authorization"]) + self.assertTrue(keen.read_key in get.call_args[1]["headers"]["Authorization"]) def test_get_all_collections(self, get): get.return_value = self.LIST_RESPONSE @@ -589,7 +589,7 @@ def test_get_all_collections(self, get): # Check that the URL is generated correctly self.assertEqual("https://api.keen.io/3.0/projects/1k4jb23kjbkjkjsd/events", get.call_args[0][0]) # Check that the master_key in the Authorization header - self.assertTrue(keen.master_key in get.call_args[1]["headers"]["Authorization"]) + self.assertTrue(keen.read_key in get.call_args[1]["headers"]["Authorization"]) # only need to test unicode separately in python2 if sys.version_info[0] < 3: From 9692c54798ddf28e6d1621056ca7240c40b199d9 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 21 Mar 2017 23:50:26 -0500 Subject: [PATCH 146/224] PR feedback. --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index cce6a7e..f3d7e55 100644 --- a/README.rst +++ b/README.rst @@ -30,7 +30,7 @@ keys (if you need an account, `sign up here `_ - it's free). Setting a write key is required for publishing events. Setting a read key is required for running queries. The recommended way to set this configuration information is via the environment. The keys you can set are `KEEN_PROJECT_ID`, `KEEN_WRITE_KEY`, `KEEN_READ_KEY`, and `KEEN_MASTER_KEY`. -As per the Principle of Least Privilege, it's recommended that you not use the master_key if not +As per the `Principle of Least Privilege `_, it's recommended that you not use the master_key if not necessary. This SDK will expect and use the precise key for a given operation, and throw an exception in cases of misuse. @@ -289,21 +289,21 @@ You can manage your `saved queries Date: Wed, 22 Mar 2017 00:34:09 -0500 Subject: [PATCH 147/224] Fix bad merge. --- keen/saved_queries.py | 1 - 1 file changed, 1 deletion(-) diff --git a/keen/saved_queries.py b/keen/saved_queries.py index 80a9b0f..912b4c7 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -68,7 +68,6 @@ def create(self, query_name, saved_query): payload = json.dumps(saved_query) response = self._get_json(HTTPMethods.PUT, url, self._get_master_key(), data=payload) - keen_api._error_handling(response) return response From 50363a84f1f35c7d86ff6011f5b1f1b3beb49a74 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 22 Mar 2017 01:00:40 -0500 Subject: [PATCH 148/224] Get the docs corrected in master while I fix up PR #123 to be ship-ready. --- README.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f3d7e55..ac6f3b2 100644 --- a/README.rst +++ b/README.rst @@ -283,7 +283,7 @@ You can manage your `saved queries Date: Mon, 17 Apr 2017 06:58:31 -0500 Subject: [PATCH 149/224] Smooth out Python 2 vs 3 issues, respond to PR feedback and fix a few bugs. --- keen/saved_queries.py | 29 ++++++++++++++++++----------- keen/scoped_keys.py | 6 +++--- requirements.txt | 1 + 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/keen/saved_queries.py b/keen/saved_queries.py index 38a17b6..8c5c22c 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -1,5 +1,11 @@ +try: + from collections.abc import Mapping as Mapping # for Python 3 +except ImportError as ie: + from collections import Mapping as Mapping # for Python 2 + import json +import six from keen.api import KeenApi, HTTPMethods from keen import exceptions, utilities @@ -72,18 +78,19 @@ def create(self, query_name, saved_query): return response @requires_key(KeenKeys.MASTER) - def update(self, query_name, saved_query_full_definition): + def update_full(self, query_name, saved_query_full_definition): """ Updates the saved query via a PUT request to Keen IO Saved Query endpoint. The entire query definition must be provided--anything excluded will be considered an explicit removal of that property. + Master key must be set. """ return self.create(query_name, saved_query_full_definition) @requires_key(KeenKeys.MASTER) - def update_partial(self, query_name, saved_query_attributes): + def update(self, query_name, saved_query_attributes): """ Given a dict of attributes to be updated, update only those attributes in the Saved Query at the resource given by 'query_name'. This will @@ -111,7 +118,7 @@ def update_partial(self, query_name, saved_query_attributes): # If metadata was set, preserve it. The Explorer UI currently stores information here. old_metadata = (old_saved_query[metadata_attr_name] - if hasattr(old_saved_query, metadata_attr_name) + if metadata_attr_name in old_saved_query else None) if old_metadata: @@ -123,12 +130,12 @@ def update_partial(self, query_name, saved_query_attributes): # Using dict.items() in both Python 2.x and Python 3.x. iteritems() might be better in 2.7. # Shallow copy since we want the entire object heirarchy to start with. - for (key, value) in old_query.items(): + for (key, value) in six.iteritems(old_query): if value: new_saved_query[query_attr_name][key] = value # Now, recursively overwrite any attributes passed in. - _deep_update(new_saved_query, saved_query_attributes) + SavedQueriesInterface._deep_update(new_saved_query, saved_query_attributes) return self.create(query_name, new_saved_query) @@ -144,13 +151,13 @@ def delete(self, query_name): return True + @staticmethod def _deep_update(mapping, updates): - # NOTE : Careful with items()/iteritems()/viewitems() on Python 2 vs 3. - for (key, value) in updates.items(): - # NOTE : should really check collections/collections.abc Mapping instead of dict. - if isinstance(mapping, dict): - if isinstance(value, dict): - next_level_value = _deep_update(mapping.get(key, {}), value) + for (key, value) in six.iteritems(updates): + if isinstance(mapping, Mapping): + if isinstance(value, Mapping): + next_level_value = SavedQueriesInterface._deep_update(mapping.get(key, {}), + value) mapping[key] = next_level_value else: mapping[key] = value diff --git a/keen/scoped_keys.py b/keen/scoped_keys.py index 417e384..30794ec 100644 --- a/keen/scoped_keys.py +++ b/keen/scoped_keys.py @@ -15,9 +15,9 @@ def ensure_bytes(s, encoding=None): - if isinstance(s, six.text_type): - return s.encode(encoding or DEFAULT_ENCODING) - return s + if isinstance(s, six.text_type): + return s.encode(encoding or DEFAULT_ENCODING) + return s def pad_aes256(s): diff --git a/requirements.txt b/requirements.txt index 54ff87c..a489fb7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ pycryptodome>=3.4 requests>=2.5,<2.11.0 +six~=1.10.0 \ No newline at end of file From 4ae8955d03630671400c025c5298f418a8ab4352 Mon Sep 17 00:00:00 2001 From: Justin Date: Mon, 17 Apr 2017 07:32:53 -0500 Subject: [PATCH 150/224] Fix up tests. --- keen/tests/saved_query_tests.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/keen/tests/saved_query_tests.py b/keen/tests/saved_query_tests.py index ce125f2..42fee2f 100644 --- a/keen/tests/saved_query_tests.py +++ b/keen/tests/saved_query_tests.py @@ -89,18 +89,30 @@ def test_create_saved_query(self): saved_query = self.client.saved_queries.create("saved-query-name", saved_queries_response) - self.assertEquals(saved_query, saved_queries_response) + self.assertEqual(saved_query, saved_queries_response) @responses.activate def test_update_saved_query(self): saved_queries_response = { - "query_name": "saved-query-name" + "query_name": "saved-query-name", + "refresh_rate": 14400, + "query": { + "analysis_type": "average", + "event_collection": "TheCollection", + "target_property": "TheProperty", + "timeframe": "this_2_weeks" + } } url = "{0}/{1}/projects/{2}/queries/saved/saved-query-name".format( self.client.api.base_url, self.client.api.api_version, self.exp_project_id ) + + responses.add( + responses.GET, url, status=200, json=saved_queries_response + ) + responses.add( responses.PUT, url, status=200, json=saved_queries_response ) From 5d96480a93d327502b429372ed64b609fd9955e9 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 18 Apr 2017 02:30:00 -0500 Subject: [PATCH 151/224] Respond to PR feedback. --- keen/saved_queries.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/keen/saved_queries.py b/keen/saved_queries.py index 8c5c22c..e5b6160 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -1,12 +1,12 @@ -try: - from collections.abc import Mapping as Mapping # for Python 3 -except ImportError as ie: - from collections import Mapping as Mapping # for Python 2 - import json import six +if six.PY3: + from collections.abc import Mapping +elif six.PY2: + from collections import Mapping + from keen.api import KeenApi, HTTPMethods from keen import exceptions, utilities from keen.utilities import KeenKeys, requires_key @@ -128,7 +128,6 @@ def update(self, query_name, saved_query_attributes): # for 'group_by', 'interval' or 'timezone', but those aren't accepted values when updating. old_query = old_saved_query[query_attr_name] # expected - # Using dict.items() in both Python 2.x and Python 3.x. iteritems() might be better in 2.7. # Shallow copy since we want the entire object heirarchy to start with. for (key, value) in six.iteritems(old_query): if value: From cd8850e21578205d2465b6c03a862197df1f9bdb Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 18 Apr 2017 02:48:09 -0500 Subject: [PATCH 152/224] Fix import for Python 3.2. --- keen/saved_queries.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/keen/saved_queries.py b/keen/saved_queries.py index e5b6160..9c562df 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -2,9 +2,9 @@ import json import six -if six.PY3: - from collections.abc import Mapping -elif six.PY2: +try: + from collections.abc import Mapping # Python >=3.3 +except ImportError: from collections import Mapping from keen.api import KeenApi, HTTPMethods From 476c00ff35b75d304db9504340fe764c15c34379 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 18 Apr 2017 02:57:32 -0500 Subject: [PATCH 153/224] Fix some code review issues brought up by codacy. --- keen/saved_queries.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/keen/saved_queries.py b/keen/saved_queries.py index 9c562df..0260a0b 100644 --- a/keen/saved_queries.py +++ b/keen/saved_queries.py @@ -7,8 +7,8 @@ except ImportError: from collections import Mapping -from keen.api import KeenApi, HTTPMethods -from keen import exceptions, utilities +from keen.api import HTTPMethods +from keen import utilities from keen.utilities import KeenKeys, requires_key @@ -146,7 +146,7 @@ def delete(self, query_name): """ url = "{0}/{1}".format(self.saved_query_url, query_name) - response = self._get_json(HTTPMethods.DELETE, url, self._get_master_key()) + self._get_json(HTTPMethods.DELETE, url, self._get_master_key()) return True From c4e3cae8914cadf8b0045052bc78b367b3276695 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 16 May 2017 20:54:42 -0500 Subject: [PATCH 154/224] Update tests to validate differences between update() and update_full(). --- keen/tests/saved_query_tests.py | 94 ++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 8 deletions(-) diff --git a/keen/tests/saved_query_tests.py b/keen/tests/saved_query_tests.py index 42fee2f..df689ec 100644 --- a/keen/tests/saved_query_tests.py +++ b/keen/tests/saved_query_tests.py @@ -1,8 +1,13 @@ +import copy +import json +import re +from requests.exceptions import HTTPError +import responses + from keen.tests.base_test_case import BaseTestCase from keen.client import KeenClient from keen import exceptions -import responses class SavedQueryTests(BaseTestCase): @@ -93,7 +98,10 @@ def test_create_saved_query(self): @responses.activate def test_update_saved_query(self): - saved_queries_response = { + unacceptable_attr = "run_information" + metadata_attr_name = "metadata" + + original_query = { "query_name": "saved-query-name", "refresh_rate": 14400, "query": { @@ -101,8 +109,11 @@ def test_update_saved_query(self): "event_collection": "TheCollection", "target_property": "TheProperty", "timeframe": "this_2_weeks" - } + }, + metadata_attr_name: { "foo": "bar" }, + unacceptable_attr: { "foo": "bar" } } + url = "{0}/{1}/projects/{2}/queries/saved/saved-query-name".format( self.client.api.base_url, self.client.api.api_version, @@ -110,16 +121,83 @@ def test_update_saved_query(self): ) responses.add( - responses.GET, url, status=200, json=saved_queries_response + responses.GET, url, status=200, json=original_query ) - responses.add( - responses.PUT, url, status=200, json=saved_queries_response + updated_query = { "query": {} } # copy.deepcopy(original_query) + new_analysis_type = "sum" + updated_query["query"]["analysis_type"] = new_analysis_type + + def request_callback(request): + payload = json.loads(request.body) + + # Ensure update() round-trips some necessary things like "metadata" + self.assertEqual(payload[metadata_attr_name], original_query[metadata_attr_name]) + + # Ensure update() doesn't pass unacceptable attributes + self.assertNotIn(unacceptable_attr, payload) + + # Ensure update() merges deep updates + self.assertEqual(payload["query"]["analysis_type"], new_analysis_type) + payload["query"]["analysis_type"] = "average" + payload[unacceptable_attr] = original_query[unacceptable_attr] + self.assertEqual(payload, original_query) + + headers = {} + return (200, headers, json.dumps(updated_query)) + + responses.add_callback( + responses.PUT, + url, + callback=request_callback, + content_type='application/json', ) - saved_query = self.client.saved_queries.update("saved-query-name", saved_queries_response) + saved_query = self.client.saved_queries.update("saved-query-name", updated_query) - self.assertEquals(saved_query, saved_queries_response) + self.assertEqual(saved_query, updated_query) + + @responses.activate + def test_update_full_saved_query(self): + saved_queries_response = { + "query_name": "saved-query-name", + "refresh_rate": 14400, + "query": { + "analysis_type": "average", + "event_collection": "TheCollection", + "target_property": "TheProperty", + "timeframe": "this_2_weeks" + } + } + + url = "{0}/{1}/projects/{2}/queries/saved/saved-query-name".format( + self.client.api.base_url, + self.client.api.api_version, + self.exp_project_id + ) + + # Unlike update(), update_full() should not be fetching the existing definition. + exception = HTTPError("No GET expected when performing a full update.") + responses.add(responses.GET, re.compile(".*"), body=exception) + + def request_callback(request): + payload = json.loads(request.body) + + # Ensure update_full() passes along the unaltered complete Saved/Cached Query def. + self.assertEqual(payload, saved_queries_response) + headers = {} + return (200, headers, json.dumps(saved_queries_response)) + + responses.add_callback( + responses.PUT, + url, + callback=request_callback, + content_type='application/json', + ) + + saved_query = self.client.saved_queries.update_full("saved-query-name", saved_queries_response) + + self.assertEqual(saved_query, saved_queries_response) def test_delete_saved_query_master_key(self): client = KeenClient(project_id="123123", read_key="123123") From 1dd859d19683779dced36ee8becd8a16db1c2f6e Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 16 May 2017 23:26:26 -0500 Subject: [PATCH 155/224] Get unit tests working: - Just use unittest2 everywhere for tests across versions, since it's a drop-in replacement and back-compat with <2.7. - Drop Python 3.2 testing for now, due to Issue #126. We can work on adding this back if/when there's resolution there either on Travis' side or in pycryptodome or something. - Add Python 3.6 environment in Travis CI. --- .travis.yml | 2 +- keen/tests/base_test_case.py | 2 +- setup.py | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2455552..103abc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,10 @@ language: python python: - "2.6" - "2.7" - - "3.2" - "3.3" - "3.4" - "3.5" + - "3.6" # command to install dependencies install: "pip install -r requirements.txt" # command to run tests diff --git a/keen/tests/base_test_case.py b/keen/tests/base_test_case.py index 0f861ed..c40f99b 100644 --- a/keen/tests/base_test_case.py +++ b/keen/tests/base_test_case.py @@ -1,5 +1,5 @@ import string -import unittest +import unittest2 as unittest __author__ = 'dkador' diff --git a/setup.py b/setup.py index d6cb442..d5c9e2b 100644 --- a/setup.py +++ b/setup.py @@ -21,10 +21,7 @@ reqs = reqs_file.readlines() reqs_file.close() -tests_require = ['nose', 'mock', 'responses'] - -if sys.version_info < (2, 7): - tests_require.append('unittest2') +tests_require = ['nose', 'mock', 'responses', 'unittest2'] setup( name="keen", From a78694fdbe4f45b9b8ba427bcd568f1d93550916 Mon Sep 17 00:00:00 2001 From: Justin Date: Tue, 16 May 2017 23:33:26 -0500 Subject: [PATCH 156/224] Tend to some codacy complaints about an unused import. --- keen/tests/saved_query_tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/keen/tests/saved_query_tests.py b/keen/tests/saved_query_tests.py index df689ec..b90c9b3 100644 --- a/keen/tests/saved_query_tests.py +++ b/keen/tests/saved_query_tests.py @@ -1,4 +1,3 @@ -import copy import json import re from requests.exceptions import HTTPError @@ -124,7 +123,7 @@ def test_update_saved_query(self): responses.GET, url, status=200, json=original_query ) - updated_query = { "query": {} } # copy.deepcopy(original_query) + updated_query = { "query": {} } new_analysis_type = "sum" updated_query["query"]["analysis_type"] = new_analysis_type From 8bc7cfd38fb1db07de0b441ca71be3cf1fbd5efa Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 17 May 2017 00:04:16 -0500 Subject: [PATCH 157/224] Update the readme to reflect new update()/update_full() behavior. We'll bump the version and update the changelog in an upcoming PR. --- README.rst | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index ac6f3b2..9d5ff4e 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ Use pip to install! pip install keen -This client is known to work on Python 2.6, 2.7, 3.2, 3.3, 3.4 and 3.5. +This client is known to work on Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5 and 3.6. For versions of Python < 2.7.9, you’ll need to install pyasn1, ndg-httpsclient, pyOpenSSL. @@ -283,6 +283,7 @@ You can manage your `saved queries Date: Wed, 17 May 2017 00:39:51 -0500 Subject: [PATCH 158/224] Bump version to 0.4.0. - There is slight behavior change and some new features, but since this is still pre-v1.0 according to semver a minor version bump should suffice. --- README.rst | 8 ++++++++ keen/utilities.py | 2 +- setup.py | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9d5ff4e..fe2657b 100644 --- a/README.rst +++ b/README.rst @@ -423,6 +423,14 @@ To run tests: Changelog --------- +0.4.0 +`````` + ++ SavedQueriesInterface.create() now accepts a dict as the query definition. ++ get_collection() and get_all_collections() now only require a Read Key instead of Master. ++ SavedQueriesInterface.update() now performs partial updates. update_full() exhibits old behavior. ++ Misc documentation updates. + 0.3.31 `````` diff --git a/keen/utilities.py b/keen/utilities.py index 972554a..bc7c2ba 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -5,7 +5,7 @@ from keen import exceptions -VERSION = "0.3.31" +VERSION = "0.4.0" def version(): """ diff --git a/setup.py b/setup.py index d5c9e2b..046af9a 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="keen", - version="0.3.31", + version="0.4.0", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", @@ -49,6 +49,7 @@ 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Libraries :: Python Modules', ] ) From 6befef0ce83b2a6fd234154b943ca3e18db82ccf Mon Sep 17 00:00:00 2001 From: Brian Baumhover Date: Wed, 17 May 2017 21:48:43 -0500 Subject: [PATCH 159/224] Pull in @baumatron's change to install older setuptools for Python 3.2 tests. --- .travis.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 103abc1..6078d90 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,19 @@ language: python python: - "2.6" - "2.7" + - "3.2" - "3.3" - "3.4" - "3.5" - "3.6" # command to install dependencies -install: "pip install -r requirements.txt" +install: | + if [ "$TRAVIS_PYTHON_VERSION" == 3.2 ] + then + pip install "setuptools<30" + fi + + pip install -r requirements.txt + # command to run tests script: "python setup.py test" From 7235135655f8b03b22eb7b3201e22dfa856ebb44 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 28 Sep 2017 09:34:41 -0600 Subject: [PATCH 160/224] +support for order_by Order by is in alpha currently. To make the pain points of its alpha release a little more bearable, I've added validation of the inputs for order_by so it errors out at the client in most circumstances. --- keen/__init__.py | 64 +++++++++++++++++++++++++++++++--------------- keen/api.py | 38 ++++++++++++++++++++++++++++ keen/client.py | 66 ++++++++++++++++++++++++++++++++++-------------- 3 files changed, 129 insertions(+), 39 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index b608d72..377429c 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -74,7 +74,8 @@ def generate_image_beacon(event_collection, body, timestamp=None): return _client.generate_image_beacon(event_collection, body, timestamp=timestamp) -def count(event_collection, timeframe=None, timezone=None, interval=None, filters=None, group_by=None, max_age=None): +def count(event_collection, timeframe=None, timezone=None, interval=None, filters=None, group_by=None, order_by=None, + max_age=None): """ Performs a count query Counts the number of events that meet the given criteria. @@ -90,17 +91,20 @@ def count(event_collection, timeframe=None, timezone=None, interval=None, filter example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.count(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, max_age=max_age) + interval=interval, filters=filters, group_by=group_by, order_by=order_by, + max_age=max_age) def sum(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, max_age=None): + group_by=None, order_by=None, max_age=None): """ Performs a sum query Adds the values of a target property for events that meet the given criteria. @@ -117,18 +121,20 @@ def sum(event_collection, target_property, timeframe=None, timezone=None, interv example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.sum(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) def minimum(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, max_age=None): + group_by=None, order_by=None, max_age=None): """ Performs a minimum query Finds the minimum value of a target property for events that meet the given criteria. @@ -145,18 +151,20 @@ def minimum(event_collection, target_property, timeframe=None, timezone=None, in example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.minimum(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) def maximum(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, max_age=None): + group_by=None, order_by=None, max_age=None): """ Performs a maximum query Finds the maximum value of a target property for events that meet the given criteria. @@ -173,18 +181,20 @@ def maximum(event_collection, target_property, timeframe=None, timezone=None, in example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.maximum(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) def average(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, max_age=None): + group_by=None, order_by=None, max_age=None): """ Performs a average query Finds the average of a target property for events that meet the given criteria. @@ -201,18 +211,20 @@ def average(event_collection, target_property, timeframe=None, timezone=None, in example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.average(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) def median(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, max_age=None): + group_by=None, order_by=None, max_age=None): """ Performs a median query Finds the median of a target property for events that meet the given criteria. @@ -229,18 +241,20 @@ def median(event_collection, target_property, timeframe=None, timezone=None, int example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.median(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) def percentile(event_collection, target_property, percentile, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None): """ Performs a percentile query Finds the percentile of a target property for events that meet the given criteria. @@ -259,6 +273,8 @@ def percentile(event_collection, target_property, percentile, timeframe=None, ti example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -272,13 +288,14 @@ def percentile(event_collection, target_property, percentile, timeframe=None, ti interval=interval, filters=filters, group_by=group_by, + order_by=order_by, target_property=target_property, max_age=max_age, ) def count_unique(event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None): """ Performs a count unique query Counts the unique values of a target property for events that meet the given criteria. @@ -295,18 +312,20 @@ def count_unique(event_collection, target_property, timeframe=None, timezone=Non example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.count_unique(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) def select_unique(event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None): """ Performs a select unique query Returns an array of the unique values of a target property for events that meet the given criteria. @@ -323,13 +342,15 @@ def select_unique(event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ _initialize_client_from_environment() return _client.select_unique(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) @@ -377,8 +398,8 @@ def funnel(*args, **kwargs): return _client.funnel(*args, **kwargs) -def multi_analysis(event_collection, analyses, timeframe=None, interval=None, - timezone=None, filters=None, group_by=None, max_age=None): +def multi_analysis(event_collection, analyses, timeframe=None, interval=None, timezone=None, + filters=None, group_by=None, order_by=None, max_age=None): """ Performs a multi-analysis query Returns a dictionary of analysis results. @@ -397,6 +418,8 @@ def multi_analysis(event_collection, analyses, timeframe=None, interval=None, example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -404,7 +427,8 @@ def multi_analysis(event_collection, analyses, timeframe=None, interval=None, _initialize_client_from_environment() return _client.multi_analysis(event_collection=event_collection, timeframe=timeframe, interval=interval, timezone=timezone, filters=filters, - group_by=group_by, analyses=analyses, max_age=max_age) + group_by=group_by, order_by=order_by, analyses=analyses, + max_age=max_age) def delete_events(*args, **kwargs): diff --git a/keen/api.py b/keen/api.py index ebdc411..4d9fd48 100644 --- a/keen/api.py +++ b/keen/api.py @@ -126,12 +126,50 @@ def post_events(self, events): self._error_handling(response) return self._get_response_json(response) + def _order_by_is_valid_or_none(self, params): + """ + Validates that a given order_by has proper syntax. + + :return: Returns True if either no order_by is present, or if the order_by is well-formed. + """ + if not "order_by" in params or not params["order_by"]: + return True + + def _order_by_dict_is_not_well_formed(d): + if not isinstance(d, dict): + # Bad type. + return True + if "property_name" in d and d["property_name"]: + if "direction" in d and not (d["direction"] == "ASC" or d["direction"] == "DESC"): + # Bad direction provided. + return True + for k in d: + if k != "property_name" and k != "direction": + # Unexpected key. + return True + # Everything looks good! + return False + # Missing required key. + return True + + # order_by is converted to a list before this point if it wasn't one before. + order_by_list = json.loads(params["order_by"]) + if filter(_order_by_dict_is_not_well_formed, order_by_list): + # At least one order_by dict is broken. + return False + if not "group_by" in params or not params["group_by"]: + # We must have group_by to have order_by make sense. + return False + return True + @requires_key(KeenKeys.READ) def query(self, analysis_type, params, all_keys=False): """ Performs a query using the Keen IO analysis API. A read key must be set first. """ + if not self._order_by_is_valid_or_none(params): + raise ValueError("order_by given is invalid or is missing required group_by.") url = "{0}/{1}/projects/{2}/queries/{3}".format(self.base_url, self.api_version, self.project_id, analysis_type) diff --git a/keen/client.py b/keen/client.py index d463cab..afff1c0 100644 --- a/keen/client.py +++ b/keen/client.py @@ -213,7 +213,7 @@ def _url_escape(self, url): return urllib.parse.quote(url) def count(self, event_collection, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None): """ Performs a count query Counts the number of events that meet the given criteria. @@ -229,16 +229,19 @@ def count(self, event_collection, timeframe=None, timezone=None, interval=None, example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, max_age=max_age) + interval=interval, filters=filters, group_by=group_by, order_by=order_by, + max_age=max_age) return self.api.query("count", params) def sum(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, max_age=None): + group_by=None, order_by=None, max_age=None): """ Performs a sum query Adds the values of a target property for events that meet the given criteria. @@ -255,17 +258,19 @@ def sum(self, event_collection, target_property, timeframe=None, timezone=None, example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) return self.api.query("sum", params) def minimum(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None): """ Performs a minimum query Finds the minimum value of a target property for events that meet the given criteria. @@ -282,6 +287,8 @@ def minimum(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -292,7 +299,7 @@ def minimum(self, event_collection, target_property, timeframe=None, timezone=No return self.api.query("minimum", params) def maximum(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None): """ Performs a maximum query Finds the maximum value of a target property for events that meet the given criteria. @@ -309,17 +316,19 @@ def maximum(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) return self.api.query("maximum", params) def average(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None): """ Performs a average query Finds the average of a target property for events that meet the given criteria. @@ -336,17 +345,19 @@ def average(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) return self.api.query("average", params) def median(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None): """ Performs a median query Finds the median of a target property for events that meet the given criteria. @@ -363,17 +374,19 @@ def median(self, event_collection, target_property, timeframe=None, timezone=Non example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) return self.api.query("median", params) def percentile(self, event_collection, target_property, percentile, timeframe=None, timezone=None, - interval=None, filters=None, group_by=None, max_age=None): + interval=None, filters=None, group_by=None, order_by=None, max_age=None): """ Performs a percentile query Finds the percentile of a target property for events that meet the given criteria. @@ -392,6 +405,8 @@ def percentile(self, event_collection, target_property, percentile, timeframe=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -404,13 +419,14 @@ def percentile(self, event_collection, target_property, percentile, timeframe=No interval=interval, filters=filters, group_by=group_by, + order_by=order_by, target_property=target_property, max_age=max_age, ) return self.api.query("percentile", params) def count_unique(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None): """ Performs a count unique query Counts the unique values of a target property for events that meet the given criteria. @@ -427,17 +443,19 @@ def count_unique(self, event_collection, target_property, timeframe=None, timezo example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) return self.api.query("count_unique", params) def select_unique(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None): """ Performs a select unique query Returns an array of the unique values of a target property for events that meet the given criteria. @@ -454,12 +472,14 @@ def select_unique(self, event_collection, target_property, timeframe=None, timez example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, - interval=interval, filters=filters, group_by=group_by, + interval=interval, filters=filters, group_by=group_by, order_by=order_by, target_property=target_property, max_age=max_age) return self.api.query("select_unique", params) @@ -513,7 +533,7 @@ def funnel(self, steps, timeframe=None, timezone=None, max_age=None, all_keys=Fa return self.api.query("funnel", params, all_keys=all_keys) def multi_analysis(self, event_collection, analyses, timeframe=None, interval=None, timezone=None, filters=None, - group_by=None, max_age=None): + group_by=None, order_by=None, max_age=None): """ Performs a multi-analysis query Returns a dictionary of analysis results. @@ -532,6 +552,8 @@ def multi_analysis(self, event_collection, analyses, timeframe=None, interval=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] + :param order_by: dictionary object containing the property_name to order by and the + desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -543,6 +565,7 @@ def multi_analysis(self, event_collection, analyses, timeframe=None, interval=No timezone=timezone, filters=filters, group_by=group_by, + order_by=order_by, analyses=analyses, max_age=max_age, ) @@ -550,8 +573,8 @@ def multi_analysis(self, event_collection, analyses, timeframe=None, interval=No return self.api.query("multi_analysis", params) def get_params(self, event_collection=None, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, target_property=None, latest=None, email=None, analyses=None, steps=None, - property_names=None, percentile=None, max_age=None): + group_by=None, order_by=None, target_property=None, latest=None, email=None, analyses=None, + steps=None, property_names=None, percentile=None, max_age=None): params = {} if event_collection: params["event_collection"] = event_collection @@ -571,6 +594,11 @@ def get_params(self, event_collection=None, timeframe=None, timezone=None, inter params["group_by"] = json.dumps(group_by) else: params["group_by"] = group_by + if order_by: + if isinstance(order_by, list): + params["order_by"] = json.dumps(order_by) + else: + params["order_by"] = json.dumps([order_by]) if target_property: params["target_property"] = target_property if latest: From 7296fa1c1b64120563481136a230f443b5f9f7d9 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 28 Sep 2017 09:55:44 -0600 Subject: [PATCH 161/224] Added limit support. Limit is only usable with order_by so is also in alpha. This includes some validation on the client side. --- keen/__init__.py | 47 ++++++++++++++++++++++++++----------------- keen/api.py | 18 +++++++++++++++++ keen/client.py | 52 ++++++++++++++++++++++++++++++------------------ 3 files changed, 80 insertions(+), 37 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index 377429c..3205bd6 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -75,7 +75,7 @@ def generate_image_beacon(event_collection, body, timestamp=None): def count(event_collection, timeframe=None, timezone=None, interval=None, filters=None, group_by=None, order_by=None, - max_age=None): + max_age=None, limit=None): """ Performs a count query Counts the number of events that meet the given criteria. @@ -93,6 +93,7 @@ def count(event_collection, timeframe=None, timezone=None, interval=None, filter like to group you results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -100,11 +101,11 @@ def count(event_collection, timeframe=None, timezone=None, interval=None, filter _initialize_client_from_environment() return _client.count(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - max_age=max_age) + max_age=max_age, limit=limit) def sum(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, order_by=None, max_age=None): + group_by=None, order_by=None, max_age=None, limit=None): """ Performs a sum query Adds the values of a target property for events that meet the given criteria. @@ -123,6 +124,7 @@ def sum(event_collection, target_property, timeframe=None, timezone=None, interv like to group you results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -130,11 +132,11 @@ def sum(event_collection, target_property, timeframe=None, timezone=None, interv _initialize_client_from_environment() return _client.sum(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) def minimum(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, order_by=None, max_age=None): + group_by=None, order_by=None, max_age=None, limit=None): """ Performs a minimum query Finds the minimum value of a target property for events that meet the given criteria. @@ -153,6 +155,7 @@ def minimum(event_collection, target_property, timeframe=None, timezone=None, in like to group you results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -160,11 +163,11 @@ def minimum(event_collection, target_property, timeframe=None, timezone=None, in _initialize_client_from_environment() return _client.minimum(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) def maximum(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, order_by=None, max_age=None): + group_by=None, order_by=None, max_age=None, limit=None): """ Performs a maximum query Finds the maximum value of a target property for events that meet the given criteria. @@ -183,6 +186,7 @@ def maximum(event_collection, target_property, timeframe=None, timezone=None, in like to group you results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -190,11 +194,11 @@ def maximum(event_collection, target_property, timeframe=None, timezone=None, in _initialize_client_from_environment() return _client.maximum(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) def average(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, order_by=None, max_age=None): + group_by=None, order_by=None, max_age=None, limit=None): """ Performs a average query Finds the average of a target property for events that meet the given criteria. @@ -213,6 +217,7 @@ def average(event_collection, target_property, timeframe=None, timezone=None, in like to group you results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -220,11 +225,11 @@ def average(event_collection, target_property, timeframe=None, timezone=None, in _initialize_client_from_environment() return _client.average(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) def median(event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, order_by=None, max_age=None): + group_by=None, order_by=None, max_age=None, limit=None): """ Performs a median query Finds the median of a target property for events that meet the given criteria. @@ -243,6 +248,7 @@ def median(event_collection, target_property, timeframe=None, timezone=None, int like to group you results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -254,7 +260,7 @@ def median(event_collection, target_property, timeframe=None, timezone=None, int def percentile(event_collection, target_property, percentile, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, order_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a percentile query Finds the percentile of a target property for events that meet the given criteria. @@ -275,6 +281,7 @@ def percentile(event_collection, target_property, percentile, timeframe=None, ti like to group you results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -291,11 +298,12 @@ def percentile(event_collection, target_property, percentile, timeframe=None, ti order_by=order_by, target_property=target_property, max_age=max_age, + limit=limit ) def count_unique(event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, order_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a count unique query Counts the unique values of a target property for events that meet the given criteria. @@ -314,6 +322,7 @@ def count_unique(event_collection, target_property, timeframe=None, timezone=Non like to group you results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -321,11 +330,11 @@ def count_unique(event_collection, target_property, timeframe=None, timezone=Non _initialize_client_from_environment() return _client.count_unique(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) def select_unique(event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, order_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a select unique query Returns an array of the unique values of a target property for events that meet the given criteria. @@ -344,6 +353,7 @@ def select_unique(event_collection, target_property, timeframe=None, timezone=No like to group you results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -351,7 +361,7 @@ def select_unique(event_collection, target_property, timeframe=None, timezone=No _initialize_client_from_environment() return _client.select_unique(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) def extraction(event_collection, timeframe=None, timezone=None, filters=None, latest=None, email=None, @@ -399,7 +409,7 @@ def funnel(*args, **kwargs): def multi_analysis(event_collection, analyses, timeframe=None, interval=None, timezone=None, - filters=None, group_by=None, order_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a multi-analysis query Returns a dictionary of analysis results. @@ -420,6 +430,7 @@ def multi_analysis(event_collection, analyses, timeframe=None, interval=None, ti like to group you results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -428,7 +439,7 @@ def multi_analysis(event_collection, analyses, timeframe=None, interval=None, ti return _client.multi_analysis(event_collection=event_collection, timeframe=timeframe, interval=interval, timezone=timezone, filters=filters, group_by=group_by, order_by=order_by, analyses=analyses, - max_age=max_age) + max_age=max_age, limit=limit) def delete_events(*args, **kwargs): diff --git a/keen/api.py b/keen/api.py index 4d9fd48..21f6730 100644 --- a/keen/api.py +++ b/keen/api.py @@ -130,6 +130,7 @@ def _order_by_is_valid_or_none(self, params): """ Validates that a given order_by has proper syntax. + :param params: Query params. :return: Returns True if either no order_by is present, or if the order_by is well-formed. """ if not "order_by" in params or not params["order_by"]: @@ -162,6 +163,21 @@ def _order_by_dict_is_not_well_formed(d): return False return True + def _limit_is_valid_or_none(self, params): + """ + Validates that a given limit is not present or is well-formed. + + :param params: Query params. + :return: Returns True if a limit is present or is well-formed. + """ + if not "limit" in params or not params["limit"]: + return True + if not isinstance(params["limit"], int) or params["limit"] < 1: + return False + if not "order_by" in params: + return False + return True + @requires_key(KeenKeys.READ) def query(self, analysis_type, params, all_keys=False): """ @@ -170,6 +186,8 @@ def query(self, analysis_type, params, all_keys=False): """ if not self._order_by_is_valid_or_none(params): raise ValueError("order_by given is invalid or is missing required group_by.") + if not self._limit_is_valid_or_none(params): + raise ValueError("limit given is invalid or is missing required order_by.") url = "{0}/{1}/projects/{2}/queries/{3}".format(self.base_url, self.api_version, self.project_id, analysis_type) diff --git a/keen/client.py b/keen/client.py index afff1c0..3c172f9 100644 --- a/keen/client.py +++ b/keen/client.py @@ -213,7 +213,7 @@ def _url_escape(self, url): return urllib.parse.quote(url) def count(self, event_collection, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, order_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a count query Counts the number of events that meet the given criteria. @@ -231,17 +231,18 @@ def count(self, event_collection, timeframe=None, timezone=None, interval=None, like to group your results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - max_age=max_age) + max_age=max_age, limit=limit) return self.api.query("count", params) def sum(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, filters=None, - group_by=None, order_by=None, max_age=None): + group_by=None, order_by=None, max_age=None, limit=None): """ Performs a sum query Adds the values of a target property for events that meet the given criteria. @@ -260,17 +261,18 @@ def sum(self, event_collection, target_property, timeframe=None, timezone=None, like to group your results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) return self.api.query("sum", params) def minimum(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, order_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a minimum query Finds the minimum value of a target property for events that meet the given criteria. @@ -289,17 +291,18 @@ def minimum(self, event_collection, target_property, timeframe=None, timezone=No like to group your results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) return self.api.query("minimum", params) def maximum(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, order_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a maximum query Finds the maximum value of a target property for events that meet the given criteria. @@ -318,17 +321,18 @@ def maximum(self, event_collection, target_property, timeframe=None, timezone=No like to group your results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) return self.api.query("maximum", params) def average(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, order_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a average query Finds the average of a target property for events that meet the given criteria. @@ -347,17 +351,18 @@ def average(self, event_collection, target_property, timeframe=None, timezone=No like to group your results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) return self.api.query("average", params) def median(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, order_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a median query Finds the median of a target property for events that meet the given criteria. @@ -376,17 +381,18 @@ def median(self, event_collection, target_property, timeframe=None, timezone=Non like to group your results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) return self.api.query("median", params) def percentile(self, event_collection, target_property, percentile, timeframe=None, timezone=None, - interval=None, filters=None, group_by=None, order_by=None, max_age=None): + interval=None, filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a percentile query Finds the percentile of a target property for events that meet the given criteria. @@ -407,6 +413,7 @@ def percentile(self, event_collection, target_property, percentile, timeframe=No like to group your results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -422,11 +429,12 @@ def percentile(self, event_collection, target_property, percentile, timeframe=No order_by=order_by, target_property=target_property, max_age=max_age, + limit=limit ) return self.api.query("percentile", params) def count_unique(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, order_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a count unique query Counts the unique values of a target property for events that meet the given criteria. @@ -445,17 +453,18 @@ def count_unique(self, event_collection, target_property, timeframe=None, timezo like to group your results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) return self.api.query("count_unique", params) def select_unique(self, event_collection, target_property, timeframe=None, timezone=None, interval=None, - filters=None, group_by=None, order_by=None, max_age=None): + filters=None, group_by=None, order_by=None, max_age=None, limit=None): """ Performs a select unique query Returns an array of the unique values of a target property for events that meet the given criteria. @@ -474,13 +483,14 @@ def select_unique(self, event_collection, target_property, timeframe=None, timez like to group your results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds """ params = self.get_params(event_collection=event_collection, timeframe=timeframe, timezone=timezone, interval=interval, filters=filters, group_by=group_by, order_by=order_by, - target_property=target_property, max_age=max_age) + target_property=target_property, max_age=max_age, limit=limit) return self.api.query("select_unique", params) def extraction(self, event_collection, timeframe=None, timezone=None, filters=None, latest=None, @@ -533,7 +543,7 @@ def funnel(self, steps, timeframe=None, timezone=None, max_age=None, all_keys=Fa return self.api.query("funnel", params, all_keys=all_keys) def multi_analysis(self, event_collection, analyses, timeframe=None, interval=None, timezone=None, filters=None, - group_by=None, order_by=None, max_age=None): + group_by=None, order_by=None, max_age=None, limit=None): """ Performs a multi-analysis query Returns a dictionary of analysis results. @@ -554,6 +564,7 @@ def multi_analysis(self, event_collection, analyses, timeframe=None, interval=No like to group your results by. example: "customer.id" or ["browser","operating_system"] :param order_by: dictionary object containing the property_name to order by and the desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -568,13 +579,14 @@ def multi_analysis(self, event_collection, analyses, timeframe=None, interval=No order_by=order_by, analyses=analyses, max_age=max_age, + limit=limit ) return self.api.query("multi_analysis", params) def get_params(self, event_collection=None, timeframe=None, timezone=None, interval=None, filters=None, group_by=None, order_by=None, target_property=None, latest=None, email=None, analyses=None, - steps=None, property_names=None, percentile=None, max_age=None): + steps=None, property_names=None, percentile=None, max_age=None, limit=None): params = {} if event_collection: params["event_collection"] = event_collection @@ -599,6 +611,8 @@ def get_params(self, event_collection=None, timeframe=None, timezone=None, inter params["order_by"] = json.dumps(order_by) else: params["order_by"] = json.dumps([order_by]) + if limit: + params["limit"] = limit if target_property: params["target_property"] = target_property if latest: From 1ca829957b144f1c67a8dca8cd82470d839ec91f Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 28 Sep 2017 10:08:53 -0600 Subject: [PATCH 162/224] +docs for order_by --- README.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.rst b/README.rst index fe2657b..4e7bbcb 100644 --- a/README.rst +++ b/README.rst @@ -149,6 +149,28 @@ Advanced Usage See below for more options. +Order Your Grouped Results with order_by and limit +'''''''''''''''''''''''''''''''''''''''''''''''''' + +Alpha support for ordering your results and limiting what is displayed is now supported in the Python SDK. +Keep in mind that even if you limit your results with the 'limit' keyword, you are still querying over the +normal amount of data, and thus your compute costs will not change. Limit only changes what is displayed. + +The keyword 'limit' must be a positive integer. The keyword 'order_by' must be a dictionary with a required +"property_name" specified and optionally a "direction". The "direction" may be either "DESC" (descending) or +"ASC" (ascending). No other keywords may be used in the 'order_by' dictionary. + +You may only use 'order_by' if you supply a 'group_by'. You may only use 'limit' if you supply an 'order_by'. + +.. code-block:: python + + # This will run a count query with results grouped by zip code. + # It will display only the top ten zip code results based upon how many times + # users in those zip codes logged in. + keen.count(event_collection="logins", timeframe="this_2_days", group_by="zip_code", "limit"=10, + order_by={"property_name": "result", "direction": "DESC"}) + + Check Batch Upload Response For Errors '''''''''''''''''''''''''''''''''''''' From 50c6ad407e35eb04666a1d943947734fddb2934f Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 28 Sep 2017 12:09:28 -0600 Subject: [PATCH 163/224] response to doc review --- README.rst | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/README.rst b/README.rst index 4e7bbcb..94d5cbb 100644 --- a/README.rst +++ b/README.rst @@ -101,6 +101,21 @@ For more code samples, take a look at Keen's `docs 3 keen.select_unique("purchases", target_property="user.email", timeframe="this_14_days") # => ["bob@aol.com", "joe@yahoo.biz"] + # Alpha support for ordering your results and limiting what is displayed is now supported in the Python SDK. + # Keep in mind that even if you limit your results with the 'limit' keyword, you are still querying over the + # normal amount of data, and thus your compute costs will not change. Limit only changes what is displayed. + + # The keyword "limit" must be a positive integer. The keyword "order_by" must be a dictionary with a required + # "property_name" specified and optionally a "direction". The "direction" may be either "DESC" (descending) or + # "ASC" (ascending). No other keywords may be used in the "order_by" dictionary. + + # You may only use "order_by" if you supply a "group_by". You may only use "limit" if you supply an "order_by". + + # This will run a count query with results grouped by zip code. + # It will display only the top ten zip code results based upon how many times users in those zip codes logged in. + keen.count("purchases", group_by="zip_code", timeframe="this_14_days", "limit"=10, + order_by={"property_name": "result", "direction": "DESC"}) + keen.extraction("purchases", timeframe="today") # => [{ "price" => 20, ... }, { ... }] keen.multi_analysis( @@ -149,28 +164,6 @@ Advanced Usage See below for more options. -Order Your Grouped Results with order_by and limit -'''''''''''''''''''''''''''''''''''''''''''''''''' - -Alpha support for ordering your results and limiting what is displayed is now supported in the Python SDK. -Keep in mind that even if you limit your results with the 'limit' keyword, you are still querying over the -normal amount of data, and thus your compute costs will not change. Limit only changes what is displayed. - -The keyword 'limit' must be a positive integer. The keyword 'order_by' must be a dictionary with a required -"property_name" specified and optionally a "direction". The "direction" may be either "DESC" (descending) or -"ASC" (ascending). No other keywords may be used in the 'order_by' dictionary. - -You may only use 'order_by' if you supply a 'group_by'. You may only use 'limit' if you supply an 'order_by'. - -.. code-block:: python - - # This will run a count query with results grouped by zip code. - # It will display only the top ten zip code results based upon how many times - # users in those zip codes logged in. - keen.count(event_collection="logins", timeframe="this_2_days", group_by="zip_code", "limit"=10, - order_by={"property_name": "result", "direction": "DESC"}) - - Check Batch Upload Response For Errors '''''''''''''''''''''''''''''''''''''' From d49e3d3211736fb778a7bd16af63166dfa7df242 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 28 Sep 2017 12:10:16 -0600 Subject: [PATCH 164/224] displayed->returned --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 94d5cbb..587461d 100644 --- a/README.rst +++ b/README.rst @@ -101,7 +101,7 @@ For more code samples, take a look at Keen's `docs 3 keen.select_unique("purchases", target_property="user.email", timeframe="this_14_days") # => ["bob@aol.com", "joe@yahoo.biz"] - # Alpha support for ordering your results and limiting what is displayed is now supported in the Python SDK. + # Alpha support for ordering your results and limiting what is returned is now supported in the Python SDK. # Keep in mind that even if you limit your results with the 'limit' keyword, you are still querying over the # normal amount of data, and thus your compute costs will not change. Limit only changes what is displayed. From e2b0fe371f1fe9fbaa56883e2783b00fbe88836b Mon Sep 17 00:00:00 2001 From: Taylor Barnett Date: Thu, 28 Sep 2017 16:41:37 -0700 Subject: [PATCH 165/224] add existing Keen CoC --- CODE_OF_CONDUCT.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..19d751d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,3 @@ +# Keen IO Community Code of Conduct + +The Keen IO Community is dedicated to providing a safe, inclusive, welcoming, and harassment-free space and experience for all community participants, regardless of gender identity and expression, sexual orientation, disability, physical appearance, socioeconomic status, body size, ethnicity, nationality, level of experience, age, religion (or lack thereof), or other identity markers. Our Code of Conduct exists because of that dedication, and we do not tolerate harassment in any form. See our reporting guidelines [here](https://github.com/keen/community-code-of-conduct/blob/master/incident-reporting.md). Our full Code of Conduct can be found at this [link](https://github.com/keen/community-code-of-conduct/blob/master/long-form-code-of-conduct.md). From 895db913cc82dd90b67b4f42d061c14cb07c1c4f Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 5 Oct 2017 09:11:35 -0600 Subject: [PATCH 166/224] added order_by test --- keen/tests/client_tests.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 5af0a5c..fe5b2e5 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -364,6 +364,9 @@ class QueryTests(BaseTestCase): LIST_RESPONSE = MockedResponse( status_code=200, json_response={"result": [{"value": {"total": 1}}, {"value": {"total": 2}}]}) + LIST_RESPONSE_DESCENDING = MockedResponse( + status_code=200, json_response={"result": [{"value": {"total": 2}}, {"value": {"total": 1}}]}) + def setUp(self): super(QueryTests, self).setUp() keen._client = None @@ -492,6 +495,25 @@ def test_interval(self, get): resp = keen.count("query test", timeframe="this_2_days", interval="daily") self.assertEqual(type(resp), list) + def test_order_by(self, get): + get.return_value = self.LIST_RESPONSE_DESCENDING + collection = "query_test" + limit = 2 + order_by = {'property_name': 'result', 'direction': 'DESC'} + resp = keen.count(collection, timeframe="today", group_by="number", order_by=order_by, limit=limit) + self.assertTrue("https://api.keen.io/3.0/projects/{}/queries/count".format(keen.project_id) in + get.call_args[0][0]) + self.assertEqual(2, get.call_args[1]["params"]["limit"]) + self.assertEqual(collection, get.call_args[1]["params"]["event_collection"]) + # Order by should always be wrapped in a list, and stringified. + # This test is broken because JSON. It thinks double quoted strings != single quoted strings. + # ... annoying, to say the least. + # self.assertTrue(str([order_by]), get.call_args[1]["params"]["order_by"]) + # So instead let's use this only slightly useful test: + self.assertTrue("order_by" in get.call_args[1]["params"]) + self.assertTrue(keen.read_key in get.call_args[1]["headers"]["Authorization"]) + self.assertEqual(resp, self.LIST_RESPONSE_DESCENDING.json()["result"]) + def test_passing_invalid_custom_api_client(self, get): class CustomApiClient(object): api_version = "3.0" From 90421602b935773f591ffd0d25e6981c9b35083a Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 5 Oct 2017 09:25:41 -0600 Subject: [PATCH 167/224] see if this works with MOAR python --- keen/tests/client_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index fe5b2e5..cc92db2 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -499,7 +499,7 @@ def test_order_by(self, get): get.return_value = self.LIST_RESPONSE_DESCENDING collection = "query_test" limit = 2 - order_by = {'property_name': 'result', 'direction': 'DESC'} + order_by = {"property_name": "result", "direction": "DESC"} resp = keen.count(collection, timeframe="today", group_by="number", order_by=order_by, limit=limit) self.assertTrue("https://api.keen.io/3.0/projects/{}/queries/count".format(keen.project_id) in get.call_args[0][0]) From d2a630ad031a8649563de01abcf7b159f5ae249a Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 5 Oct 2017 09:34:59 -0600 Subject: [PATCH 168/224] debug only --- keen/api.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/keen/api.py b/keen/api.py index 21f6730..1ce9107 100644 --- a/keen/api.py +++ b/keen/api.py @@ -134,32 +134,39 @@ def _order_by_is_valid_or_none(self, params): :return: Returns True if either no order_by is present, or if the order_by is well-formed. """ if not "order_by" in params or not params["order_by"]: + print "couldn't find 'order_by'" return True def _order_by_dict_is_not_well_formed(d): if not isinstance(d, dict): + print "wasn't a good type" # Bad type. return True if "property_name" in d and d["property_name"]: if "direction" in d and not (d["direction"] == "ASC" or d["direction"] == "DESC"): # Bad direction provided. + print "found a bad direction" return True for k in d: if k != "property_name" and k != "direction": # Unexpected key. + print "found a weird key: {}".format(k) return True # Everything looks good! return False # Missing required key. + print "missing a needed key" return True # order_by is converted to a list before this point if it wasn't one before. order_by_list = json.loads(params["order_by"]) if filter(_order_by_dict_is_not_well_formed, order_by_list): # At least one order_by dict is broken. + print "a dict is broken" return False if not "group_by" in params or not params["group_by"]: # We must have group_by to have order_by make sense. + print "missing group_by" return False return True From f43dba5832bf90e62e525de7d52f116821522eb1 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 5 Oct 2017 09:47:29 -0600 Subject: [PATCH 169/224] debugging for newbs --- keen/api.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/keen/api.py b/keen/api.py index 1ce9107..9463899 100644 --- a/keen/api.py +++ b/keen/api.py @@ -1,3 +1,4 @@ +from __future__ import print_function # stdlib import json import ssl @@ -134,39 +135,39 @@ def _order_by_is_valid_or_none(self, params): :return: Returns True if either no order_by is present, or if the order_by is well-formed. """ if not "order_by" in params or not params["order_by"]: - print "couldn't find 'order_by'" + print("couldn't find 'order_by'") return True def _order_by_dict_is_not_well_formed(d): if not isinstance(d, dict): - print "wasn't a good type" + print("wasn't a good type") # Bad type. return True if "property_name" in d and d["property_name"]: if "direction" in d and not (d["direction"] == "ASC" or d["direction"] == "DESC"): # Bad direction provided. - print "found a bad direction" + print("found a bad direction") return True for k in d: if k != "property_name" and k != "direction": # Unexpected key. - print "found a weird key: {}".format(k) + print("found a weird key: {}".format(k)) return True # Everything looks good! return False # Missing required key. - print "missing a needed key" + print("missing a needed key") return True # order_by is converted to a list before this point if it wasn't one before. order_by_list = json.loads(params["order_by"]) if filter(_order_by_dict_is_not_well_formed, order_by_list): # At least one order_by dict is broken. - print "a dict is broken" + print("a dict is broken") return False if not "group_by" in params or not params["group_by"]: # We must have group_by to have order_by make sense. - print "missing group_by" + print("missing group_by") return False return True From 096494c7d6a6e4d5a635e6ff518f97ee666c0184 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 5 Oct 2017 11:32:33 -0600 Subject: [PATCH 170/224] Portability for python 3 filter returns an iterator in python 3. Porting libraries do some awkward stuff to compensate, such as wrapping an empty result in a list, making a filter call of filter(lambda x: False, ["a", "b"]) => [[]] So I added a helper function to always solve that. --- keen/api.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/keen/api.py b/keen/api.py index 9463899..78e8ad7 100644 --- a/keen/api.py +++ b/keen/api.py @@ -127,6 +127,20 @@ def post_events(self, events): self._error_handling(response) return self._get_response_json(response) + def _filtered_list_is_empty(self, lyst): + """ + Python 3 does some odd porting of the filter function. + To be safe, we'll handle any number of layers of nesting of filter functions recursively. + + :return: True if a python 2.7 version of an applied filter function would return an empty list. + """ + if len(lyst) == 0: + return True + elif len(lyst) == 1 and _filtered_list_is_empty(lyst[0]): + return True + else: + return False + def _order_by_is_valid_or_none(self, params): """ Validates that a given order_by has proper syntax. @@ -161,7 +175,8 @@ def _order_by_dict_is_not_well_formed(d): # order_by is converted to a list before this point if it wasn't one before. order_by_list = json.loads(params["order_by"]) - if filter(_order_by_dict_is_not_well_formed, order_by_list): + + if not self._filtered_list_is_empty(filter(_order_by_dict_is_not_well_formed, order_by_list)): # At least one order_by dict is broken. print("a dict is broken") return False From 7a2086fc93b05188998bd2b8029dc3a381a66c06 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 5 Oct 2017 13:16:26 -0600 Subject: [PATCH 171/224] don't even filter --- keen/api.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/keen/api.py b/keen/api.py index 78e8ad7..03f0e88 100644 --- a/keen/api.py +++ b/keen/api.py @@ -127,19 +127,6 @@ def post_events(self, events): self._error_handling(response) return self._get_response_json(response) - def _filtered_list_is_empty(self, lyst): - """ - Python 3 does some odd porting of the filter function. - To be safe, we'll handle any number of layers of nesting of filter functions recursively. - - :return: True if a python 2.7 version of an applied filter function would return an empty list. - """ - if len(lyst) == 0: - return True - elif len(lyst) == 1 and _filtered_list_is_empty(lyst[0]): - return True - else: - return False def _order_by_is_valid_or_none(self, params): """ @@ -176,10 +163,10 @@ def _order_by_dict_is_not_well_formed(d): # order_by is converted to a list before this point if it wasn't one before. order_by_list = json.loads(params["order_by"]) - if not self._filtered_list_is_empty(filter(_order_by_dict_is_not_well_formed, order_by_list)): - # At least one order_by dict is broken. - print("a dict is broken") - return False + for order_by in order_by_list: + if _order_by_dict_is_not_well_formed(order_by): + print("a dict is broken") + return False if not "group_by" in params or not params["group_by"]: # We must have group_by to have order_by make sense. print("missing group_by") From b0d0fe55e2bdd0f7c8049d33c46754eecc734488 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 5 Oct 2017 13:23:45 -0600 Subject: [PATCH 172/224] rm print debugs --- keen/api.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/keen/api.py b/keen/api.py index 03f0e88..a944c26 100644 --- a/keen/api.py +++ b/keen/api.py @@ -136,28 +136,23 @@ def _order_by_is_valid_or_none(self, params): :return: Returns True if either no order_by is present, or if the order_by is well-formed. """ if not "order_by" in params or not params["order_by"]: - print("couldn't find 'order_by'") return True def _order_by_dict_is_not_well_formed(d): if not isinstance(d, dict): - print("wasn't a good type") # Bad type. return True if "property_name" in d and d["property_name"]: if "direction" in d and not (d["direction"] == "ASC" or d["direction"] == "DESC"): # Bad direction provided. - print("found a bad direction") return True for k in d: if k != "property_name" and k != "direction": # Unexpected key. - print("found a weird key: {}".format(k)) return True # Everything looks good! return False # Missing required key. - print("missing a needed key") return True # order_by is converted to a list before this point if it wasn't one before. @@ -165,11 +160,9 @@ def _order_by_dict_is_not_well_formed(d): for order_by in order_by_list: if _order_by_dict_is_not_well_formed(order_by): - print("a dict is broken") return False if not "group_by" in params or not params["group_by"]: # We must have group_by to have order_by make sense. - print("missing group_by") return False return True From 959957ef680117f87fc21acb5bfc563f24f34366 Mon Sep 17 00:00:00 2001 From: Mamat Rahmat Date: Fri, 6 Oct 2017 04:13:16 +0700 Subject: [PATCH 173/224] Add docs Data Enrichment --- README.rst | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/README.rst b/README.rst index fe2657b..840db51 100644 --- a/README.rst +++ b/README.rst @@ -59,6 +59,68 @@ Once you've set `KEEN_PROJECT_ID` and `KEEN_WRITE_KEY`, sending events is simple "referred_by": "harry" }) +Data Enrichment +``````````````` + +Keen IO can enrich event data by parsing or joining it with other data sets. This is done through the concept of “add-ons”. To activate add-ons, you simply add some new properties within the "keen" namespace in your events. Detailed documentation for the configuration of our add-ons is available `here `_. + +Here is an example of using the `URL parser `_: + +.. code-block:: python + + keen.add_event("requests", { + "page_url" : "http://my-website.com/cool/link?source=twitter&foo=bar/#title", + "keen" : { + "addons" : [ + { + "name" : "keen:url_parser", + "input" : { + "url" : "page_url" + }, + "output" : "parsed_page_url" + } + ] + } + }) + +Keen IO will parse the URL for you and that would equivalent to: + +.. code-block:: python + + keen.add_event("request", { + "page_url" : "http://my-website.com/cool/link?source=twitter&foo=bar/#title", + "parsed_page_url": { + "protocol" : "http", + "domain" : "my-website.com", + "path" : "/cool/link", + "anchor" : "title", + "query_string" : { + "source" : "twitter", + "foo" : "bar" + } + } + }) + +Here is another example of using the `Datetime parser `_. Let's assume you want to do a deeper analysis on the "purchases" event by day of the week (Monday, Tuesday, Wednesday, etc.) and other interesting Datetime components. You can use "keen.timestamp" property that is included in your event automatically. + +.. code-block:: python + + keen.add_event("purchases", { + "keen": { + "addons": [ + { + "name": "keen:date_time_parser", + "input": { + "date_time" : "keen.timestamp" + }, + "output": "timestamp_info" + } + ] + }, + "price": 500 + }) + +Other data enrichment add-ons are located in the `API reference docs `_. Send Batch Events to Keen IO ```````````````````````````` From 74ed9646692ed66b10066048bfbf43fcfdeb6d24 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Fri, 6 Oct 2017 09:20:27 -0600 Subject: [PATCH 174/224] Response to review Fixed up some function documentation. Added a direction.py file to help the code stay slightly cleaner and, more importantly, allow users to avoid having to memorize string literals. --- README.rst | 10 ++++---- keen/__init__.py | 60 +++++++++++++++++++++++++++++++---------------- keen/api.py | 5 ++-- keen/client.py | 60 +++++++++++++++++++++++++++++++---------------- keen/direction.py | 14 +++++++++++ 5 files changed, 101 insertions(+), 48 deletions(-) create mode 100644 keen/direction.py diff --git a/README.rst b/README.rst index 587461d..c010379 100644 --- a/README.rst +++ b/README.rst @@ -102,19 +102,19 @@ For more code samples, take a look at Keen's `docs ["bob@aol.com", "joe@yahoo.biz"] # Alpha support for ordering your results and limiting what is returned is now supported in the Python SDK. - # Keep in mind that even if you limit your results with the 'limit' keyword, you are still querying over the + # Keep in mind that even if you limit your results with the "limit" keyword, you are still querying over the # normal amount of data, and thus your compute costs will not change. Limit only changes what is displayed. # The keyword "limit" must be a positive integer. The keyword "order_by" must be a dictionary with a required - # "property_name" specified and optionally a "direction". The "direction" may be either "DESC" (descending) or - # "ASC" (ascending). No other keywords may be used in the "order_by" dictionary. + # "property_name" specified and optionally a "direction". The "direction" may be either keen.direction.DESCENDING or + # keen.direction.ASCENDING. No other keywords may be used in the "order_by" dictionary. # You may only use "order_by" if you supply a "group_by". You may only use "limit" if you supply an "order_by". # This will run a count query with results grouped by zip code. # It will display only the top ten zip code results based upon how many times users in those zip codes logged in. - keen.count("purchases", group_by="zip_code", timeframe="this_14_days", "limit"=10, - order_by={"property_name": "result", "direction": "DESC"}) + keen.count("purchases", group_by="zip_code", timeframe="this_14_days", limit=10, + order_by={"property_name": "result", "direction": keen.direction.DESCENDING}) keen.extraction("purchases", timeframe="today") # => [{ "price" => 20, ... }, { ... }] diff --git a/keen/__init__.py b/keen/__init__.py index 3205bd6..e71874a 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -91,8 +91,10 @@ def count(event_collection, timeframe=None, timezone=None, interval=None, filter example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -122,8 +124,10 @@ def sum(event_collection, target_property, timeframe=None, timezone=None, interv example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -153,8 +157,10 @@ def minimum(event_collection, target_property, timeframe=None, timezone=None, in example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -184,8 +190,10 @@ def maximum(event_collection, target_property, timeframe=None, timezone=None, in example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -215,8 +223,10 @@ def average(event_collection, target_property, timeframe=None, timezone=None, in example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -246,8 +256,10 @@ def median(event_collection, target_property, timeframe=None, timezone=None, int example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -279,8 +291,10 @@ def percentile(event_collection, target_property, percentile, timeframe=None, ti example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -320,8 +334,10 @@ def count_unique(event_collection, target_property, timeframe=None, timezone=Non example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -351,8 +367,10 @@ def select_unique(event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -428,8 +446,10 @@ def multi_analysis(event_collection, analyses, timeframe=None, interval=None, ti example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group you results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds diff --git a/keen/api.py b/keen/api.py index a944c26..963d161 100644 --- a/keen/api.py +++ b/keen/api.py @@ -1,4 +1,3 @@ -from __future__ import print_function # stdlib import json import ssl @@ -9,7 +8,7 @@ from requests.packages.urllib3.poolmanager import PoolManager # keen -from keen import exceptions, utilities +from keen import direction, exceptions, utilities from keen.utilities import KeenKeys, requires_key # json @@ -143,7 +142,7 @@ def _order_by_dict_is_not_well_formed(d): # Bad type. return True if "property_name" in d and d["property_name"]: - if "direction" in d and not (d["direction"] == "ASC" or d["direction"] == "DESC"): + if "direction" in d and not direction.is_valid_direction(d["direction"]): # Bad direction provided. return True for k in d: diff --git a/keen/client.py b/keen/client.py index 3c172f9..38a4c01 100644 --- a/keen/client.py +++ b/keen/client.py @@ -229,8 +229,10 @@ def count(self, event_collection, timeframe=None, timezone=None, interval=None, example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -259,8 +261,10 @@ def sum(self, event_collection, target_property, timeframe=None, timezone=None, example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -289,8 +293,10 @@ def minimum(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -319,8 +325,10 @@ def maximum(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -349,8 +357,10 @@ def average(self, event_collection, target_property, timeframe=None, timezone=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -379,8 +389,10 @@ def median(self, event_collection, target_property, timeframe=None, timezone=Non example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -411,8 +423,10 @@ def percentile(self, event_collection, target_property, percentile, timeframe=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -451,8 +465,10 @@ def count_unique(self, event_collection, target_property, timeframe=None, timezo example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -481,8 +497,10 @@ def select_unique(self, event_collection, target_property, timeframe=None, timez example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds @@ -562,8 +580,10 @@ def multi_analysis(self, event_collection, analyses, timeframe=None, interval=No example: [{"property_name":"device", "operator":"eq", "property_value":"iPhone"}] :param group_by: string or array of strings, the name(s) of the properties you would like to group your results by. example: "customer.id" or ["browser","operating_system"] - :param order_by: dictionary object containing the property_name to order by and the - desired direction of sorting. Example: {"property_name":"result", "direction":"DESC"} + :param order_by: dictionary or list of dictionary objects containing the property_name(s) + to order by and the desired direction(s) of sorting. + Example: {"property_name":"result", "direction":keen.direction.DESCENDING} + May not be used without a group_by specified. :param limit: positive integer limiting the displayed results of a query using order_by :param max_age: an integer, greater than 30 seconds, the maximum 'staleness' you're willing to trade for increased query performance, in seconds diff --git a/keen/direction.py b/keen/direction.py new file mode 100644 index 0000000..fbbd36a --- /dev/null +++ b/keen/direction.py @@ -0,0 +1,14 @@ +# Helper file for avoiding string literals prone to typos with order_by. + +ASCENDING = "ASC" +DESCENDING = "DESC" + +def is_valid_direction(direction_string): + """ + Determines whether the given string is a valid direction string + for order_by. + + :param direction_string: A string representing an order_by direction. + :return: True if the string is a valid direction string, False otherwise. + """ + return direction_string in [ASCENDING, DESCENDING] From 232dbeec17e069712e5339d043bee37e54718a69 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Fri, 6 Oct 2017 09:37:28 -0600 Subject: [PATCH 175/224] Added more order_by tests. --- keen/tests/client_tests.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index cc92db2..62dea35 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -499,7 +499,7 @@ def test_order_by(self, get): get.return_value = self.LIST_RESPONSE_DESCENDING collection = "query_test" limit = 2 - order_by = {"property_name": "result", "direction": "DESC"} + order_by = {"property_name": "result", "direction": keen.direction.DESCENDING} resp = keen.count(collection, timeframe="today", group_by="number", order_by=order_by, limit=limit) self.assertTrue("https://api.keen.io/3.0/projects/{}/queries/count".format(keen.project_id) in get.call_args[0][0]) @@ -514,6 +514,31 @@ def test_order_by(self, get): self.assertTrue(keen.read_key in get.call_args[1]["headers"]["Authorization"]) self.assertEqual(resp, self.LIST_RESPONSE_DESCENDING.json()["result"]) + def test_order_by_invalid_limit(self, get): + collection = "query_test" + limit = -1 # Limit should be positive + order_by = {"property_name": "result", "direction": keen.direction.DESCENDING} + self.assertRaises(ValueError, keen.count, collection, timeframe="today", group_by="number", order_by=order_by, + limit=limit) + + def test_order_by_invalid_direction(self, get): + collection = "query_test" + limit = 2 + order_by = {"property_name": "result", "direction": "INVALID"} + self.assertRaises(ValueError, keen.count, collection, timeframe="today", group_by="number", order_by=order_by, + limit=limit) + + def test_order_by_no_group_by(self, get): + collection = "query_test" + limit = 2 + order_by = {"property_name": "result", "direction": keen.direction.DESCENDING} + self.assertRaises(ValueError, keen.count, collection, timeframe="today", order_by=order_by, limit=limit) + + def test_limit_no_order_by(self, get): + collection = "query_test" + limit = 2 + self.assertRaises(ValueError, keen.count, collection, timeframe="today", group_by="number", limit=limit) + def test_passing_invalid_custom_api_client(self, get): class CustomApiClient(object): api_version = "3.0" From f0ab3dc078e37db1c8b0ec425694b4fcaba9fa70 Mon Sep 17 00:00:00 2001 From: Cameron Yick Date: Mon, 9 Oct 2017 17:56:04 -0400 Subject: [PATCH 176/224] Break CHANGELOG into separate file --- CHANGELOG.rst | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.rst | 222 +------------------------------------------------ 2 files changed, 225 insertions(+), 221 deletions(-) create mode 100644 CHANGELOG.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..185eeb8 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,224 @@ +Changelog +--------- + +0.4.0 +`````` + ++ SavedQueriesInterface.create() now accepts a dict as the query definition. ++ get_collection() and get_all_collections() now only require a Read Key instead of Master. ++ SavedQueriesInterface.update() now performs partial updates. update_full() exhibits old behavior. ++ Misc documentation updates. + +0.3.31 +`````` + ++ Fix broken releases. + +0.3.29 +`````` + ++ Add Keen-Sdk header to all requests, containing the SDK version. + +0.3.28 +`````` + ++ Fix incorrect README. + +0.3.27 +`````` + ++ Return JSON response when uploading events in a batch. + +0.3.26 +`````` + ++ Removed unused `Padding` from requirements.txt to make python 3.x installs cleaner. + +0.3.25 +`````` + ++ Replaced defunct `pycrypto` library with `cryptodome`. ++ Fixed UnicodeDecodeError under PY3 while installing in Windows. + +0.3.24 +`````` + ++ Updated documentation + +0.3.23 +`````` + ++ Added status code to JSON parse error response + +0.3.22 +`````` + ++ Added support for python 3.5 + +0.3.21 +`````` + ++ Fixed bug with scoped key generation not working with newer Keen projects. + +0.3.20 +`````` + ++ Added `saved_queries` support ++ Added Python 3.4 support + +0.3.19 +`````` + ++ Added `base_url` as a possible env variable + +0.3.18 +`````` + ++ Updated error handling to except `ValueError` + +0.3.17 +`````` + ++ Fixed timestamp overriding keen addons ++ Added `get_collection` and `get_all_collections` methods + +0.3.16 +`````` + ++ Added `all_keys` parameter which allows users to expose all keys in query response. ++ Added `delete_events` method. + +0.3.15 +`````` + ++ Added better error handling to surface all errors from HTTP API calls. + +0.3.14 +`````` + ++ Added compatibility for pip 1.0 + +0.3.13 +`````` + ++ Added compatibility for pip < 1.5.6 + +0.3.12 +`````` + ++ Made requirements more flexible. + +0.3.11 +`````` + ++ Added `requirements.txt` to pypi package. + +0.3.10 +`````` + ++ Fixed requirements in `setup.py` ++ Updated test inputs and documentation. + +0.3.9 +````` + ++ Added ```master_key``` parameter. + +0.3.8 +````` + ++ Mocked tests. ++ Added ```median``` query method. ++ Added support for `$python setup.py test`. + +0.3.7 +````` + ++ Upgraded to requests==2.5.1 + +0.3.6 +````` + ++ Added ```max_age``` parameter for caching. + +0.3.5 +````` + ++ Added client configurable timeout to gets. + +0.3.4 +````` + ++ Added ```percentile``` query method. + +0.3.3 +````` + ++ Support ```interval``` parameter for multi analyses on the keen module. + +0.3.2 +````` + ++ Reuse internal requests' session inside an instance of KeenApi. + +0.3.1 +````` + ++ Support ```property_names``` parameter for extractions. + +0.3.0 +````` + ++ Added client configurable timeout to posts. ++ Upgraded to requests==2.2.1. + +0.2.3 +````` + ++ Fixed sys.version_info issue with Python 2.6. + +0.2.2 +````` + ++ Added interval to multi_analysis. + +0.2.1 +````` + ++ Added stacktrace_id and unique_id to Keen API errors. + +0.2.0 +````` + ++ Added add_events method to keen/__init__.py so it can be used at a module level. ++ Added method to generate image beacon URLs. + +0.1.9 +````` + ++ Added support for publishing events in batches ++ Added support for configuring client automatically from environment ++ Added methods on keen module directly + +0.1.8 +````` + ++ Added querying support + +0.1.7 +````` + ++ Bugfix to use write key when sending events - do not use 0.1.6! + +0.1.6 +````` + ++ Changed project token -> project ID. ++ Added support for read and write scoped keys. ++ Added support for generating scoped keys yourself. ++ Added support for python 2.6, 3.2, and 3.3 + +0.1.5 +````` + ++ Added documentation. diff --git a/README.rst b/README.rst index fe2657b..844d844 100644 --- a/README.rst +++ b/README.rst @@ -423,227 +423,7 @@ To run tests: Changelog --------- -0.4.0 -`````` - -+ SavedQueriesInterface.create() now accepts a dict as the query definition. -+ get_collection() and get_all_collections() now only require a Read Key instead of Master. -+ SavedQueriesInterface.update() now performs partial updates. update_full() exhibits old behavior. -+ Misc documentation updates. - -0.3.31 -`````` - -+ Fix broken releases. - -0.3.29 -`````` - -+ Add Keen-Sdk header to all requests, containing the SDK version. - -0.3.28 -`````` - -+ Fix incorrect README. - -0.3.27 -`````` - -+ Return JSON response when uploading events in a batch. - -0.3.26 -`````` - -+ Removed unused `Padding` from requirements.txt to make python 3.x installs cleaner. - -0.3.25 -`````` - -+ Replaced defunct `pycrypto` library with `cryptodome`. -+ Fixed UnicodeDecodeError under PY3 while installing in Windows. - -0.3.24 -`````` - -+ Updated documentation - -0.3.23 -`````` - -+ Added status code to JSON parse error response - -0.3.22 -`````` - -+ Added support for python 3.5 - -0.3.21 -`````` - -+ Fixed bug with scoped key generation not working with newer Keen projects. - -0.3.20 -`````` - -+ Added `saved_queries` support -+ Added Python 3.4 support - -0.3.19 -`````` - -+ Added `base_url` as a possible env variable - -0.3.18 -`````` - -+ Updated error handling to except `ValueError` - -0.3.17 -`````` - -+ Fixed timestamp overriding keen addons -+ Added `get_collection` and `get_all_collections` methods - -0.3.16 -`````` - -+ Added `all_keys` parameter which allows users to expose all keys in query response. -+ Added `delete_events` method. - -0.3.15 -`````` - -+ Added better error handling to surface all errors from HTTP API calls. - -0.3.14 -`````` - -+ Added compatibility for pip 1.0 - -0.3.13 -`````` - -+ Added compatibility for pip < 1.5.6 - -0.3.12 -`````` - -+ Made requirements more flexible. - -0.3.11 -`````` - -+ Added `requirements.txt` to pypi package. - -0.3.10 -`````` - -+ Fixed requirements in `setup.py` -+ Updated test inputs and documentation. - -0.3.9 -````` - -+ Added ```master_key``` parameter. - -0.3.8 -````` - -+ Mocked tests. -+ Added ```median``` query method. -+ Added support for `$python setup.py test`. - -0.3.7 -````` - -+ Upgraded to requests==2.5.1 - -0.3.6 -````` - -+ Added ```max_age``` parameter for caching. - -0.3.5 -````` - -+ Added client configurable timeout to gets. - -0.3.4 -````` - -+ Added ```percentile``` query method. - -0.3.3 -````` - -+ Support ```interval``` parameter for multi analyses on the keen module. - -0.3.2 -````` - -+ Reuse internal requests' session inside an instance of KeenApi. - -0.3.1 -````` - -+ Support ```property_names``` parameter for extractions. - -0.3.0 -````` - -+ Added client configurable timeout to posts. -+ Upgraded to requests==2.2.1. - -0.2.3 -````` - -+ Fixed sys.version_info issue with Python 2.6. - -0.2.2 -````` - -+ Added interval to multi_analysis. - -0.2.1 -````` - -+ Added stacktrace_id and unique_id to Keen API errors. - -0.2.0 -````` - -+ Added add_events method to keen/__init__.py so it can be used at a module level. -+ Added method to generate image beacon URLs. - -0.1.9 -````` - -+ Added support for publishing events in batches -+ Added support for configuring client automatically from environment -+ Added methods on keen module directly - -0.1.8 -````` - -+ Added querying support - -0.1.7 -````` - -+ Bugfix to use write key when sending events - do not use 0.1.6! - -0.1.6 -````` - -+ Changed project token -> project ID. -+ Added support for read and write scoped keys. -+ Added support for generating scoped keys yourself. -+ Added support for python 2.6, 3.2, and 3.3 - -0.1.5 -````` - -+ Added documentation. +This project is in alpha stage at version 0.4.0 . See the full CHANGELOG `here <./CHANGELOG.rst>`_. To Do ----- From bc1d9c27ecf81088f59b2f8563956c7959aea84a Mon Sep 17 00:00:00 2001 From: Cameron Yick Date: Mon, 9 Oct 2017 18:08:47 -0400 Subject: [PATCH 177/224] Create initial contributions guidelines This is mostly Dustin Larimer's work, with tweaking for Python --- CONTRIBUTING.md | 87 +++++++++++++++++++++++++++++++++++++++++++++++++ README.rst | 2 +- 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b6849a9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,87 @@ +# Keen IO Python Client + +First off, thank you for considering contributing to this official Keen IO client. It's people like you that make Keen IO such a great tool. + +We put these guidelines together to try and make working with our SDK as straight forward as possible, and hopefully help you understand how we communicate about potential changes and improvements. + +Improving documentation, bug triaging, building modules for various frameworks or writing tutorials are all examples of helpful contributions we really appreciate. + +Please, don't use the issue tracker for support questions. If you have a support question please come hang out in our Slack at http://keen.chat or send an email to team@keen.io. + +## Guidelines + +* Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback. +* Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. See the [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/). + +## Your First Contribution + +Here are a couple of friendly tutorials with more information about contributing to OSS projects: + +- http://makeapullrequest.com/, +- http://www.firsttimersonly.com/ +-[How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github) +- [Github's Open Source Guide](https://opensource.guide) + +At this point, you're ready to make your changes! Feel free to ask for help; everyone is a beginner at first :smile_cat: + +If a maintainer asks you to "rebase" your PR, they're saying that a lot of code has changed, and that you need to update your branch so it's easier to merge. + +### Run the following commands to get this project installed locally + +```ssh +$ git clone https://github.com/keenlabs/KeenClient-Python && cd KeenClient-Python +# Make sure you're in a python virtual environment before installing +$ python setup.py develop + +# Run tests locally +$ python setup.py test +``` + +### Submitting a Pull Request + +Use the template below. If certain testing steps are not relevant, specify that in the PR. If additional checks are needed, add 'em! Please run through all testing steps before asking for a review. + +``` +## What does this PR do? How does it affect users? + +## How should this be tested? + +Step through the code line by line. Things to keep in mind as you review: + - Are there any edge cases not covered by this code? + - Does this code follow conventions (naming, formatting, modularization, etc) where applicable? + +Fetch the branch and/or deploy to staging to test the following: + +- [ ] Does the code compile without warnings (check shell, console)? +- [ ] Do all tests pass? +- [ ] Does the UI, pixel by pixel, look exactly as expected (check various screen sizes, including mobile)? +- [ ] If the feature makes requests from the browser, inspect them in the Web Inspector. Do they look as expected (parameters, headers, etc)? +- [ ] If the feature sends data to Keen, is the data visible in the project if you run an extraction (include link to collection/query)? +- [ ] If the feature saves data to a database, can you confirm the data is indeed created in the database? + +## Related tickets? +``` + +## How to report a bug +If you find a security vulnerability, do NOT open an issue. Email team@keen.io instead. + +If you find a bug that's not a security vulnerability please head over to the issues tab of this rep and open up an issue. + +We created these labels to help us organize issues: + +-`bugs` +-`docs` +-`enhancements` +-`feature-request` + +Please use them when creating an issue where it makes sense! + +## Suggesting features + +We welcome your feedback and requests. If you have a straight forward request please open up an issue that details the request. If you want to talk to someone on the Keen team head over to http://keen.chat or send a note to team@keen.io and we will make sure and get you in touch with the product team. + +# Code review process + +The core team looks at Pull Requests and issues on a regular basis and will typically respond within 5 business days. + + \ No newline at end of file diff --git a/README.rst b/README.rst index 844d844..f6f750e 100644 --- a/README.rst +++ b/README.rst @@ -440,7 +440,7 @@ report them via Github Issues. We'd love to hear your feedback and ideas! Contributing ------------ -This is an open source project and we love involvement from the community! Hit us up with pull requests and issues. +This is an open source project and we love involvement from the community! Hit us up with pull requests and issues. Here are our `guidelines <./CONTRIBUTING.md>`_ for how to contribute. .. |build-status| image:: https://secure.travis-ci.org/keenlabs/KeenClient-Python.png :target: http://travis-ci.org/keenlabs/KeenClient-Python From 53bb3e578bebc219ca5246d735e2b99b62830c1d Mon Sep 17 00:00:00 2001 From: Cameron Yick Date: Mon, 9 Oct 2017 18:15:41 -0400 Subject: [PATCH 178/224] Create PR template, remove Webapp Related Directions Remove checklist items that don't apply to a Python client --- .github/PULL_REQUEST_TEMPLATE.md | 16 ++++++++++++++++ CONTRIBUTING.md | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..bfec0e5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,16 @@ +## What does this PR do? How does it affect users? + +## How should this be tested? + +Step through the code line by line. Things to keep in mind as you review: + - Are there any edge cases not covered by this code? + - Does this code follow conventions (naming, formatting, modularization, etc) where applicable? + +Fetch the branch and/or deploy to staging to test the following: + +- [ ] Does the code compile without warnings (check shell, console)? +- [ ] Do all tests pass? +- [ ] If the feature sends data to Keen, is the data visible in the project if you run an extraction (include link to collection/query)? +- [ ] If the feature saves data to a database, can you confirm the data is indeed created in the database? + +## Related tickets? diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6849a9..bb5e78f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,14 +54,14 @@ Fetch the branch and/or deploy to staging to test the following: - [ ] Does the code compile without warnings (check shell, console)? - [ ] Do all tests pass? -- [ ] Does the UI, pixel by pixel, look exactly as expected (check various screen sizes, including mobile)? -- [ ] If the feature makes requests from the browser, inspect them in the Web Inspector. Do they look as expected (parameters, headers, etc)? - [ ] If the feature sends data to Keen, is the data visible in the project if you run an extraction (include link to collection/query)? - [ ] If the feature saves data to a database, can you confirm the data is indeed created in the database? ## Related tickets? ``` +This PR template can be viewed rendered in Markdown [here](./.github/PULL_REQUEST_TEMPLATE.md). Github will auto-populate any new PRs filed with this template, so don't worry about copy-pasting it. + ## How to report a bug If you find a security vulnerability, do NOT open an issue. Email team@keen.io instead. From 5e8b4a424e6e21cb0aaaff85a082b44774af1196 Mon Sep 17 00:00:00 2001 From: Cameron Yick Date: Mon, 9 Oct 2017 18:18:13 -0400 Subject: [PATCH 179/224] Reuse Contributing Message from keen-js Use the same sentence for consistency. --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f6f750e..c35fb06 100644 --- a/README.rst +++ b/README.rst @@ -440,7 +440,9 @@ report them via Github Issues. We'd love to hear your feedback and ideas! Contributing ------------ -This is an open source project and we love involvement from the community! Hit us up with pull requests and issues. Here are our `guidelines <./CONTRIBUTING.md>`_ for how to contribute. +This is an open source project and we love involvement from the community! Hit us up with pull requests and issues. + +`Learn more about contributing to this project <./CONTRIBUTING.md>`_. .. |build-status| image:: https://secure.travis-ci.org/keenlabs/KeenClient-Python.png :target: http://travis-ci.org/keenlabs/KeenClient-Python From 8746706ecb824e1e724bf12dc4112b581015a86b Mon Sep 17 00:00:00 2001 From: Cameron Yick Date: Mon, 9 Oct 2017 18:28:06 -0400 Subject: [PATCH 180/224] Create Issue Template, Update Links to Issues Tab --- .github/ISSUE_TEMPLATE.md | 27 +++++++++++++++++++++++++++ CONTRIBUTING.md | 3 ++- README.rst | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..944b6c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,27 @@ +Welcome to the Keen Python Client GitHub repo! 👋🎉 + +- Please [**search for existing issues**](https://github.com/keenlabs/KeenClient-Python/issues) in order to ensure we don't have duplicate bugs/feature requests. +- Please be respectful and considerate of others when commenting on issues +- Found a bug🐜? + - If it's a security vulnerability, please email team@keen.io instead of filing an issue + - If not, please fill out the sections below... thank you 😎! + +### Issue Summary + +A summary of the issue and the browser/OS environment in which it occurs. + +Stack traces, error messages, and/or screenshots can be very helpful here. + +Note: DO NOT include your credentials in ANY code examples, descriptions, or media you make public. + +### Steps to Reproduce + +1. This is the first step +2. This is the second step, etc. + +Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead? + +### Technical details: + +* Python Version: +* Operating System (OSX Sierra 10.12.6, Windows 10, etc): diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb5e78f..336f983 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,9 +63,10 @@ Fetch the branch and/or deploy to staging to test the following: This PR template can be viewed rendered in Markdown [here](./.github/PULL_REQUEST_TEMPLATE.md). Github will auto-populate any new PRs filed with this template, so don't worry about copy-pasting it. ## How to report a bug + If you find a security vulnerability, do NOT open an issue. Email team@keen.io instead. -If you find a bug that's not a security vulnerability please head over to the issues tab of this rep and open up an issue. +If you find a bug that's not a security vulnerability please head over to the `issues <./issues>`_ page, and file a new report. We created these labels to help us organize issues: diff --git a/README.rst b/README.rst index c35fb06..009bc61 100644 --- a/README.rst +++ b/README.rst @@ -435,7 +435,7 @@ Questions & Support ------------------- If you have any questions, bugs, or suggestions, please -report them via Github Issues. We'd love to hear your feedback and ideas! +report them via Github `Issues <./issues>`_. We'd love to hear your feedback and ideas! Contributing ------------ From 3abd596418ebe1a928f590c2c5feb870024abcfc Mon Sep 17 00:00:00 2001 From: Taylor Barnett Date: Tue, 10 Oct 2017 15:30:15 -0500 Subject: [PATCH 181/224] small wording changes --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 840db51..d143a75 100644 --- a/README.rst +++ b/README.rst @@ -62,7 +62,7 @@ Once you've set `KEEN_PROJECT_ID` and `KEEN_WRITE_KEY`, sending events is simple Data Enrichment ``````````````` -Keen IO can enrich event data by parsing or joining it with other data sets. This is done through the concept of “add-ons”. To activate add-ons, you simply add some new properties within the "keen" namespace in your events. Detailed documentation for the configuration of our add-ons is available `here `_. +Keen IO can enrich event data by parsing or joining it with other data properties. This is done through the concept of “add-ons”. To activate add-ons, you simply add some new properties within the "keen" namespace in your events. Detailed documentation for the configuration of our add-ons is available `here `_. Here is an example of using the `URL parser `_: @@ -120,7 +120,7 @@ Here is another example of using the `Datetime parser `_. +Other Data Enrichment add-ons are located in the `API reference docs `_. Send Batch Events to Keen IO ```````````````````````````` From 60410366681a6cf13e3145a4cda50aa1becdd773 Mon Sep 17 00:00:00 2001 From: Taylor Barnett Date: Tue, 10 Oct 2017 15:34:21 -0500 Subject: [PATCH 182/224] add data enrichment definition --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d143a75..fda375a 100644 --- a/README.rst +++ b/README.rst @@ -62,7 +62,7 @@ Once you've set `KEEN_PROJECT_ID` and `KEEN_WRITE_KEY`, sending events is simple Data Enrichment ``````````````` -Keen IO can enrich event data by parsing or joining it with other data properties. This is done through the concept of “add-ons”. To activate add-ons, you simply add some new properties within the "keen" namespace in your events. Detailed documentation for the configuration of our add-ons is available `here `_. +A data enrichment is a powerful add-on to enrich the data you're already streaming to Keen IO by pre-processing the data and adding helpful data properties. To activate add-ons, you simply add some new properties within the "keen" namespace in your events. Detailed documentation for the configuration of our add-ons is available `here `_. Here is an example of using the `URL parser `_: From c13008c6080a8afc1fbeb73a852bd6dca0661296 Mon Sep 17 00:00:00 2001 From: Taylor Barnett Date: Tue, 10 Oct 2017 16:54:49 -0500 Subject: [PATCH 183/224] change Keen -> Keen IO --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 944b6c1..4741522 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,4 @@ -Welcome to the Keen Python Client GitHub repo! 👋🎉 +Welcome to the Keen IO Python Client GitHub repo! 👋🎉 - Please [**search for existing issues**](https://github.com/keenlabs/KeenClient-Python/issues) in order to ensure we don't have duplicate bugs/feature requests. - Please be respectful and considerate of others when commenting on issues From 13c4d37c82d1f00eaa452479b19db7c9ec927956 Mon Sep 17 00:00:00 2001 From: Cameron Yick Date: Tue, 10 Oct 2017 21:47:33 -0400 Subject: [PATCH 184/224] Expand Issue Template, Update Community Conduct Guidelines to Latest --- .github/ISSUE_TEMPLATE.md | 11 ++++++++++- CONTRIBUTING.md | 7 ++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 4741522..70ca310 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -10,7 +10,15 @@ Welcome to the Keen IO Python Client GitHub repo! 👋🎉 A summary of the issue and the browser/OS environment in which it occurs. -Stack traces, error messages, and/or screenshots can be very helpful here. +Things you can include that are helpful + +- Code samples for what produces the error (or a link if the code is long) +- Error mesages / stack traces +- Screenshots can be very helpful here. + +```python +print("Hello world") +``` Note: DO NOT include your credentials in ANY code examples, descriptions, or media you make public. @@ -25,3 +33,4 @@ Any other info e.g. Why do you consider this to be a bug? What did you expect to * Python Version: * Operating System (OSX Sierra 10.12.6, Windows 10, etc): +* Frameworks + Versions (if applicable): diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 336f983..12314bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,12 @@ Please, don't use the issue tracker for support questions. If you have a support ## Guidelines * Create issues for any major changes and enhancements that you wish to make. Discuss things transparently and get community feedback. -* Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. See the [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/). +* Be welcoming to newcomers and encourage diverse new contributors from all backgrounds. +* Please take time to read our [Community Code of Conduct](#community-code-of-conduct) which includes Reporting Guidelines. + +## Community Code of Conduct + +The Keen IO Community is dedicated to providing a safe, inclusive, welcoming, and harassment-free space and experience for all community participants, regardless of gender identity and expression, sexual orientation, disability, physical appearance, socioeconomic status, body size, ethnicity, nationality, level of experience, age, religion (or lack thereof), or other identity markers. Our Code of Conduct exists because of that dedication, and we do not tolerate harassment in any form. See our reporting guidelines [here](https://github.com/keen/community-code-of-conduct/blob/master/incident-reporting.md). Our full Code of Conduct can be found at this [link](https://github.com/keen/community-code-of-conduct/blob/master/long-form-code-of-conduct.md). ## Your First Contribution From 312e12d19730626c2991f8f51e5effcdeb94d7d7 Mon Sep 17 00:00:00 2001 From: Cameron Yick Date: Tue, 10 Oct 2017 21:53:37 -0400 Subject: [PATCH 185/224] Add badge and remove stale TODOs from README.md --- README.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 009bc61..1382ed9 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ Keen IO Official Python Client Library ====================================== -|build-status| +|build-status| |pypi-version| This is the official Python Client for the `Keen IO `_ API. The Keen IO API lets developers build analytics features directly into their apps. @@ -425,11 +425,6 @@ Changelog This project is in alpha stage at version 0.4.0 . See the full CHANGELOG `here <./CHANGELOG.rst>`_. -To Do ------ - -* Asynchronous insert -* Scoped keys Questions & Support ------------------- @@ -447,3 +442,7 @@ This is an open source project and we love involvement from the community! Hit u .. |build-status| image:: https://secure.travis-ci.org/keenlabs/KeenClient-Python.png :target: http://travis-ci.org/keenlabs/KeenClient-Python :alt: Build status + +.. |pypi-version| image:: https://img.shields.io/pypi/v/keen.svg?maxAge=600 + :target: https://pypi.python.org/pypi/keen/ + :alt: Keen on PyPI From 0f70ddf9b7d7be60bdfbe1561464ec55ec097506 Mon Sep 17 00:00:00 2001 From: Cameron Yick Date: Tue, 10 Oct 2017 21:56:45 -0400 Subject: [PATCH 186/224] Use crisper Travis badge to match PyPI badge SVG is sharper than PNG --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1382ed9..1ee1327 100644 --- a/README.rst +++ b/README.rst @@ -439,7 +439,7 @@ This is an open source project and we love involvement from the community! Hit u `Learn more about contributing to this project <./CONTRIBUTING.md>`_. -.. |build-status| image:: https://secure.travis-ci.org/keenlabs/KeenClient-Python.png +.. |build-status| image:: https://img.shields.io/travis/keenlabs/KeenClient-Python.svg?maxAge=600 :target: http://travis-ci.org/keenlabs/KeenClient-Python :alt: Build status From f7c4f9d4ce71be88f95a47960ce84b25597464f9 Mon Sep 17 00:00:00 2001 From: Taylor Barnett Date: Wed, 11 Oct 2017 14:45:06 -0500 Subject: [PATCH 187/224] fix md list --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12314bf..3e5b6ab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,10 +75,10 @@ If you find a bug that's not a security vulnerability please head over to the `i We created these labels to help us organize issues: --`bugs` --`docs` --`enhancements` --`feature-request` +- `bugs` +- `docs` +- `enhancements` +- `feature-request` Please use them when creating an issue where it makes sense! @@ -90,4 +90,4 @@ We welcome your feedback and requests. If you have a straight forward request pl The core team looks at Pull Requests and issues on a regular basis and will typically respond within 5 business days. - \ No newline at end of file + From d837c84ef0c6e2db38646e07eb3c19a061d05f58 Mon Sep 17 00:00:00 2001 From: Devin Ekins Date: Mon, 16 Oct 2017 07:43:33 -0600 Subject: [PATCH 188/224] README mentions default direction --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c010379..96a9841 100644 --- a/README.rst +++ b/README.rst @@ -107,7 +107,8 @@ For more code samples, take a look at Keen's `docs Date: Wed, 13 Sep 2017 09:03:01 -0600 Subject: [PATCH 189/224] Added support for Access Keys. This does not include docs yet. --- keen/__init__.py | 59 +++++++++++++++++++++++++ keen/api.py | 110 ++++++++++++++++++++++++++++++++++++++++++++++- keen/client.py | 55 ++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 1 deletion(-) diff --git a/keen/__init__.py b/keen/__init__.py index e71874a..acb7eba 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -496,3 +496,62 @@ def get_all_collections(): """ _initialize_client_from_environment() return _client.get_all_collections() + +def create_access_key(name, is_active=True, permitted=[], options={}): + """ Creates a new access key. A master key must be set first. + + :param name: the name of the access key to create + :param is_active: Boolean value dictating whether this key is currently active (default True) + :param permitted: list of strings describing which operation types this key will permit + Legal values include "writes", "queries", "saved_queries", "cached_queries", + "datasets", and "schema". + :param options: dictionary containing more details about the key's permitted and restricted + functionality + """ + _initialize_client_from_environment() + return _client.create_access_key(name=name, is_active=is_active, + permitted=permitted, options=options) + +def list_access_keys(): + """ + Returns a list of all access keys in this project. A master key must be set first. + """ + _initialize_client_from_environment() + return _client.list_access_keys() + +def get_access_key(access_key_id): + """ + Returns details on a particular access key. A master key must be set first. + + :param access_key_id: the 'key' value of the access key to retreive data from + """ + _initialize_client_from_environment() + return _client.get_access_key(access_key_id) + +def update_access_key(access_key_id, name, is_active, permitted, options): + """ + Replaces the 'name', 'is_active', 'permitted', and 'options' values of a given key. + A master key must be set first. + + :param access_key_id: the 'key' value of the access key for which the values will be replaced + :param name: the new name desired for this access key + :param is_active: whether the key should become enabled (True) or revoked (False) + :param permitted: the new list of permissions desired for this access key + :param options: the new dictionary of options for this access key + """ + _initialize_client_from_environment() + return _client.update_access_key(access_key_id, name, is_active, permitted, options) + +def revoke_access_key(access_key_id): + """ + Revokes an access key. "Bad dog! No biscuit!" + """ + _initialize_client_from_environment() + return _client.revoke_access_key(access_key_id) + +def unrevoke_access_key(access_key_id): + """ + Re-enables an access key. + """ + _initialize_client_from_environment() + return _client.unrevoke_access_key(access_key_id) diff --git a/keen/api.py b/keen/api.py index 963d161..d3bb8aa 100644 --- a/keen/api.py +++ b/keen/api.py @@ -244,7 +244,7 @@ def get_collection(self, event_collection): @requires_key(KeenKeys.READ) def get_all_collections(self): """ - Extracts schema for all collections using the Keen IO API. A master key must be set first. + Extracts schema for all collections using the Keen IO API. A read key must be set first. """ @@ -255,6 +255,114 @@ def get_all_collections(self): return response.json() + @requires_key(KeenKeys.MASTER) + def create_access_key(self, name, is_active=True, permitted=[], options={}): + """ + Creates a new access key. A master key must be set first. + + :param name: the name of the access key to create + :param is_active: Boolean value dictating whether this key is currently active (default True) + :param permitted: list of strings describing which operation types this key will permit + Legal values include "writes", "queries", "saved_queries", "cached_queries", + "datasets", and "schema". + :param options: dictionary containing more details about the key's permitted and restricted + functionality + """ + + url = "{0}/{1}/projects/{2}/keys".format(self.base_url, self.api_version, self.project_id) + headers = utilities.headers(self.master_key) + + payload_dict = { + "name": name, + "is_active": is_active, + "permitted": permitted, + "options": options + } + payload = json.dumps(payload_dict) + + response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.get_timeout) + self._error_handling(response) + return response.json() + + @requires_key(KeenKeys.MASTER) + def list_access_keys(self): + """ + Returns a list of all access keys in this project. A master key must be set first. + """ + url = "{0}/{1}/projects/{2}/keys".format(self.base_url, self.api_version, self.project_id) + headers = utilities.headers(self.master_key) + response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) + self._error_handling(response) + + return response.json() + + @requires_key(KeenKeys.MASTER) + def get_access_key(self, access_key_id): + """ + Returns details on a particular access key. A master key must be set first. + + :param access_key_id: the 'key' value of the access key to retreive data from + """ + url = "{0}/{1}/projects/{2}/keys/{3}".format(self.base_url, self.api_version, self.project_id, + access_key_id) + headers = utilities.headers(self.master_key) + response = self.fulfill(HTTPMethods.GET, url, headers=headers, timeout=self.get_timeout) + self._error_handling(response) + + return response.json() + + @requires_key(KeenKeys.MASTER) + def update_access_key(self, access_key_id, name, is_active, permitted, options): + """ + Replaces the 'name', 'is_active', 'permitted', and 'options' values of a given key. + A master key must be set first. + + :param access_key_id: the 'key' value of the access key for which the values will be replaced + :param name: the new name desired for this access key + :param is_active: whether the key should become enabled (True) or revoked (False) + :param permitted: the new list of permissions desired for this access key + :param options: the new dictionary of options for this access key + """ + url = "{0}/{1}/projects/{2}/keys/{3}".format(self.base_url, self.api_version, + self.project_id, access_key_id) + headers = utilities.headers(self.master_key) + payload_dict = { + "name": name, + "is_active": is_active, + "permitted": permitted, + "options": options + } + payload = json.dumps(payload_dict) + response = self.fulfill(HTTPMethods.POST, url, data=payload, headers=headers, timeout=self.get_timeout) + self._error_handling(response) + return response.json() + + @requires_key(KeenKeys.MASTER) + def revoke_access_key(self, access_key_id): + """ + Revokes an access key. "Bad dog! No biscuit!" + """ + url = "{0}/{1}/projects/{2}/keys/{3}/revoke".format(self.base_url, self.api_version, + self.project_id, access_key_id) + headers = utilities.headers(self.master_key) + response = self.fulfill(HTTPMethods.POST, url, headers=headers, timeout=self.get_timeout) + + self._error_handling(response) + return response.json() + + @requires_key(KeenKeys.MASTER) + def unrevoke_access_key(self, access_key_id): + """ + Re-enables an access key. + """ + url = "{0}/{1}/projects/{2}/keys/{3}/unrevoke".format(self.base_url, self.api_version, + self.project_id, access_key_id) + headers = utilities.headers(self.master_key) + response = self.fulfill(HTTPMethods.POST, url, headers=headers, timeout=self.get_timeout) + + self._error_handling(response) + return response.json() + def _error_handling(self, res): """ Helper function to do the error handling diff --git a/keen/client.py b/keen/client.py index 38a4c01..7b50486 100644 --- a/keen/client.py +++ b/keen/client.py @@ -190,6 +190,61 @@ def get_all_collections(self): return self.api.get_all_collections() + def create_access_key(self, name, is_active=True, permitted=[], options={}): + """ + Creates a new access key. A master key must be set first. + + :param name: the name of the access key to create + :param is_active: Boolean value dictating whether this key is currently active (default True) + :param permitted: list of strings describing which operation types this key will permit + Legal values include "writes", "queries", "saved_queries", "cached_queries", + "datasets", and "schema". + :param options: dictionary containing more details about the key's permitted and restricted + functionality + """ + + return self.api.create_access_key(name=name, is_active=is_active, + permitted=permitted, options=options) + + def list_access_keys(self): + """ + Returns a list of all access keys in this project. A master key must be set first. + """ + return self.api.list_access_keys() + + def get_access_key(self, access_key_id): + """ + Returns details on a particular access key. A master key must be set first. + + :param access_key_id: the 'key' value of the access key to retreive data from + """ + return self.api.get_access_key(access_key_id) + + def update_access_key(self, access_key_id, name, is_active, permitted, options): + """ + Replaces the 'name', 'is_active', 'permitted', and 'options' values of a given key. + A master key must be set first. + + :param access_key_id: the 'key' value of the access key for which the values will be replaced + :param name: the new name desired for this access key + :param is_active: whether the key should become enabled (True) or revoked (False) + :param permitted: the new list of permissions desired for this access key + :param options: the new dictionary of options for this access key + """ + return self.api.update_access_key(access_key_id, name, is_active, permitted, options) + + def revoke_access_key(self, access_key_id): + """ + Revokes an access key. "Bad dog! No biscuit!" + """ + return self.api.revoke_access_key(access_key_id) + + def unrevoke_access_key(self, access_key_id): + """ + Re-enables an access key. + """ + return self.api.unrevoke_access_key(access_key_id) + def _base64_encode(self, string_to_encode): """ Base64 encodes a string, with either Python 2 or 3. From 7104030e75e10f375e92eaf8305a072ccdff7434 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Wed, 13 Sep 2017 09:27:48 -0600 Subject: [PATCH 190/224] updated README with access keys --- README.rst | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index fdbace3..c4e8457 100644 --- a/README.rst +++ b/README.rst @@ -470,10 +470,29 @@ returned by the server in the specified time. For example: This will cause both add_event() and add_events() to timeout after 100 seconds. If this timeout limit is hit, a requests.Timeout will be raised. Due to a bug in the requests library, you might also see an SSLError (https://github.com/kennethreitz/requests/issues/1294) -Create Scoped Keys +Create Access Keys '''''''''''''''''' -The Python client enables you to create `Scoped Keys `_ easily. For example: +The Python client enables the creation and manipulation of `Access Keys `_. Example: + +.. code-block:: python + + import keen + + # Master key must be set in an environment variable ahead of time. + + keen.create_access_key(name="Dave_Barry_Key", is_enabled=True, permitted=["writes", "cached_queries"], + options={"cached_queries": {"allowed": ["dave_barry_in_cyberspace_sales"]}}) + +This will generate a key with the user-friendly name "Dave_Barry_Key" with event writing and cached query permissions. +Other access key functions include `list_all_access_keys`, `get_access_key`, `revoke_access_key`, `unrevoke_access_key`, +and `update_access_key`. Use `help(keen.list_all_access_keys)` and friends for details on how to use them. + +Create Scoped Keys (Deprecated) +'''''''''''''''''' + +The Python client enables you to create `Scoped Keys `_ easily, but access keys are better! +If you need to use anyway, for legacy reasons, here's how: .. code-block:: python From 3a8ebda24b83151a99144cbb1bddf32184120b18 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Wed, 13 Sep 2017 09:30:09 -0600 Subject: [PATCH 191/224] minor fixes --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index c4e8457..f7349ed 100644 --- a/README.rst +++ b/README.rst @@ -485,14 +485,14 @@ The Python client enables the creation and manipulation of `Access Keys `_ easily, but access keys are better! -If you need to use anyway, for legacy reasons, here's how: +If you need to use them anyway, for legacy reasons, here's how: .. code-block:: python From 55953d4dcecf2662b09497a4a9c9f92436a9ef54 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Wed, 13 Sep 2017 10:00:54 -0600 Subject: [PATCH 192/224] Fixed Travis test dependencies --- .travis.yml | 6 ------ requirements.txt | 4 ++-- setup.py | 3 +-- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6078d90..1b4515d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,18 +2,12 @@ language: python python: - "2.6" - "2.7" - - "3.2" - "3.3" - "3.4" - "3.5" - "3.6" # command to install dependencies install: | - if [ "$TRAVIS_PYTHON_VERSION" == 3.2 ] - then - pip install "setuptools<30" - fi - pip install -r requirements.txt # command to run tests diff --git a/requirements.txt b/requirements.txt index a489fb7..8224fb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ pycryptodome>=3.4 -requests>=2.5,<2.11.0 -six~=1.10.0 \ No newline at end of file +requests>=2.5,<3.0 +six~=1.10.0 diff --git a/setup.py b/setup.py index 046af9a..5e7f550 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ reqs = reqs_file.readlines() reqs_file.close() -tests_require = ['nose', 'mock', 'responses', 'unittest2'] +tests_require = ['nose', 'mock', 'responses==0.5.1', 'unittest2'] setup( name="keen", @@ -45,7 +45,6 @@ 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', From cba39a890b2400cb61b962ac4bcc02d53aff9e49 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Wed, 13 Sep 2017 11:10:28 -0600 Subject: [PATCH 193/224] Version 0.5.0 Changelog and cutting a new version. --- README.rst | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f7349ed..cb4d80c 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ Use pip to install! pip install keen -This client is known to work on Python 2.6, 2.7, 3.2, 3.3, 3.4, 3.5 and 3.6. +This client is known to work on Python 2.6, 2.7, 3.3, 3.4, 3.5 and 3.6. For versions of Python < 2.7.9, you’ll need to install pyasn1, ndg-httpsclient, pyOpenSSL. @@ -520,7 +520,7 @@ To run tests: Changelog --------- -This project is in alpha stage at version 0.4.0 . See the full CHANGELOG `here <./CHANGELOG.rst>`_. +This project is in alpha stage at version 0.5.0 . See the full CHANGELOG `here <./CHANGELOG.rst>`_. Questions & Support diff --git a/setup.py b/setup.py index 5e7f550..e6fedab 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="keen", - version="0.4.0", + version="0.5.0", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", From abfa25c813fc067da143d169f945687325b33c85 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Wed, 13 Sep 2017 12:40:16 -0600 Subject: [PATCH 194/224] Updated utils version string --- keen/utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/utilities.py b/keen/utilities.py index bc7c2ba..a872765 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -5,7 +5,7 @@ from keen import exceptions -VERSION = "0.4.0" +VERSION = "0.5.0" def version(): """ From 8ba17b84dfc36b91d9fe6d6c785ac88264be417c Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Wed, 13 Sep 2017 13:45:30 -0600 Subject: [PATCH 195/224] Added Access Key helpers This API will now support specific updates of access key names, options, and permissions. It also supports adding and removing some permissions from permission lists without requiring the user to replace the entire permissions list every time with exactly the end result he or she desires. --- README.rst | 4 +- keen/__init__.py | 67 +++++++++++++++++++++++++- keen/api.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++- keen/client.py | 62 +++++++++++++++++++++++- 4 files changed, 245 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index cb4d80c..b88b194 100644 --- a/README.rst +++ b/README.rst @@ -486,12 +486,12 @@ The Python client enables the creation and manipulation of `Access Keys `_ easily, but access keys are better! +The Python client enables you to create `Scoped Keys `_ easily, but Access Keys are better! If you need to use them anyway, for legacy reasons, here's how: .. code-block:: python diff --git a/keen/__init__.py b/keen/__init__.py index acb7eba..848e1a3 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -528,7 +528,66 @@ def get_access_key(access_key_id): _initialize_client_from_environment() return _client.get_access_key(access_key_id) -def update_access_key(access_key_id, name, is_active, permitted, options): +def update_access_key_name(access_key_id, name): + """ + Updates only the name portion of an access key. + + :param access_key_id: the 'key' value of the access key to change the name of + :param name: the new name to give this access key + """ + _initialize_client_from_environment() + return _client.update_access_key_name(access_key_id, name) + +def add_access_key_permissions(access_key_id, permissions): + """ + Adds to the existing list of permissions on this key with the contents of this list. + Will not remove any existing permissions or modify the remainder of the key. + + :param access_key_id: the 'key' value of the access key to add permissions to + :param permissions: the new permissions to add to the existing list of permissions + """ + _initialize_client_from_environment() + return _client.add_access_key_permissions(access_key_id, permissions) + +def remove_access_key_permissions(access_key_id, permissions): + """ + Removes a list of permissions from the existing list of permissions. + Will not remove all existing permissions unless all such permissions are included + in this list. Not to be confused with key revocation. + + See also: revoke_access_key() + + :param access_key_id: the 'key' value of the access key to remove some permissions from + :param permissions: the permissions you wish to remove from this access key + """ + _initialize_client_from_environment() + return _client.remove_access_key_permissions(access_key_id, permissions) + +def update_access_key_permissions(access_key_id, permissions): + """ + Replaces all of the permissions on the access key but does not change + non-permission properties such as the key's name. + + See also: add_access_key_permissions() and remove_access_key_permissions(). + + :param access_key_id: the 'key' value of the access key to change the permissions of + :param permissions: the new list of permissions for this key + """ + _initialize_client_from_environment() + return _client.update_access_key_permissions(access_key_id, permissions) + +def update_access_key_options(self, access_key_id, options): + """ + Replaces all of the options on the access key but does not change + non-option properties such as permissions or the key's name. + + :param access_key_id: the 'key' value of the access key to change the options of + :param options: the new dictionary of options for this key + """ + _initialize_client_from_environment() + return _client.update_access_key_options(access_key_id, options) + +def update_access_key_full(access_key_id, name, is_active, permitted, options): """ Replaces the 'name', 'is_active', 'permitted', and 'options' values of a given key. A master key must be set first. @@ -540,11 +599,13 @@ def update_access_key(access_key_id, name, is_active, permitted, options): :param options: the new dictionary of options for this access key """ _initialize_client_from_environment() - return _client.update_access_key(access_key_id, name, is_active, permitted, options) + return _client.update_access_key_full(access_key_id, name, is_active, permitted, options) def revoke_access_key(access_key_id): """ Revokes an access key. "Bad dog! No biscuit!" + + :param access_key_id: the 'key' value of the access key to revoke """ _initialize_client_from_environment() return _client.revoke_access_key(access_key_id) @@ -552,6 +613,8 @@ def revoke_access_key(access_key_id): def unrevoke_access_key(access_key_id): """ Re-enables an access key. + + :param access_key_id: the 'key' value of the access key to re-enable (unrevoke) """ _initialize_client_from_environment() return _client.unrevoke_access_key(access_key_id) diff --git a/keen/api.py b/keen/api.py index d3bb8aa..f0602c4 100644 --- a/keen/api.py +++ b/keen/api.py @@ -311,8 +311,121 @@ def get_access_key(self, access_key_id): return response.json() + def _build_access_key_dict(self, access_key): + """ + Populates a dictionary payload usable in a POST request from a full access key object. + + :param access_key: the access_key to copy data from + """ + return { + "name": access_key["name"], + "is_active": access_key["is_active"], + "permitted": access_key["permitted"], + "options": access_key["options"] + } + + def _update_access_key_pair(self, access_key_id, key, val): + """ + Helper for updating access keys in a DRY fashion. + """ + # Get current state via HTTPS. + current_access_key = self.get_access_key(access_key_id) + + # Copy and only change the single parameter. + payload_dict = self._build_access_key_dict(current_access_key) + payload_dict[key] = val + + # Now just treat it like a full update. + return self.update_access_key_full(access_key_id, **payload_dict) + @requires_key(KeenKeys.MASTER) - def update_access_key(self, access_key_id, name, is_active, permitted, options): + def update_access_key_name(self, access_key_id, name): + """ + Updates only the name portion of an access key. + + :param access_key_id: the 'key' value of the access key to change the name of + :param name: the new name to give this access key + """ + return self._update_access_key_pair(access_key_id, "name", name) + + @requires_key(KeenKeys.MASTER) + def add_access_key_permissions(self, access_key_id, permissions): + """ + Adds to the existing list of permissions on this key with the contents of this list. + Will not remove any existing permissions or modify the remainder of the key. + + :param access_key_id: the 'key' value of the access key to add permissions to + :param permissions: the new permissions to add to the existing list of permissions + """ + # Get current state via HTTPS. + current_access_key = self.get_access_key(access_key_id) + + # Copy and only change the single parameter. + payload_dict = self._build_access_key_dict(current_access_key) + + # Turn into sets to avoid duplicates. + old_permissions = set(payload_dict["permissions"]) + new_permissions = set(permissions) + combined_permissions = old_permissions.union(new_permissions) + payload_dict["permissions"] = list(combined_permissions) + + # Now just treat it like a full update. + return self.update_access_key_full(access_key_id, **payload_dict) + + @requires_key(KeenKeys.MASTER) + def remove_access_key_permissions(self, access_key_id, permissions): + """ + Removes a list of permissions from the existing list of permissions. + Will not remove all existing permissions unless all such permissions are included + in this list. Not to be confused with key revocation. + + See also: revoke_access_key() + + :param access_key_id: the 'key' value of the access key to remove some permissions from + :param permissions: the permissions you wish to remove from this access key + """ + # Get current state via HTTPS. + current_access_key = self.get_access_key(access_key_id) + + # Copy and only change the single parameter. + payload_dict = self._build_access_key_dict(current_access_key) + + # Turn into sets to avoid duplicates. + old_permissions = set(payload_dict["permissions"]) + removal_permissions = set(permissions) + reduced_permissions = old_permissions.difference_update(removal_permissions) + payload_dict["permissions"] = list(reduced_permissions) + + # Now just treat it like a full update. + return self.update_access_key_full(access_key_id, **payload_dict) + + @requires_key(KeenKeys.MASTER) + def update_access_key_permissions(self, access_key_id, permissions): + """ + Replaces all of the permissions on the access key but does not change + non-permission properties such as the key's name. + + See also: add_access_key_permissions() and remove_access_key_permissions(). + + :param access_key_id: the 'key' value of the access key to change the permissions of + :param permissions: the new list of permissions for this key + """ + return self._update_access_key_pair(access_key_id, "permissions", permission) + + @requires_key(KeenKeys.MASTER) + def update_access_key_options(self, access_key_id, options): + """ + Replaces all of the options on the access key but does not change + non-option properties such as permissions or the key's name. + + :param access_key_id: the 'key' value of the access key to change the options of + :param options: the new dictionary of options for this key + """ + return self._update_access_key_pair(access_key_id, "options", options) + + + @requires_key(KeenKeys.MASTER) + def update_access_key_full(self, access_key_id, name, is_active, permitted, options): """ Replaces the 'name', 'is_active', 'permitted', and 'options' values of a given key. A master key must be set first. @@ -341,6 +454,8 @@ def update_access_key(self, access_key_id, name, is_active, permitted, options): def revoke_access_key(self, access_key_id): """ Revokes an access key. "Bad dog! No biscuit!" + + :param access_key_id: the 'key' value of the access key to revoke """ url = "{0}/{1}/projects/{2}/keys/{3}/revoke".format(self.base_url, self.api_version, self.project_id, access_key_id) @@ -354,6 +469,8 @@ def revoke_access_key(self, access_key_id): def unrevoke_access_key(self, access_key_id): """ Re-enables an access key. + + :param access_key_id: the 'key' value of the access key to re-enable (unrevoke) """ url = "{0}/{1}/projects/{2}/keys/{3}/unrevoke".format(self.base_url, self.api_version, self.project_id, access_key_id) diff --git a/keen/client.py b/keen/client.py index 7b50486..4998776 100644 --- a/keen/client.py +++ b/keen/client.py @@ -220,7 +220,61 @@ def get_access_key(self, access_key_id): """ return self.api.get_access_key(access_key_id) - def update_access_key(self, access_key_id, name, is_active, permitted, options): + def update_access_key_name(self, access_key_id, name): + """ + Updates only the name portion of an access key. + + :param access_key_id: the 'key' value of the access key to change the name of + :param name: the new name to give this access key + """ + return self.api.update_access_key_name(access_key_id, name) + + def add_access_key_permissions(self, access_key_id, permissions): + """ + Adds to the existing list of permissions on this key with the contents of this list. + Will not remove any existing permissions or modify the remainder of the key. + + :param access_key_id: the 'key' value of the access key to add permissions to + :param permissions: the new permissions to add to the existing list of permissions + """ + return self.api.add_access_key_permissions(access_key_id, permissions) + + def remove_access_key_permissions(self, access_key_id, permissions): + """ + Removes a list of permissions from the existing list of permissions. + Will not remove all existing permissions unless all such permissions are included + in this list. Not to be confused with key revocation. + + See also: revoke_access_key() + + :param access_key_id: the 'key' value of the access key to remove some permissions from + :param permissions: the permissions you wish to remove from this access key + """ + return self.api.remove_access_key_permissions(access_key_id, permissions) + + def update_access_key_permissions(self, access_key_id, permissions): + """ + Replaces all of the permissions on the access key but does not change + non-permission properties such as the key's name. + + See also: add_access_key_permissions() and remove_access_key_permissions(). + + :param access_key_id: the 'key' value of the access key to change the permissions of + :param permissions: the new list of permissions for this key + """ + return self.api.update_access_key_permissions(access_key_id, permissions) + + def update_access_key_options(self, access_key_id, options): + """ + Replaces all of the options on the access key but does not change + non-option properties such as permissions or the key's name. + + :param access_key_id: the 'key' value of the access key to change the options of + :param options: the new dictionary of options for this key + """ + return self.api.update_access_key_options(access_key_id, options) + + def update_access_key_full(self, access_key_id, name, is_active, permitted, options): """ Replaces the 'name', 'is_active', 'permitted', and 'options' values of a given key. A master key must be set first. @@ -231,17 +285,21 @@ def update_access_key(self, access_key_id, name, is_active, permitted, options): :param permitted: the new list of permissions desired for this access key :param options: the new dictionary of options for this access key """ - return self.api.update_access_key(access_key_id, name, is_active, permitted, options) + return self.api.update_access_key_full(access_key_id, name, is_active, permitted, options) def revoke_access_key(self, access_key_id): """ Revokes an access key. "Bad dog! No biscuit!" + + :param access_key_id: the 'key' value of the access key to revoke """ return self.api.revoke_access_key(access_key_id) def unrevoke_access_key(self, access_key_id): """ Re-enables an access key. + + :param access_key_id: the 'key' value of the access key to re-enable (unrevoke) """ return self.api.unrevoke_access_key(access_key_id) From 966fff313571f2cfbb0b275e98a28ee66f305f6c Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 21 Sep 2017 14:12:11 -0600 Subject: [PATCH 196/224] better docs --- README.rst | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index b88b194..ad8dd8a 100644 --- a/README.rst +++ b/README.rst @@ -473,7 +473,7 @@ This will cause both add_event() and add_events() to timeout after 100 seconds. Create Access Keys '''''''''''''''''' -The Python client enables the creation and manipulation of `Access Keys `_. Example: +The Python client enables the creation and manipulation of `Access Keys `_. Examples: .. code-block:: python @@ -481,12 +481,59 @@ The Python client enables the creation and manipulation of `Access Keys Date: Tue, 26 Sep 2017 08:46:58 -0600 Subject: [PATCH 197/224] docs update from review --- README.rst | 41 +++++++++++++++++++++++------------------ keen/api.py | 2 +- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index ad8dd8a..07104a7 100644 --- a/README.rst +++ b/README.rst @@ -477,50 +477,55 @@ The Python client enables the creation and manipulation of `Access Keys `_ easily, but Access Keys are better! diff --git a/keen/api.py b/keen/api.py index f0602c4..4385dfa 100644 --- a/keen/api.py +++ b/keen/api.py @@ -311,7 +311,7 @@ def get_access_key(self, access_key_id): return response.json() - def _build_access_key_dict(self, access_key): + def _build_access_key_dict(access_key): """ Populates a dictionary payload usable in a POST request from a full access key object. From 734b11124c02f6b4f3ca08e917ed549a4350df57 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 5 Oct 2017 14:40:30 -0600 Subject: [PATCH 198/224] plus initial test More tests. I've broken these tests out into their own file. An item of followup work this presents is to move the helper function and using a constant for a url prefix pattern to the remainder of the tests as well, but I feel that is beyond the scope of this already large issue. friendlier imports for non2.7 2.6 hates format without numbers --- keen/tests/access_key_tests.py | 111 +++++++++++++++++++++++++++++++++ keen/tests/client_tests.py | 1 - 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 keen/tests/access_key_tests.py diff --git a/keen/tests/access_key_tests.py b/keen/tests/access_key_tests.py new file mode 100644 index 0000000..56b58eb --- /dev/null +++ b/keen/tests/access_key_tests.py @@ -0,0 +1,111 @@ + +from keen.tests.base_test_case import BaseTestCase +from keen.tests.client_tests import MockedResponse +from mock import patch + +import keen + +__author__ = 'BlackVegetable' + +class AccessKeyTests(BaseTestCase): + + ACCESS_KEY_NAME = "Bob_Key" + ACCESS_KEY_RESPONSE = MockedResponse( + status_code=201, + json_response={'name': "Bob_Key", + 'is_active': True, + 'permitted': [], + 'key': '320104AEFFC569EEE60BCAC9BB064DFF9897E391AB8C59608AC0869AFD291B4E', + 'project_id': '55777979e085574e8ad3523c', + 'options': {'saved_queries': None, + 'writes': None, + 'datasets': None, + 'cached_queries': None, + 'queries': None}}) + + UPDATED_ACCESS_KEY_RESPONSE = MockedResponse( + status_code=201, + json_response={'name': "Jim_Key", + 'is_active': False, + 'permitted': ["queries"], + 'key': '320104AEFFC569EEE60BCAC9BB064DFF9897E391AB8C59608AC0869AFD291B4E', + 'project_id': '55777979e085574e8ad3523c', + 'options': {'saved_queries': None, + 'writes': None, + 'datasets': None, + 'cached_queries': None, + 'queries': { + "filters": [{ + "property_name": "customer.id", + "operator": "eq", + "property_value": "asdf12345z" + }]}}}) + + NO_CONTENT_RESPONSE = MockedResponse(status_code=204, json_response="") + + def setUp(self): + super(AccessKeyTests, self).setUp() + keen.project_id = "55777979e085574e8ad3523c" + keen.write_key = "DEADBEEF" + keen.read_key = "BADFEED" + keen.master_key = "BADHORSE" + self.keys_uri_prefix = "https://api.keen.io/3.0/projects/{0}/keys".format(keen.project_id) + + def _assert_proper_permissions(self, method, permission): + self.assertTrue(permission in method.call_args[1]["headers"]["Authorization"]) + + @patch("requests.Session.post") + def test_create_access_key(self, post): + post.return_value = self.ACCESS_KEY_RESPONSE + resp = keen.create_access_key(self.ACCESS_KEY_NAME) + self.assertTrue(self.ACCESS_KEY_NAME in post.call_args[1]["data"]) + self._assert_proper_permissions(post, keen.master_key) + self.assertEqual(resp, self.ACCESS_KEY_RESPONSE.json()) + + @patch("requests.Session.get") + def test_list_access_keys(self, get): + get.return_value = self.ACCESS_KEY_RESPONSE + resp = keen.list_access_keys() + self.assertEqual(self.keys_uri_prefix, get.call_args[0][0]) + self._assert_proper_permissions(get, keen.master_key) + self.assertEqual(resp, self.ACCESS_KEY_RESPONSE.json()) + + @patch("requests.Session.get") + def test_get_access_key(self, get): + get.return_value = self.ACCESS_KEY_RESPONSE + resp = keen.get_access_key(self.ACCESS_KEY_NAME) + self.assertEqual("{0}/{1}".format(self.keys_uri_prefix, self.ACCESS_KEY_NAME), get.call_args[0][0]) + self._assert_proper_permissions(get, keen.master_key) + self.assertEqual(resp, self.ACCESS_KEY_RESPONSE.json()) + + @patch("requests.Session.post") + def test_revoke_access_key(self, post): + post.return_value = self.NO_CONTENT_RESPONSE + resp = keen.revoke_access_key(self.ACCESS_KEY_NAME) + self.assertEqual("{0}/{1}/revoke".format(self.keys_uri_prefix, self.ACCESS_KEY_NAME), post.call_args[0][0]) + self._assert_proper_permissions(post, keen.master_key) + self.assertEqual(resp, self.NO_CONTENT_RESPONSE.json()) + + @patch("requests.Session.post") + def test_unrevoke_access_key(self, post): + post.return_value = self.NO_CONTENT_RESPONSE + resp = keen.unrevoke_access_key(self.ACCESS_KEY_NAME) + self.assertEqual("{0}/{1}/unrevoke".format(self.keys_uri_prefix, self.ACCESS_KEY_NAME), post.call_args[0][0]) + self._assert_proper_permissions(post, keen.master_key) + self.assertEqual(resp, self.NO_CONTENT_RESPONSE.json()) + + @patch("requests.Session.post") + def test_update_access_key_full(self, post): + # The update tests have a significant amount of logic that will not be tested via blackbox testing without + # un-mocking Keen's API. So this is the only test that will really cover any of them, and not even very + # well. + post.return_value = self.UPDATED_ACCESS_KEY_RESPONSE + options_dict = {"queries": self.UPDATED_ACCESS_KEY_RESPONSE.json_response["options"]["queries"]} + resp = keen.update_access_key_full(self.ACCESS_KEY_NAME, + name=self.UPDATED_ACCESS_KEY_RESPONSE.json_response["name"], + is_active=self.UPDATED_ACCESS_KEY_RESPONSE.json_response["is_active"], + permitted=self.UPDATED_ACCESS_KEY_RESPONSE.json_response["permitted"], + options=options_dict) + self.assertEqual("{0}/{1}".format(self.keys_uri_prefix, self.ACCESS_KEY_NAME), post.call_args[0][0]) + self._assert_proper_permissions(post, keen.master_key) + self.assertEqual(resp, self.UPDATED_ACCESS_KEY_RESPONSE.json()) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 62dea35..229c09e 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -598,7 +598,6 @@ def test_delete_events(self, delete): # Check that the master_key is in the Authorization header. self.assertTrue(keen.master_key in delete.call_args[1]["headers"]["Authorization"]) - @patch("requests.Session.get") class GetTests(BaseTestCase): From 2e72d86eec8a8d48b9ae1d330a5c1adcadaa15cd Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 19 Oct 2017 14:56:29 -0600 Subject: [PATCH 199/224] Actually update changelog --- CHANGELOG.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 185eeb8..1452a06 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog --------- +0.5.0 +`````` ++ Added support for Access Keys. ++ Added support for order_by and limit, group_by options. ++ Deprecated python 3.2. ++ Scoped Keys are now deprecated in favor of Access Keys. ++ Now permits more versions of the requests library. (issue #133) + + 0.4.0 `````` From a672c89f8191ae2eaca8f980af64d7a3b2dbc71d Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Thu, 19 Oct 2017 14:58:31 -0600 Subject: [PATCH 200/224] fixed 2.6 compat with order_by test --- keen/tests/client_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keen/tests/client_tests.py b/keen/tests/client_tests.py index 229c09e..a78939c 100644 --- a/keen/tests/client_tests.py +++ b/keen/tests/client_tests.py @@ -501,7 +501,7 @@ def test_order_by(self, get): limit = 2 order_by = {"property_name": "result", "direction": keen.direction.DESCENDING} resp = keen.count(collection, timeframe="today", group_by="number", order_by=order_by, limit=limit) - self.assertTrue("https://api.keen.io/3.0/projects/{}/queries/count".format(keen.project_id) in + self.assertTrue("https://api.keen.io/3.0/projects/{0}/queries/count".format(keen.project_id) in get.call_args[0][0]) self.assertEqual(2, get.call_args[1]["params"]["limit"]) self.assertEqual(collection, get.call_args[1]["params"]["event_collection"]) From 1fc2b1155f907eb60488197c3018945abf52d771 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Fri, 20 Oct 2017 09:44:34 -0600 Subject: [PATCH 201/224] +staticmethod decorator --- keen/api.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/keen/api.py b/keen/api.py index 4385dfa..e64b401 100644 --- a/keen/api.py +++ b/keen/api.py @@ -311,6 +311,7 @@ def get_access_key(self, access_key_id): return response.json() + @staticmethod def _build_access_key_dict(access_key): """ Populates a dictionary payload usable in a POST request from a full access key object. @@ -332,7 +333,7 @@ def _update_access_key_pair(self, access_key_id, key, val): current_access_key = self.get_access_key(access_key_id) # Copy and only change the single parameter. - payload_dict = self._build_access_key_dict(current_access_key) + payload_dict = _build_access_key_dict(current_access_key) payload_dict[key] = val # Now just treat it like a full update. @@ -361,7 +362,7 @@ def add_access_key_permissions(self, access_key_id, permissions): current_access_key = self.get_access_key(access_key_id) # Copy and only change the single parameter. - payload_dict = self._build_access_key_dict(current_access_key) + payload_dict = _build_access_key_dict(current_access_key) # Turn into sets to avoid duplicates. old_permissions = set(payload_dict["permissions"]) @@ -388,7 +389,7 @@ def remove_access_key_permissions(self, access_key_id, permissions): current_access_key = self.get_access_key(access_key_id) # Copy and only change the single parameter. - payload_dict = self._build_access_key_dict(current_access_key) + payload_dict = _build_access_key_dict(current_access_key) # Turn into sets to avoid duplicates. old_permissions = set(payload_dict["permissions"]) From bd88aee96063e7344d47a56704942bbc73cc6c3d Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Fri, 20 Oct 2017 10:09:45 -0600 Subject: [PATCH 202/224] Deprecate 2.6 and 3.3 We will no longer support python 2.6 or 3.3. Neither is being used heavily with this library according to pip data, and both are EOL (2.6 since 2013!) --- .travis.yml | 2 -- CHANGELOG.rst | 2 +- README.rst | 2 +- setup.py | 2 -- 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1b4515d..458f6c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: python python: - - "2.6" - "2.7" - - "3.3" - "3.4" - "3.5" - "3.6" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1452a06..0544b9b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,7 +5,7 @@ Changelog `````` + Added support for Access Keys. + Added support for order_by and limit, group_by options. -+ Deprecated python 3.2. ++ Deprecated python 2.6, 3.2 and 3.3. + Scoped Keys are now deprecated in favor of Access Keys. + Now permits more versions of the requests library. (issue #133) diff --git a/README.rst b/README.rst index 07104a7..4e975c3 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ Use pip to install! pip install keen -This client is known to work on Python 2.6, 2.7, 3.3, 3.4, 3.5 and 3.6. +This client is known to work on Python 2.7, 3.4, 3.5 and 3.6. For versions of Python < 2.7.9, you’ll need to install pyasn1, ndg-httpsclient, pyOpenSSL. diff --git a/setup.py b/setup.py index e6fedab..5b560c9 100644 --- a/setup.py +++ b/setup.py @@ -42,10 +42,8 @@ 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', From 8d3174c0454f95e67da8710be3da9fab4bccb918 Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Mon, 30 Oct 2017 09:05:42 -0600 Subject: [PATCH 203/224] Access keys fixes Nonexisting unit tests... are a bad idea. Sanity testing showed several of the access key functions were horribly broken. Also the tests were slightly misleading in their mocking of the ACCESS_KEY_ID. Namely, the user-supplied NAME was being used where in practice it would not actually work. --- keen/__init__.py | 2 +- keen/api.py | 18 +++++++++--------- keen/tests/access_key_tests.py | 30 ++++++++++++++++++++++-------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/keen/__init__.py b/keen/__init__.py index 848e1a3..37659fd 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -576,7 +576,7 @@ def update_access_key_permissions(access_key_id, permissions): _initialize_client_from_environment() return _client.update_access_key_permissions(access_key_id, permissions) -def update_access_key_options(self, access_key_id, options): +def update_access_key_options(access_key_id, options): """ Replaces all of the options on the access key but does not change non-option properties such as permissions or the key's name. diff --git a/keen/api.py b/keen/api.py index e64b401..fcf53dc 100644 --- a/keen/api.py +++ b/keen/api.py @@ -333,7 +333,7 @@ def _update_access_key_pair(self, access_key_id, key, val): current_access_key = self.get_access_key(access_key_id) # Copy and only change the single parameter. - payload_dict = _build_access_key_dict(current_access_key) + payload_dict = KeenApi._build_access_key_dict(current_access_key) payload_dict[key] = val # Now just treat it like a full update. @@ -362,13 +362,13 @@ def add_access_key_permissions(self, access_key_id, permissions): current_access_key = self.get_access_key(access_key_id) # Copy and only change the single parameter. - payload_dict = _build_access_key_dict(current_access_key) + payload_dict = KeenApi._build_access_key_dict(current_access_key) # Turn into sets to avoid duplicates. - old_permissions = set(payload_dict["permissions"]) + old_permissions = set(payload_dict["permitted"]) new_permissions = set(permissions) combined_permissions = old_permissions.union(new_permissions) - payload_dict["permissions"] = list(combined_permissions) + payload_dict["permitted"] = list(combined_permissions) # Now just treat it like a full update. return self.update_access_key_full(access_key_id, **payload_dict) @@ -389,13 +389,13 @@ def remove_access_key_permissions(self, access_key_id, permissions): current_access_key = self.get_access_key(access_key_id) # Copy and only change the single parameter. - payload_dict = _build_access_key_dict(current_access_key) + payload_dict = KeenApi._build_access_key_dict(current_access_key) # Turn into sets to avoid duplicates. - old_permissions = set(payload_dict["permissions"]) + old_permissions = set(payload_dict["permitted"]) removal_permissions = set(permissions) - reduced_permissions = old_permissions.difference_update(removal_permissions) - payload_dict["permissions"] = list(reduced_permissions) + reduced_permissions = old_permissions.difference(removal_permissions) + payload_dict["permitted"] = list(reduced_permissions) # Now just treat it like a full update. return self.update_access_key_full(access_key_id, **payload_dict) @@ -411,7 +411,7 @@ def update_access_key_permissions(self, access_key_id, permissions): :param access_key_id: the 'key' value of the access key to change the permissions of :param permissions: the new list of permissions for this key """ - return self._update_access_key_pair(access_key_id, "permissions", permission) + return self._update_access_key_pair(access_key_id, "permitted", permissions) @requires_key(KeenKeys.MASTER) def update_access_key_options(self, access_key_id, options): diff --git a/keen/tests/access_key_tests.py b/keen/tests/access_key_tests.py index 56b58eb..f20e5ad 100644 --- a/keen/tests/access_key_tests.py +++ b/keen/tests/access_key_tests.py @@ -10,6 +10,7 @@ class AccessKeyTests(BaseTestCase): ACCESS_KEY_NAME = "Bob_Key" + ACCESS_KEY_ID = "320104AEFFC569EEE60BCAC9BB064DFF9897E391AB8C59608AC0869AFD291B4E" ACCESS_KEY_RESPONSE = MockedResponse( status_code=201, json_response={'name': "Bob_Key", @@ -73,24 +74,24 @@ def test_list_access_keys(self, get): @patch("requests.Session.get") def test_get_access_key(self, get): get.return_value = self.ACCESS_KEY_RESPONSE - resp = keen.get_access_key(self.ACCESS_KEY_NAME) - self.assertEqual("{0}/{1}".format(self.keys_uri_prefix, self.ACCESS_KEY_NAME), get.call_args[0][0]) + resp = keen.get_access_key(self.ACCESS_KEY_ID) + self.assertEqual("{0}/{1}".format(self.keys_uri_prefix, self.ACCESS_KEY_ID), get.call_args[0][0]) self._assert_proper_permissions(get, keen.master_key) self.assertEqual(resp, self.ACCESS_KEY_RESPONSE.json()) @patch("requests.Session.post") def test_revoke_access_key(self, post): post.return_value = self.NO_CONTENT_RESPONSE - resp = keen.revoke_access_key(self.ACCESS_KEY_NAME) - self.assertEqual("{0}/{1}/revoke".format(self.keys_uri_prefix, self.ACCESS_KEY_NAME), post.call_args[0][0]) + resp = keen.revoke_access_key(self.ACCESS_KEY_ID) + self.assertEqual("{0}/{1}/revoke".format(self.keys_uri_prefix, self.ACCESS_KEY_ID), post.call_args[0][0]) self._assert_proper_permissions(post, keen.master_key) self.assertEqual(resp, self.NO_CONTENT_RESPONSE.json()) @patch("requests.Session.post") def test_unrevoke_access_key(self, post): post.return_value = self.NO_CONTENT_RESPONSE - resp = keen.unrevoke_access_key(self.ACCESS_KEY_NAME) - self.assertEqual("{0}/{1}/unrevoke".format(self.keys_uri_prefix, self.ACCESS_KEY_NAME), post.call_args[0][0]) + resp = keen.unrevoke_access_key(self.ACCESS_KEY_ID) + self.assertEqual("{0}/{1}/unrevoke".format(self.keys_uri_prefix, self.ACCESS_KEY_ID), post.call_args[0][0]) self._assert_proper_permissions(post, keen.master_key) self.assertEqual(resp, self.NO_CONTENT_RESPONSE.json()) @@ -101,11 +102,24 @@ def test_update_access_key_full(self, post): # well. post.return_value = self.UPDATED_ACCESS_KEY_RESPONSE options_dict = {"queries": self.UPDATED_ACCESS_KEY_RESPONSE.json_response["options"]["queries"]} - resp = keen.update_access_key_full(self.ACCESS_KEY_NAME, + resp = keen.update_access_key_full(self.ACCESS_KEY_ID, name=self.UPDATED_ACCESS_KEY_RESPONSE.json_response["name"], is_active=self.UPDATED_ACCESS_KEY_RESPONSE.json_response["is_active"], permitted=self.UPDATED_ACCESS_KEY_RESPONSE.json_response["permitted"], options=options_dict) - self.assertEqual("{0}/{1}".format(self.keys_uri_prefix, self.ACCESS_KEY_NAME), post.call_args[0][0]) + self.assertEqual("{0}/{1}".format(self.keys_uri_prefix, self.ACCESS_KEY_ID), post.call_args[0][0]) self._assert_proper_permissions(post, keen.master_key) self.assertEqual(resp, self.UPDATED_ACCESS_KEY_RESPONSE.json()) + + @patch("requests.Session.get") + @patch("requests.Session.post") + def test_sanity_of_update_functions(self, post, get): + post.return_value = self.UPDATED_ACCESS_KEY_RESPONSE + get.return_value = self.UPDATED_ACCESS_KEY_RESPONSE + # Ensure at the very least that the other access key functions don't crash when run. + keen.update_access_key_name(self.ACCESS_KEY_ID, name="Marzipan") + keen.add_access_key_permissions(self.ACCESS_KEY_ID, ["writes"]) + keen.remove_access_key_permissions(self.ACCESS_KEY_ID, ["writes"]) + keen.update_access_key_permissions(self.ACCESS_KEY_ID, ["writes", "cached_queries"]) + keen.update_access_key_options(self.ACCESS_KEY_ID, options={}) + self.assertTrue(True) # Best test assertion ever. From f442c03e9f64e15e0546d81cfa5bc0960b254b0a Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Mon, 30 Oct 2017 09:27:21 -0600 Subject: [PATCH 204/224] 0.5.1 release --- CHANGELOG.rst | 5 +++++ README.rst | 2 +- keen/utilities.py | 2 +- setup.py | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0544b9b..7cea24e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog --------- +0.5.1 +`````` + ++ Fixed various Access Key bugs. + 0.5.0 `````` + Added support for Access Keys. diff --git a/README.rst b/README.rst index 4e975c3..3bc54ce 100644 --- a/README.rst +++ b/README.rst @@ -572,7 +572,7 @@ To run tests: Changelog --------- -This project is in alpha stage at version 0.5.0 . See the full CHANGELOG `here <./CHANGELOG.rst>`_. +This project is in alpha stage at version 0.5.1 . See the full CHANGELOG `here <./CHANGELOG.rst>`_. Questions & Support diff --git a/keen/utilities.py b/keen/utilities.py index a872765..2738134 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -5,7 +5,7 @@ from keen import exceptions -VERSION = "0.5.0" +VERSION = "0.5.1" def version(): """ diff --git a/setup.py b/setup.py index 5b560c9..2062f8d 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="keen", - version="0.5.0", + version="0.5.1", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", From 8a6803c250646a80067d7593f4f7b5c193c50c8a Mon Sep 17 00:00:00 2001 From: Devin Ekins Date: Mon, 6 Nov 2017 14:57:58 -0700 Subject: [PATCH 205/224] Update order_by docs I was missing some crucial detail there. --- README.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 3bc54ce..c932bac 100644 --- a/README.rst +++ b/README.rst @@ -168,9 +168,10 @@ For more code samples, take a look at Keen's `docs Date: Mon, 26 Nov 2018 11:24:20 +0000 Subject: [PATCH 206/224] initial test setup --- keen/tests/cached_datasets_tests.py | 69 +++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 keen/tests/cached_datasets_tests.py diff --git a/keen/tests/cached_datasets_tests.py b/keen/tests/cached_datasets_tests.py new file mode 100644 index 0000000..adcc844 --- /dev/null +++ b/keen/tests/cached_datasets_tests.py @@ -0,0 +1,69 @@ +import responses + +from keen import exceptions +from keen.client import KeenClient +from keen.tests.base_test_case import BaseTestCase + + +class CachedDatasetsTestCase(BaseTestCase): + + def setUp(self): + super(CachedDatasetsTestCase, self).setUp() + self.organization_id = "1234xxxx5678" + self.project_id = "xxxx1234" + self.read_key = "abcd5678read" + self.master_key = "abcd5678master" + self.client = KeenClient( + project_id=self.project_id, + read_key=self.read_key, + master_key=self.master_key + ) + + self.datasets = [ + { + "project_id": self.project_id, + "organization_id": self.organization_id, + "dataset_name": "DATASET_NAME_1", + "display_name": "a first dataset wee", + "query": { + "project_id": self.project_id, + "analysis_type": "count", + "event_collection": "best collection", + "filters": [ + { + "property_name": "request.foo", + "operator": "lt", + "property_value": 300, + } + ], + "timeframe": "this_500_hours", + "timezone": "US/Pacific", + "interval": "hourly", + "group_by": ["exception.name"], + }, + "index_by": ["project.id"], + "last_scheduled_date": "2016-11-04T18:03:38.430Z", + "latest_subtimeframe_available": "2016-11-04T19:00:00.000Z", + "milliseconds_behind": 3600000, + }, + { + "project_id": self.project_id, + "organization_id": self.organization_id, + "dataset_name": "DATASET_NAME_10", + "display_name": "tenth dataset wee", + "query": { + "project_id": self.project_id, + "analysis_type": "count", + "event_collection": "tenth best collection", + "filters": [], + "timeframe": "this_500_days", + "timezone": "UTC", + "interval": "daily", + "group_by": ["analysis_type"], + }, + "index_by": ["project.organization.id"], + "last_scheduled_date": "2016-11-04T19:28:36.639Z", + "latest_subtimeframe_available": "2016-11-05T00:00:00.000Z", + "milliseconds_behind": 3600000, + }, + ] From ab6e70e74bc28e3a88b38b536a929ce2979fc1a9 Mon Sep 17 00:00:00 2001 From: Ben Steadman Date: Mon, 26 Nov 2018 11:30:00 +0000 Subject: [PATCH 207/224] cached datasets interface get all --- keen/cached_datasets.py | 44 +++++++++++++++++++++++++++++ keen/client.py | 3 +- keen/tests/cached_datasets_tests.py | 30 ++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 keen/cached_datasets.py diff --git a/keen/cached_datasets.py b/keen/cached_datasets.py new file mode 100644 index 0000000..efe72a0 --- /dev/null +++ b/keen/cached_datasets.py @@ -0,0 +1,44 @@ +import json + +from keen.api import HTTPMethods +from keen.utilities import KeenKeys, headers, requires_key + + +class CachedDatasetsInterface: + + def __init__(self, api): + self.api = api + self._cached_datasets_url = "{0}/{1}/projects/{2}/datasets".format( + self.api.base_url, self.api.api_version, self.api.project_id + ) + + @requires_key(KeenKeys.READ) + def all(self): + """ Fetch all Cached Datasets for a Project. Read key must be set. + """ + return self._get_json(HTTPMethods.GET, + self._cached_datasets_url, + self._get_master_key()) + + def _get_json(self, http_method, url, key, *args, **kwargs): + response = self.api.fulfill( + http_method, + url, + headers=headers(key), + *args, + **kwargs) + + self.api._error_handling(response) + + try: + response = response.json() + except ValueError: + response = "No JSON available." + + return response + + def _get_read_key(self): + return self.api._get_read_key() + + def _get_master_key(self): + return self.api._get_master_key() diff --git a/keen/client.py b/keen/client.py index 4998776..8717939 100644 --- a/keen/client.py +++ b/keen/client.py @@ -2,7 +2,7 @@ import copy import json import sys -from keen import persistence_strategies, exceptions, saved_queries +from keen import persistence_strategies, exceptions, saved_queries, cached_datasets from keen.api import KeenApi from keen.persistence_strategies import BasePersistenceStrategy @@ -98,6 +98,7 @@ def __init__(self, project_id, write_key=None, read_key=None, self.get_timeout = get_timeout self.post_timeout = post_timeout self.saved_queries = saved_queries.SavedQueriesInterface(self.api) + self.cached_datasets = cached_datasets.CachedDatasetsInterface(self.api) if sys.version_info[0] < 3: @staticmethod diff --git a/keen/tests/cached_datasets_tests.py b/keen/tests/cached_datasets_tests.py index adcc844..05c308a 100644 --- a/keen/tests/cached_datasets_tests.py +++ b/keen/tests/cached_datasets_tests.py @@ -67,3 +67,33 @@ def setUp(self): "milliseconds_behind": 3600000, }, ] + + def test_get_all_raises_with_no_keys(self): + client = KeenClient(project_id=self.project_id) + + with self.assertRaises(exceptions.InvalidEnvironmentError): + client.cached_datasets.all() + + @responses.activate + def test_get_all(self): + keen_response = { + "datasets": self.datasets, + "next_page_url": ( + "https://api.keen.io/3.0/projects/{0}/datasets?" + "limit=LIMIT&after_name={1}" + ).format(self.project_id, self.datasets[-1]['dataset_name']) + } + + url = "{0}/{1}/projects/{2}/datasets".format( + self.client.api.base_url, + self.client.api.api_version, + self.project_id + ) + + responses.add( + responses.GET, url, status=200, json=keen_response + ) + + all_cached_datasets = self.client.cached_datasets.all() + + self.assertEquals(all_cached_datasets, keen_response) From 186cd1c7faea9f4e51654aef3f6a5566a16dcfc4 Mon Sep 17 00:00:00 2001 From: Ben Steadman Date: Mon, 26 Nov 2018 11:37:12 +0000 Subject: [PATCH 208/224] cached datasets interface get method --- keen/cached_datasets.py | 9 +++++++++ keen/tests/cached_datasets_tests.py | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/keen/cached_datasets.py b/keen/cached_datasets.py index efe72a0..4a2d430 100644 --- a/keen/cached_datasets.py +++ b/keen/cached_datasets.py @@ -20,6 +20,15 @@ def all(self): self._cached_datasets_url, self._get_master_key()) + @requires_key(KeenKeys.READ) + def get(self, dataset_name): + """ Fetch a single Cached Dataset for a Project. Read key must be set. + + :param dataset_name: Name of Cached Dataset (not `display_name`) + """ + url = "{}/{}".format(self._cached_datasets_url, dataset_name) + return self._get_json(HTTPMethods.GET, url, self._get_read_key()) + def _get_json(self, http_method, url, key, *args, **kwargs): response = self.api.fulfill( http_method, diff --git a/keen/tests/cached_datasets_tests.py b/keen/tests/cached_datasets_tests.py index 05c308a..c423bf4 100644 --- a/keen/tests/cached_datasets_tests.py +++ b/keen/tests/cached_datasets_tests.py @@ -97,3 +97,20 @@ def test_get_all(self): all_cached_datasets = self.client.cached_datasets.all() self.assertEquals(all_cached_datasets, keen_response) + + def test_get_one_raises_with_no_keys(self): + client = KeenClient(project_id=self.project_id) + + with self.assertRaises(exceptions.InvalidEnvironmentError): + client.cached_datasets.get() + + @responses.activate + def test_get_one(self): + keen_response = self.datasets[0] + + url = "{0}/{1}/projects/{2}/datasets/{3}".format( + self.client.api.base_url, + self.client.api.api_version, + self.project_id, + self.datasets[0]['dataset_name'] + ) From c2b3b90be8b5a74288ce8c179d79d0c9d3d2d326 Mon Sep 17 00:00:00 2001 From: Ben Steadman Date: Mon, 26 Nov 2018 11:50:21 +0000 Subject: [PATCH 209/224] cached datasets create method --- keen/cached_datasets.py | 12 ++++++ keen/tests/cached_datasets_tests.py | 65 +++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/keen/cached_datasets.py b/keen/cached_datasets.py index 4a2d430..328f902 100644 --- a/keen/cached_datasets.py +++ b/keen/cached_datasets.py @@ -29,6 +29,18 @@ def get(self, dataset_name): url = "{}/{}".format(self._cached_datasets_url, dataset_name) return self._get_json(HTTPMethods.GET, url, self._get_read_key()) + @requires_key(KeenKeys.MASTER) + def create(self, dataset_name, query, index_by, display_name): + """ Create a Cached Dataset for a Project. Master key must be set. + """ + url = "{}/{}".format(self._cached_datasets_url, dataset_name) + payload = { + "query": query, + "index_by": index_by, + "display_name": display_name + } + return self._get_json(HTTPMethods.PUT, url, self._get_master_key(), json=payload) + def _get_json(self, http_method, url, key, *args, **kwargs): response = self.api.fulfill( http_method, diff --git a/keen/tests/cached_datasets_tests.py b/keen/tests/cached_datasets_tests.py index c423bf4..45f57a1 100644 --- a/keen/tests/cached_datasets_tests.py +++ b/keen/tests/cached_datasets_tests.py @@ -114,3 +114,68 @@ def test_get_one(self): self.project_id, self.datasets[0]['dataset_name'] ) + + def test_create_raises_with_no_keys(self): + client = KeenClient(project_id=self.project_id) + + with self.assertRaises(exceptions.InvalidEnvironmentError): + client.cached_datasets.create( + "NEW_DATASET", {}, "product.id", "My new dataset" + ) + + def test_create_raises_with_read_key(self): + client = KeenClient(project_id=self.project_id, read_key=self.read_key) + + with self.assertRaises(exceptions.InvalidEnvironmentError): + client.cached_datasets.create( + "NEW_DATASET", {}, "product.id", "My new dataset" + ) + + @responses.activate + def test_create(self): + dataset_name = "NEW_DATASET" + display_name = "My new dataset" + query = { + "project_id": "PROJECT ID", + "analysis_type": "count", + "event_collection": "purchases", + "filters": [ + { + "property_name": "price", + "operator": "gte", + "property_value": 100 + } + ], + "timeframe": "this_500_days", + "timezone": None, + "interval": "daily", + "group_by": ["ip_geo_info.country"] + } + index_by = "product.id" + + keen_response = { + "project_id": self.project_id, + "organization_id": self.organization_id, + "dataset_name": dataset_name, + "display_name": display_name, + "query": query, + "index_by": index_by, + "last_scheduled_date": "1970-01-01T00:00:00.000Z", + "latest_subtimeframe_available": "1970-01-01T00:00:00.000Z", + "milliseconds_behind": 3600000 + } + + url = "{0}/{1}/projects/{2}/datasets/{3}".format( + self.client.api.base_url, + self.client.api.api_version, + self.project_id, + dataset_name + ) + + responses.add(responses.PUT, url, status=201, json=keen_response) + + dataset = self.client.cached_datasets.create( + dataset_name, query, index_by, display_name + ) + + self.assertEqual(dataset, keen_response) From 30c161b01679bc2405d3ec83daf33ced2d89f877 Mon Sep 17 00:00:00 2001 From: Ben Steadman Date: Mon, 26 Nov 2018 15:53:35 +0000 Subject: [PATCH 210/224] string format param numbers for consistency --- keen/cached_datasets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keen/cached_datasets.py b/keen/cached_datasets.py index 328f902..32aaeae 100644 --- a/keen/cached_datasets.py +++ b/keen/cached_datasets.py @@ -26,14 +26,14 @@ def get(self, dataset_name): :param dataset_name: Name of Cached Dataset (not `display_name`) """ - url = "{}/{}".format(self._cached_datasets_url, dataset_name) + url = "{0}/{1}".format(self._cached_datasets_url, dataset_name) return self._get_json(HTTPMethods.GET, url, self._get_read_key()) @requires_key(KeenKeys.MASTER) def create(self, dataset_name, query, index_by, display_name): """ Create a Cached Dataset for a Project. Master key must be set. """ - url = "{}/{}".format(self._cached_datasets_url, dataset_name) + url = "{0}/{1}".format(self._cached_datasets_url, dataset_name) payload = { "query": query, "index_by": index_by, From 5a5fa7fb0b195bffd34d95d890afa0f5bac90070 Mon Sep 17 00:00:00 2001 From: Ben Steadman Date: Mon, 26 Nov 2018 15:54:36 +0000 Subject: [PATCH 211/224] cached datsets results method --- keen/cached_datasets.py | 18 +++ keen/tests/cached_datasets_tests.py | 211 ++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) diff --git a/keen/cached_datasets.py b/keen/cached_datasets.py index 32aaeae..3493fda 100644 --- a/keen/cached_datasets.py +++ b/keen/cached_datasets.py @@ -41,6 +41,24 @@ def create(self, dataset_name, query, index_by, display_name): } return self._get_json(HTTPMethods.PUT, url, self._get_master_key(), json=payload) + @requires_key(KeenKeys.READ) + def results(self, dataset_name, index_by, timeframe): + """ Retrieve results from a Cached Dataset. Read key must be set. + """ + url = "{0}/{1}/results".format(self._cached_datasets_url, dataset_name) + + index_by = index_by if isinstance(index_by, str) else json.dumps(index_by) + timeframe = timeframe if isinstance(timeframe, str) else json.dumps(timeframe) + + query_params = { + "index_by": index_by, + "timeframe": timeframe + } + + return self._get_json( + HTTPMethods.GET, url, self._get_read_key(), params=query_params + ) + def _get_json(self, http_method, url, key, *args, **kwargs): response = self.api.fulfill( http_method, diff --git a/keen/tests/cached_datasets_tests.py b/keen/tests/cached_datasets_tests.py index 45f57a1..6bbe6f2 100644 --- a/keen/tests/cached_datasets_tests.py +++ b/keen/tests/cached_datasets_tests.py @@ -1,3 +1,5 @@ +import json + import responses from keen import exceptions @@ -179,3 +181,212 @@ def test_create(self): ) self.assertEqual(dataset, keen_response) + + def test_results_raises_with_no_keys(self): + client = KeenClient(project_id=self.project_id) + + with self.assertRaises(exceptions.InvalidEnvironmentError): + client.cached_datasets.results( + "DATASET_ONE", "product.id", "this_100_days" + ) + + @responses.activate + def test_results(self): + keen_response = { + "result": [ + { + "timeframe": { + "start": "2016-11-02T00:00:00.000Z", + "end": "2016-11-02T00:01:00.000Z" + }, + "value": [ + { + "exception.name": "ValueError", + "result": 20 + }, + { + "exception.name": "KeyError", + "result": 18 + } + ] + }, + { + "timeframe": { + "start": "2016-11-02T01:00:00.000Z", + "end": "2016-11-02T02:00:00.000Z" + }, + "value": [ + { + "exception.name": "ValueError", + "result": 1 + }, + { + "exception.name": "KeyError", + "result": 13 + } + ] + } + ] + } + + dataset_name = self.datasets[0]["dataset_name"] + index_by = self.project_id + timeframe = "this_two_hours" + + url = "{0}/{1}/projects/{2}/datasets/{3}/results?index_by={4}&timeframe={5}".format( + self.client.api.base_url, + self.client.api.api_version, + self.project_id, + dataset_name, + index_by, + timeframe + ) + + responses.add( + responses.GET, + url, + status=200, + json=keen_response, + match_querystring=True + ) + + results = self.client.cached_datasets.results( + dataset_name, index_by, timeframe + ) + + self.assertEqual(results, keen_response) + + @responses.activate + def test_results_absolute_timeframe(self): + keen_response = { + "result": [ + { + "timeframe": { + "start": "2016-11-02T00:00:00.000Z", + "end": "2016-11-02T00:01:00.000Z" + }, + "value": [ + { + "exception.name": "ValueError", + "result": 20 + }, + { + "exception.name": "KeyError", + "result": 18 + } + ] + }, + { + "timeframe": { + "start": "2016-11-02T01:00:00.000Z", + "end": "2016-11-02T02:00:00.000Z" + }, + "value": [ + { + "exception.name": "ValueError", + "result": 1 + }, + { + "exception.name": "KeyError", + "result": 13 + } + ] + } + ] + } + + dataset_name = self.datasets[0]["dataset_name"] + index_by = self.project_id + timeframe = { + "start": "2016-11-02T00:00:00.000Z", + "end": "2016-11-02T02:00:00.000Z" + } + + url = "{0}/{1}/projects/{2}/datasets/{3}/results?index_by={4}&timeframe={5}".format( + self.client.api.base_url, + self.client.api.api_version, + self.project_id, + dataset_name, + index_by, + json.dumps(timeframe) + ) + + responses.add( + responses.GET, + url, + status=200, + json=keen_response, + match_querystring=True + ) + + results = self.client.cached_datasets.results( + dataset_name, index_by, timeframe + ) + + self.assertEqual(results, keen_response) + + @responses.activate + def test_results_multiple_index_by(self): + keen_response = { + "result": [ + { + "timeframe": { + "start": "2016-11-02T00:00:00.000Z", + "end": "2016-11-02T00:01:00.000Z" + }, + "value": [ + { + "exception.name": "ValueError", + "result": 20 + }, + { + "exception.name": "KeyError", + "result": 18 + } + ] + }, + { + "timeframe": { + "start": "2016-11-02T01:00:00.000Z", + "end": "2016-11-02T02:00:00.000Z" + }, + "value": [ + { + "exception.name": "ValueError", + "result": 1 + }, + { + "exception.name": "KeyError", + "result": 13 + } + ] + } + ] + } + + dataset_name = self.datasets[0]["dataset_name"] + index_by = [self.project_id, 'another_id'] + timeframe = "this_two_hours" + + url = "{0}/{1}/projects/{2}/datasets/{3}/results?index_by={4}&timeframe={5}".format( + self.client.api.base_url, + self.client.api.api_version, + self.project_id, + dataset_name, + json.dumps(index_by), + timeframe + ) + + responses.add( + responses.GET, + url, + status=200, + json=keen_response, + match_querystring=True + ) + + results = self.client.cached_datasets.results( + dataset_name, index_by, timeframe + ) + + self.assertEqual(results, keen_response) \ No newline at end of file From d23e98f4e20f5c53f694fefed583d623296220c5 Mon Sep 17 00:00:00 2001 From: Ben Steadman Date: Mon, 26 Nov 2018 16:14:21 +0000 Subject: [PATCH 212/224] cached datasets delete method --- keen/cached_datasets.py | 8 ++++++++ keen/tests/cached_datasets_tests.py | 23 ++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/keen/cached_datasets.py b/keen/cached_datasets.py index 3493fda..a765884 100644 --- a/keen/cached_datasets.py +++ b/keen/cached_datasets.py @@ -59,6 +59,14 @@ def results(self, dataset_name, index_by, timeframe): HTTPMethods.GET, url, self._get_read_key(), params=query_params ) + @requires_key(KeenKeys.MASTER) + def delete(self, dataset_name): + """ Delete a Cached Dataset. Master Key must be set. + """ + url = "{0}/{1}".format(self._cached_datasets_url, dataset_name) + self._get_json(HTTPMethods.DELETE, url, self._get_master_key()) + return True + def _get_json(self, http_method, url, key, *args, **kwargs): response = self.api.fulfill( http_method, diff --git a/keen/tests/cached_datasets_tests.py b/keen/tests/cached_datasets_tests.py index 6bbe6f2..1e49bf0 100644 --- a/keen/tests/cached_datasets_tests.py +++ b/keen/tests/cached_datasets_tests.py @@ -389,4 +389,25 @@ def test_results_multiple_index_by(self): dataset_name, index_by, timeframe ) - self.assertEqual(results, keen_response) \ No newline at end of file + self.assertEqual(results, keen_response) + + def test_delete_raises_with_no_keys(self): + client = KeenClient(project_id=self.project_id) + with self.assertRaises(exceptions.InvalidEnvironmentError): + client.cached_datasets.delete("MY_DATASET_NAME") + + def test_create_raises_with_read_key(self): + client = KeenClient(project_id=self.project_id, read_key=self.read_key) + with self.assertRaises(exceptions.InvalidEnvironmentError): + client.cached_datasets.delete("MY_DATASET_NAME") + + @responses.activate + def test_delete(self): + dataset_name = "MY_DATASET_NAME" + url = "{0}/{1}/projects/{2}/datasets/{3}".format( + self.client.api.base_url, self.client.api.api_version, self.project_id, dataset_name) + responses.add(responses.DELETE, url, status=204) + + response = self.client.cached_datasets.delete(dataset_name) + + self.assertTrue(response) From 25f839dda8bfc365347f57a512de4486cf731ab8 Mon Sep 17 00:00:00 2001 From: Ben Steadman Date: Mon, 26 Nov 2018 16:37:03 +0000 Subject: [PATCH 213/224] Add cached datasets to README --- README.rst | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/README.rst b/README.rst index c932bac..d1f01f5 100644 --- a/README.rst +++ b/README.rst @@ -406,6 +406,85 @@ You can manage your `saved queries Date: Wed, 28 Nov 2018 11:25:41 -0800 Subject: [PATCH 214/224] Loosen six version requirement See comments on #149. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8224fb4..e087a6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ pycryptodome>=3.4 requests>=2.5,<3.0 -six~=1.10.0 +six~=1.10 From 65d89c17ab9c9855dfc6199acdfcc433556a494f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksander=20Papie=C5=BC?= Date: Tue, 10 Dec 2019 15:04:07 +0100 Subject: [PATCH 215/224] Add support for Python 3.7 and 3.8 (#155) --- .travis.yml | 2 ++ CHANGELOG.rst | 7 +++++++ README.rst | 4 ++-- keen/utilities.py | 2 +- setup.py | 4 +++- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 458f6c6..56126bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ python: - "3.4" - "3.5" - "3.6" + - "3.7" + - "3.8" # command to install dependencies install: | pip install -r requirements.txt diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7cea24e..b36dfc8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,11 +1,18 @@ Changelog --------- +0.5.2 +`````` + ++ Added support for Python 3.7 and 3.8. + + 0.5.1 `````` + Fixed various Access Key bugs. + 0.5.0 `````` + Added support for Access Keys. diff --git a/README.rst b/README.rst index d1f01f5..d229641 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ Use pip to install! pip install keen -This client is known to work on Python 2.7, 3.4, 3.5 and 3.6. +This client is known to work on Python 2.7, 3.4, 3.5, 3.6, 3.7 and 3.8. For versions of Python < 2.7.9, you’ll need to install pyasn1, ndg-httpsclient, pyOpenSSL. @@ -652,7 +652,7 @@ To run tests: Changelog --------- -This project is in alpha stage at version 0.5.1 . See the full CHANGELOG `here <./CHANGELOG.rst>`_. +This project is in alpha stage at version 0.5.2. See the full CHANGELOG `here <./CHANGELOG.rst>`_. Questions & Support diff --git a/keen/utilities.py b/keen/utilities.py index 2738134..6cf0961 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -5,7 +5,7 @@ from keen import exceptions -VERSION = "0.5.1" +VERSION = "0.5.2" def version(): """ diff --git a/setup.py b/setup.py index 2062f8d..2bed7d6 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="keen", - version="0.5.1", + version="0.5.2", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", @@ -47,6 +47,8 @@ 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Software Development :: Libraries :: Python Modules', ] ) From 23f00f33a6c9b2494fd23a90f5de3ad7162fd95e Mon Sep 17 00:00:00 2001 From: Wiktor Olko Date: Thu, 19 Mar 2020 21:38:26 +0100 Subject: [PATCH 216/224] Configure automatic PyPI deployment when a branch is merged to master --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 56126bf..a031ab2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,3 +12,10 @@ install: | # command to run tests script: "python setup.py test" + +# deploy master to PyPI +deploy: + provider: pypi + user: wiktor.olko + password: + secure: nxwi3BYKikV+nz3+OLWv7BXakLM2I+EafvCRiBoYUNQ2BBzg2pyBqwKna/AeqID8ASaHYWfHha64+7rDvJD1Sh7/Xgcc0GzDhtqUAiKXyKsdlg+4Q0XHWT3jeRTA2+vkqYMuren3MlbcSgzyyNO/IKf3zItiX+cA8/dyxRqOe3g= From 6c5ef64d0d1fa83b7af7e4f55d1fe8dcd7578572 Mon Sep 17 00:00:00 2001 From: Wiktor Olko Date: Fri, 20 Mar 2020 11:00:01 +0100 Subject: [PATCH 217/224] Fix README.rst to comply with the ReStructuredText standard --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index d229641..cabd16e 100644 --- a/README.rst +++ b/README.rst @@ -621,7 +621,7 @@ The Python client enables the creation and manipulation of `Access Keys `_ easily, but Access Keys are better! If you need to use them anyway, for legacy reasons, here's how: @@ -652,7 +652,7 @@ To run tests: Changelog --------- -This project is in alpha stage at version 0.5.2. See the full CHANGELOG `here <./CHANGELOG.rst>`_. +This project is in alpha stage at version 0.5.2. See the full `CHANGELOG <./CHANGELOG.rst>`_. Questions & Support From f2f2e395e51c5700e576034e9a007779e99a5164 Mon Sep 17 00:00:00 2001 From: Wiktor Olko Date: Fri, 20 Mar 2020 12:43:57 +0100 Subject: [PATCH 218/224] Do not publish to PyPI if file already exists. --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index a031ab2..32b2397 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,5 +17,8 @@ script: "python setup.py test" deploy: provider: pypi user: wiktor.olko + # encrypted with the Keen Travis project PublicKey. Only Travis has the PrivateKey and is able to decode it. + # travis encrypt PASSWORD_HERE --add deploy.password password: secure: nxwi3BYKikV+nz3+OLWv7BXakLM2I+EafvCRiBoYUNQ2BBzg2pyBqwKna/AeqID8ASaHYWfHha64+7rDvJD1Sh7/Xgcc0GzDhtqUAiKXyKsdlg+4Q0XHWT3jeRTA2+vkqYMuren3MlbcSgzyyNO/IKf3zItiX+cA8/dyxRqOe3g= + skip_existing: true From 61643f936e892569bccf63846b25ddad96322f84 Mon Sep 17 00:00:00 2001 From: Wiktor Olko Date: Fri, 20 Mar 2020 17:16:26 +0100 Subject: [PATCH 219/224] Add delete_access_key method to Access Key API --- README.rst | 3 +++ keen/__init__.py | 10 ++++++++++ keen/api.py | 14 ++++++++++++++ keen/client.py | 8 ++++++++ keen/tests/access_key_tests.py | 8 ++++++++ 5 files changed, 43 insertions(+) diff --git a/README.rst b/README.rst index cabd16e..dd77d97 100644 --- a/README.rst +++ b/README.rst @@ -582,6 +582,9 @@ The Python client enables the creation and manipulation of `Access Keys Date: Fri, 20 Mar 2020 17:27:46 +0100 Subject: [PATCH 220/224] Rename access_key_id to key in Access Keys API to be consistent with Keen API. Fix parameter name in README --- README.rst | 22 +++++++-------- keen/__init__.py | 60 ++++++++++++++++++++--------------------- keen/api.py | 70 ++++++++++++++++++++++++------------------------ keen/client.py | 60 ++++++++++++++++++++--------------------- 4 files changed, 106 insertions(+), 106 deletions(-) diff --git a/README.rst b/README.rst index dd77d97..28cae44 100644 --- a/README.rst +++ b/README.rst @@ -567,48 +567,48 @@ The Python client enables the creation and manipulation of `Access Keys Date: Fri, 20 Mar 2020 17:47:08 +0100 Subject: [PATCH 221/224] Bump version to 0.6.0, update readme --- CHANGELOG.rst | 8 ++++++++ README.rst | 2 +- keen/api.py | 4 ++-- keen/utilities.py | 2 +- setup.py | 2 +- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b36dfc8..962f351 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog --------- +0.6.0 +`````` + ++ Added the delete_access_key method ++ Renamed the argument names in Access Key API (breaking change) ++ Dropped support for Python 3.4 + + 0.5.2 `````` diff --git a/README.rst b/README.rst index 28cae44..7ce613b 100644 --- a/README.rst +++ b/README.rst @@ -655,7 +655,7 @@ To run tests: Changelog --------- -This project is in alpha stage at version 0.5.2. See the full `CHANGELOG <./CHANGELOG.rst>`_. +This project is in alpha stage at version 0.6.0. See the full `CHANGELOG <./CHANGELOG.rst>`_. Questions & Support diff --git a/keen/api.py b/keen/api.py index d0e3304..c7b9728 100644 --- a/keen/api.py +++ b/keen/api.py @@ -325,7 +325,7 @@ def _build_access_key_dict(access_key): "options": access_key["options"] } - def _update_access_key_pair(self, key, key, val): + def _update_access_key_pair(self, key, field, val): """ Helper for updating access keys in a DRY fashion. """ @@ -334,7 +334,7 @@ def _update_access_key_pair(self, key, key, val): # Copy and only change the single parameter. payload_dict = KeenApi._build_access_key_dict(current_access_key) - payload_dict[key] = val + payload_dict[field] = val # Now just treat it like a full update. return self.update_access_key_full(key, **payload_dict) diff --git a/keen/utilities.py b/keen/utilities.py index 6cf0961..8a1be82 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -5,7 +5,7 @@ from keen import exceptions -VERSION = "0.5.2" +VERSION = "0.6.0" def version(): """ diff --git a/setup.py b/setup.py index 2bed7d6..bf8a837 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="keen", - version="0.5.2", + version="0.6.0", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", From 8ae17995749acff0f7a49d8480a626839edd268d Mon Sep 17 00:00:00 2001 From: Wiktor Olko Date: Fri, 20 Mar 2020 17:58:26 +0100 Subject: [PATCH 222/224] Bump version to 0.6.1, drop support for Python 3.4 --- .travis.yml | 1 - CHANGELOG.rst | 7 ++++++- README.rst | 4 ++-- keen/utilities.py | 2 +- setup.py | 3 +-- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 32b2397..0d2c80b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - "2.7" - - "3.4" - "3.5" - "3.6" - "3.7" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 962f351..45d9856 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,12 +1,17 @@ Changelog --------- +0.6.1 +`````` + ++ Dropped support for Python 3.4 + + 0.6.0 `````` + Added the delete_access_key method + Renamed the argument names in Access Key API (breaking change) -+ Dropped support for Python 3.4 0.5.2 diff --git a/README.rst b/README.rst index 7ce613b..106fb55 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ Use pip to install! pip install keen -This client is known to work on Python 2.7, 3.4, 3.5, 3.6, 3.7 and 3.8. +This client is known to work on Python 2.7, 3.5, 3.6, 3.7 and 3.8. For versions of Python < 2.7.9, you’ll need to install pyasn1, ndg-httpsclient, pyOpenSSL. @@ -655,7 +655,7 @@ To run tests: Changelog --------- -This project is in alpha stage at version 0.6.0. See the full `CHANGELOG <./CHANGELOG.rst>`_. +This project is in alpha stage at version 0.6.1. See the full `CHANGELOG <./CHANGELOG.rst>`_. Questions & Support diff --git a/keen/utilities.py b/keen/utilities.py index 8a1be82..605d586 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -5,7 +5,7 @@ from keen import exceptions -VERSION = "0.6.0" +VERSION = "0.6.1" def version(): """ diff --git a/setup.py b/setup.py index bf8a837..8150ba7 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="keen", - version="0.6.0", + version="0.6.1", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO", @@ -44,7 +44,6 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', From adbb89d3e04ee2927a4aa661951cb28ef53843c3 Mon Sep 17 00:00:00 2001 From: Wiktor Olko Date: Fri, 8 Jan 2021 11:04:11 +0100 Subject: [PATCH 223/224] Do not force TLSv1 which is deprecated --- keen/api.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/keen/api.py b/keen/api.py index c7b9728..0d2a0e9 100644 --- a/keen/api.py +++ b/keen/api.py @@ -34,12 +34,11 @@ class KeenAdapter(HTTPAdapter): def init_poolmanager(self, connections, maxsize, block=False): - """ Initialize pool manager with forced TLSv1 support. """ + """ Initialize pool manager """ self.poolmanager = PoolManager(num_pools=connections, maxsize=maxsize, - block=block, - ssl_version=ssl.PROTOCOL_TLSv1) + block=block) class KeenApi(object): From 2c753163cd9d36ed8f44ca25b0cedd65c0d17502 Mon Sep 17 00:00:00 2001 From: Wiktor Olko Date: Fri, 8 Jan 2021 11:22:59 +0100 Subject: [PATCH 224/224] Bump version to 0.7.0 --- CHANGELOG.rst | 6 ++++++ README.rst | 2 +- keen/utilities.py | 2 +- setup.py | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 45d9856..7850e0a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Changelog --------- +0.7.0 +`````` + ++ Do not force TLSv1 which is deprecated + + 0.6.1 `````` diff --git a/README.rst b/README.rst index 106fb55..bf2eb28 100644 --- a/README.rst +++ b/README.rst @@ -655,7 +655,7 @@ To run tests: Changelog --------- -This project is in alpha stage at version 0.6.1. See the full `CHANGELOG <./CHANGELOG.rst>`_. +This project is in alpha stage at version 0.7.0. See the full `CHANGELOG <./CHANGELOG.rst>`_. Questions & Support diff --git a/keen/utilities.py b/keen/utilities.py index 605d586..6d8ab14 100644 --- a/keen/utilities.py +++ b/keen/utilities.py @@ -5,7 +5,7 @@ from keen import exceptions -VERSION = "0.6.1" +VERSION = "0.7.0" def version(): """ diff --git a/setup.py b/setup.py index 8150ba7..41a3be0 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ setup( name="keen", - version="0.6.1", + version="0.7.0", description="Python Client for Keen IO", long_description=codecs.open(os.path.join('README.rst'), 'r', encoding='UTF-8').read(), author="Keen IO",