From 72d2a5ce00658f22edef398ccb865caf511ffc7e Mon Sep 17 00:00:00 2001 From: BlackVegetable Date: Wed, 13 Sep 2017 09:03:01 -0600 Subject: [PATCH 01/14] 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 02/14] 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 03/14] 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 04/14] 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 05/14] 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 06/14] 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 07/14] 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 08/14] 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 09/14] 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 10/14] 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 11/14] 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 12/14] 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 13/14] +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 14/14] 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',