diff --git a/.travis.yml b/.travis.yml index 6078d90..458f6c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,11 @@ 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/CHANGELOG.rst b/CHANGELOG.rst index 185eeb8..0544b9b 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 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) + + 0.4.0 `````` diff --git a/README.rst b/README.rst index fdbace3..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.2, 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. @@ -470,10 +470,81 @@ 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 `_. Examples: + +.. code-block:: python + + from keen.client import KeenClient + # You could also simply use: import keen + # If you do this, you will need your project ID and master key set in environment variables. + + client = KeenClient( + project_id="xxxx", + master_key="zzzz" + ) + + # Create an access key. See: https://keen.io/docs/access/access-keys/#customizing-your-access-key + client.create_access_key(name="Dave_Barry_Key", is_enabled=True, permitted=["writes", "cached_queries"], + options={"cached_queries": {"allowed": ["dave_barry_in_cyberspace_sales"]}}) + + # Display all access keys associated with this client's project. + client.list_access_keys() + + # Get details on a particular access key. + client.get_access_key(access_key_id="ABCDEFGHIJKLMNOPQRSTUVWXYZ") + + # Revoke (disable) an access key. + client.revoke_access_key(access_key_id="ABCDEFGHIJKLMNOPQRSTUVWXYZ") + + # Unrevoke (re-enable) an access key. + client.unrevoke_access_key(access_key_id="ABCDEFGHIJKLMNOPQRSTUVWXYZ") + + # Change just the name of an access key. + client.update_access_key_name(access_key_id="ABCDEFGHIJKLMNOPQRSTUVWXYZ", name="Some_New_Name") + + # Add new access key permissions to existing permissions on a given key. + # In this case the set of permissions currently contains "writes" and "cached_queries". + # This function call keeps the old permissions and adds "queries" to that set. + # ("writes", "cached_queries") + ("queries") = ("writes", "cached_queries", "queries") + client.add_access_key_permissions(access_key_id="ABCDEFGHIJKLMNOPQRSTUVWXYZ", permissions=["queries"]) + + # Remove one or more access key permissions from a given key. + # In this case the set of permissions currently contains "writes", "cached_queries", and "queries". + # This function call will keep the old permissions not explicitly removed here. + # So we will remove both "writes" and "queries" from the set, leaving only "cached_queries". + # ("writes", "cached_queries", "queries") - ("writes", "queries") = ("cached_queries") + client.remove_access_key_permissions(access_key_id="ABCDEFGHIJKLMNOPQRSTUVWXYZ", permissions=["writes", "queries"]) + + # We can also perform a full update on the permissions, replacing all existing permissions with a new list. + # In this case our existing permissions contains only "cached_queries". + # We will replace this set with the "writes" permission with this function call. + # ("cached_queries") REPLACE-WITH ("writes") = ("writes") + client.update_access_key_permissions(access_key_id="ABCDEFGHIJKLMNOPQRSTUVWXYZ", permissions=["writes"]) + + # Replace all existing key options with this new options object. + client.update_access_key_options(access_key_id="ABCDEFGHIJKLMNOPQRSTUVWXYZ", options={"writes": { + "autofill": { + "customer": { + "id": "93iskds39kd93id", + "name": "Ada Corp." + } + } + }}) + + # Replace everything but the key ID with what is supplied here. + # If a field is not supplied here, it will be set to a blank value. + # In this case, no options are supplied, so all options will be removed. + client.update_access_key_full(access_key_id="ABCDEFGHIJKLMNOPQRSTUVWXYZ", name="Strong_Bad", is_active=True, permitted=["queries"]) + + +Create Scoped Keys (**Deprecated**) +'''''''''''''''''' + +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 @@ -501,7 +572,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/keen/__init__.py b/keen/__init__.py index e71874a..848e1a3 100644 --- a/keen/__init__.py +++ b/keen/__init__.py @@ -496,3 +496,125 @@ 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_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. + + :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_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) + +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 963d161..e64b401 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,232 @@ 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() + + @staticmethod + def _build_access_key_dict(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 = _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_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 = _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 = _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. + + :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!" + + :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) + 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. + + :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) + 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..4998776 100644 --- a/keen/client.py +++ b/keen/client.py @@ -190,6 +190,119 @@ 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_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. + + :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_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) + def _base64_encode(self, string_to_encode): """ Base64 encodes a string, with either Python 2 or 3. 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..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"]) @@ -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): 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(): """ 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..5b560c9 100644 --- a/setup.py +++ b/setup.py @@ -21,11 +21,11 @@ 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", - 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", @@ -42,11 +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.2', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6',