Skip to content

Commit

Permalink
#185 added get_project() helper function
Browse files Browse the repository at this point in the history
* added helper.py in pykechain
* ensured that `from pykechain import get_project` works
* added documentation to the documentation site
* added tests for the `get_project()`
* added `ClientError` to feedback errors that are with establishing a client, even before connection made
* updated the `Client.__init__()` to check if the url is valid
  • Loading branch information
Jochem Berends committed Sep 12, 2017
1 parent 0f33b1e commit 78cf6c3
Show file tree
Hide file tree
Showing 11 changed files with 174 additions and 6 deletions.
7 changes: 7 additions & 0 deletions docs/api/helpers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@


pykechain.helpers
=================

.. automodule:: pykechain.helpers
:members:
3 changes: 2 additions & 1 deletion pykechain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from .__about__ import version
from .client import Client
from .helpers import get_project

__all__ = (
'Client', 'version'
'Client', 'get_project', 'version'
)
22 changes: 19 additions & 3 deletions pykechain/client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import sys
from typing import Dict, Tuple, Optional, Any, List # flake8: noqa

import os
import requests
from envparse import env
from requests.compat import urljoin # type: ignore
from typing import Dict, Tuple, Optional, Any, List # flake8: noqa

from pykechain.enums import Category
from .__about__ import version
from .exceptions import ForbiddenError, NotFoundError, MultipleFoundError, APIError
from .exceptions import ForbiddenError, NotFoundError, MultipleFoundError, APIError, ClientError
from .models import Scope, Activity, Part, PartSet, Property

if sys.version_info.major < 3:
from urlparse import urlparse
else:
from urllib.parse import urlparse

API_PATH = {
'scopes': 'api/scopes.json',
'scope': 'api/scopes/{scope_id}.json',
Expand Down Expand Up @@ -53,6 +61,10 @@ def __init__(self, url='http://localhost:8000/', check_certificates=True):
>>> client = Client(url='https://default-tst.localhost:9443', check_certificates=False)
"""
parsed_url = urlparse(url)
if not (parsed_url.scheme and parsed_url.netloc):
raise ClientError("Please provide a valid URL to a KE-chain instance")

self.session = requests.Session()
self.api_root = url
self.headers = {'X-Requested-With': 'XMLHttpRequest', 'PyKechain-Version': version} # type: Dict[str, str]
Expand Down Expand Up @@ -94,11 +106,15 @@ def from_env(cls, env_filename=None):
KECHAIN_USERNAME=...
KECHAIN_PASSWORD=...
# optional add a scope name or scope id
KECHAIN_SCOPE=...
KECHAIN_SCOPE_ID=...
>>> client = Client().from_env()
"""
env.read_envfile(env_filename)
if env_filename and os.path.exists(env_filename):
env.read_envfile(env_filename)
client = cls(url=env('KECHAIN_URL'))

if env('KECHAIN_TOKEN', None):
Expand Down
6 changes: 6 additions & 0 deletions pykechain/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ class NotFoundError(APIError):
pass


class ClientError(APIError):
"""Error with instantiating the Client."""

pass


class InspectorComponentError(Exception):
"""Error in the InspectorComponent."""

Expand Down
81 changes: 81 additions & 0 deletions pykechain/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from envparse import env

from pykechain.client import Client
from pykechain.exceptions import ClientError


def get_project(url=None, username=None, password=None, token=None, scope=None, scope_id=None,
env_filename=None):
"""
Retrieve and return the KE-chain project to be used throughout an app.
This helper is made to bootstrap a pykechain enabled python script or an jupyter notebook with the correct
project (technically this is a `pykechain.models.Scope` model).
When no parameters are passed in this function, it will try to retrieve `url`, `token`, `scope` (or `scope_id`)
from the environment variables or a neatly placed '.env' file.
:param url: (optional) url of KE-chain
:param username: (optional) username for authentication (together with password, if not token)
:param password: (optional) password for username/password authentication (together with username, if not token)
:param token: (optional) token for authentication (if not username/password)
:param scope: (optional) name of the scope to retrieve from KE-chain.
:param scope_id: (optional) UUID of the scope to retrieve and return from KE-chain
:param env_filename: (optional) name of the environment filename to bootstrap the Client
:return: pykechain.models.Scope
:raises: NotFoundError, ClientError, APIError
Example
-------
An example with parameters provided
>>> from pykechain import get_project
>>> project = get_project(url='http://localhost:8000',
... username='foo', password='bar', scope='1st!')
>>> print(project.name)
1st
An example with a .env file on disk::
# This is an .env file on disk.
KECHAIN_TOKEN=bd9377793f7e74a29dbb11fce969
KECHAIN_URL=http://localhost:8080
KECHAIN_SCOPE_ID=c9f0-228e-4d3a-9dc0-ec5a75d7
>>> project = get_project(env_filename='/path/to/.env')
>>> project.id
c9f0-228e-4d3a-9dc0-ec5a75d7
An example for get_project that will extract all from the environment variables
>>> env_vars = os.environ
>>> env_vars.get('KECHAIN_TOKEN')
bd9377793f7e74a29dbb11fce969
>>> env_vars.get('KECHAIN_URL')
http://localhost:8080
>>> env_vars.get('KECHAIN_SCOPE')
Bike Project
>>> project = get_project()
>>> project.name
Bike Project
"""
# assert url, "Please provide a URL"
# assert (username and password) or token, "Please provide username and password or a token"
# assert scope or scope_id, "Please provide a scope_id or a scope name"

if not any((url, username, password, token, scope, scope_id)):
client = Client.from_env(env_filename=env_filename)
scope_id = env('KECHAIN_SCOPE_ID', default=None)
scope = env('KECHAIN_SCOPE', default=None)
elif (url and (username and password) or token and (scope or scope_id)):
client = Client(url=url)
client.login(username=username, password=password, token=token)
else:
raise ClientError(
"Error: We need to have sufficient arguments to connect to KE-chain. "
"See documentation of `pykechain.get_project()`")

if scope_id:
return client.scope(pk=scope_id)
else:
return client.scope(name=scope)
1 change: 1 addition & 0 deletions tests/cassettes/TestGetProjectHelper.test_get_project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"recorded_with": "betamax/0.8.0", "http_interactions": [{"recorded_at": "2017-09-12T13:57:41", "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA5VU32/TMBD+VyY/gVS3jp3EcZ8GYw9ISCAx7QE0TY59ab0mTmQ7wDT1f+fSXxuIAVMaqefvu7vvznd5IAHi2KZIll8fyBhasiTrlIa4XCw2YLge3HwD1Ky183PTdws8WETTDxAXZSNroxpGOa+A5lZoqqxhFEyhZWGlgMzO72LvyYw4i4H/xwG5XneA7LduA2efQn8HJp29Gu5RzqTiLEFMzq9eIzPBj4RM/BeTTiMWQd5cXL2/vpywoH10yfV+X9vardYtvglQSgojPHG6+PDx8+W7x9Q7m2xvZkTH6FYe8NCPbTsjHXQ1hH1EF2+1Se4bHOPhwR4/HkCnXbsXOGHa69UjOLVEnlIiZYwQDtapWrKdvSRRfV7PfftsOv5MuqmlR/rLEt71Zg3dHBHwNp7jqHzvwyZOo/Kbika38SCjfEbGr8FeqET/qfQnScVfap/M3XXjRIR0a3WaEM4ySVlGeXnFxZIx/H1BXzvCPxj1aDaAg4nyPUb0Bm5D30+T2hQ8E0IA1RlYmkNZ0DorS6qUalSmRWWaDAN0vYX26CMV18LUQJkoStwaldMqrxpaG1M3teCsMeK4YTljyOKSZri6NJelphWXuGG1LCSHUgEw7CsZQm8gTqPPrIZCCUUrIQzNK6lprcqcssLmRoBqwCiyc1iFnQeb57LimSoPD44wWJd03Z7uaIDQOVyc3erhV2XYt2sPBtCnBbTY+9DfH02D2ETc3dp2e7P9CSwjYpGeBAAA", "encoding": null}, "headers": {"Content-Type": "application/json", "X-XSS-Protection": "1; mode=block", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "SAMEORIGIN", "Vary": "Accept-Encoding", "Server": "nginx/1.10.2", "Connection": "keep-alive", "Transfer-Encoding": "chunked", "Strict-Transport-Security": "max-age=518400; includeSubDomains", "Date": "Tue, 12 Sep 2017 13:57:41 GMT", "Allow": "GET, POST, HEAD, OPTIONS", "Content-Encoding": "gzip"}, "url": "<API_URL>/api/scopes.json?status=ACTIVE&name=Bike+Project+%28pykechain+testing%29", "status": {"code": 200, "message": "OK"}}, "request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": "*/*", "Accept-Encoding": "gzip, deflate", "X-Requested-With": "XMLHttpRequest", "User-Agent": "python-requests/2.18.4", "Authorization": "Token <AUTH_TOKEN>", "PyKechain-Version": "1.11.1", "Connection": "keep-alive"}, "uri": "<API_URL>/api/scopes.json?status=ACTIVE&name=Bike+Project+%28pykechain+testing%29", "method": "GET"}}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"recorded_with": "betamax/0.8.0", "http_interactions": [{"recorded_at": "2017-09-12T13:57:42", "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA5VU32/TMBD+VyY/gVS3jp3EcZ8GYw9ISCAx7QE0TY59ab0mTmQ7wDT1f+fSXxuIAVMaqefvu7vvznd5IAHi2KZIll8fyBhasiTrlIa4XCw2YLge3HwD1Ky183PTdws8WETTDxAXZSNroxpGOa+A5lZoqqxhFEyhZWGlgMzO72LvyYw4i4H/xwG5XneA7LduA2efQn8HJp29Gu5RzqTiLEFMzq9eIzPBj4RM/BeTTiMWQd5cXL2/vpywoH10yfV+X9vardYtvglQSgojPHG6+PDx8+W7x9Q7m2xvZkTH6FYe8NCPbTsjHXQ1hH1EF2+1Se4bHOPhwR4/HkCnXbsXOGHa69UjOLVEnlIiZYwQDtapWrKdvSRRfV7PfftsOv5MuqmlR/rLEt71Zg3dHBHwNp7jqHzvwyZOo/Kbika38SCjfEbGr8FeqET/qfQnScVfap/M3XXjRIR0a3WaEM4ySVlGeXnFxZIx/H1BXzvCPxj1aDaAg4nyPUb0Bm5D30+T2hQ8E0IA1RlYmkNZ0DorS6qUalSmRWWaDAN0vYX26CMV18LUQJkoStwaldMqrxpaG1M3teCsMeK4YTljyOKSZri6NJelphWXuGG1LCSHUgEw7CsZQm8gTqPPrIZCCUUrIQzNK6lprcqcssLmRoBqwCiyc1iFnQeb57LimSoPD44wWJd03Z7uaIDQOVyc3erhV2XYt2sPBtCnBbTY+9DfH02D2ETc3dp2e7P9CSwjYpGeBAAA", "encoding": null}, "headers": {"Content-Type": "application/json", "X-XSS-Protection": "1; mode=block", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "SAMEORIGIN", "Vary": "Accept-Encoding", "Server": "nginx/1.10.2", "Connection": "keep-alive", "Transfer-Encoding": "chunked", "Strict-Transport-Security": "max-age=518400; includeSubDomains", "Date": "Tue, 12 Sep 2017 13:57:42 GMT", "Allow": "GET, POST, HEAD, OPTIONS", "Content-Encoding": "gzip"}, "url": "<API_URL>/api/scopes.json?status=ACTIVE&name=Bike+Project+%28pykechain+testing%29", "status": {"code": 200, "message": "OK"}}, "request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": "*/*", "Accept-Encoding": "gzip, deflate", "X-Requested-With": "XMLHttpRequest", "User-Agent": "python-requests/2.18.4", "Authorization": "Token <AUTH_TOKEN>", "PyKechain-Version": "1.11.1", "Connection": "keep-alive"}, "uri": "<API_URL>/api/scopes.json?status=ACTIVE&name=Bike+Project+%28pykechain+testing%29", "method": "GET"}}]}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"recorded_with": "betamax/0.8.0", "http_interactions": [{"recorded_at": "2017-09-12T13:57:42", "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA5VU32/TMBD+VyY/gVS3jp3EcZ8GYw9ISCAx7QE0TY59ab0mTmQ7wDT1f+fSXxuIAVMaqefvu7vvznd5IAHi2KZIll8fyBhasiTrlIa4XCw2YLge3HwD1Ky183PTdws8WETTDxAXZSNroxpGOa+A5lZoqqxhFEyhZWGlgMzO72LvyYw4i4H/xwG5XneA7LduA2efQn8HJp29Gu5RzqTiLEFMzq9eIzPBj4RM/BeTTiMWQd5cXL2/vpywoH10yfV+X9vardYtvglQSgojPHG6+PDx8+W7x9Q7m2xvZkTH6FYe8NCPbTsjHXQ1hH1EF2+1Se4bHOPhwR4/HkCnXbsXOGHa69UjOLVEnlIiZYwQDtapWrKdvSRRfV7PfftsOv5MuqmlR/rLEt71Zg3dHBHwNp7jqHzvwyZOo/Kbika38SCjfEbGr8FeqET/qfQnScVfap/M3XXjRIR0a3WaEM4ySVlGeXnFxZIx/H1BXzvCPxj1aDaAg4nyPUb0Bm5D30+T2hQ8E0IA1RlYmkNZ0DorS6qUalSmRWWaDAN0vYX26CMV18LUQJkoStwaldMqrxpaG1M3teCsMeK4YTljyOKSZri6NJelphWXuGG1LCSHUgEw7CsZQm8gTqPPrIZCCUUrIQzNK6lprcqcssLmRoBqwCiyc1iFnQeb57LimSoPD44wWJd03Z7uaIDQOVyc3erhV2XYt2sPBtCnBbTY+9DfH02D2ETc3dp2e7P9CSwjYpGeBAAA", "encoding": null}, "headers": {"Content-Type": "application/json", "X-XSS-Protection": "1; mode=block", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "SAMEORIGIN", "Vary": "Accept-Encoding", "Server": "nginx/1.10.2", "Connection": "keep-alive", "Transfer-Encoding": "chunked", "Strict-Transport-Security": "max-age=518400; includeSubDomains", "Date": "Tue, 12 Sep 2017 13:57:42 GMT", "Allow": "GET, POST, HEAD, OPTIONS", "Content-Encoding": "gzip"}, "url": "<API_URL>/api/scopes.json?status=ACTIVE&name=Bike+Project+%28pykechain+testing%29", "status": {"code": 200, "message": "OK"}}, "request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept": "*/*", "Accept-Encoding": "gzip, deflate", "X-Requested-With": "XMLHttpRequest", "User-Agent": "python-requests/2.18.4", "Authorization": "Token <AUTH_TOKEN>", "PyKechain-Version": "1.11.1", "Connection": "keep-alive"}, "uri": "<API_URL>/api/scopes.json?status=ACTIVE&name=Bike+Project+%28pykechain+testing%29", "method": "GET"}}]}
9 changes: 9 additions & 0 deletions tests/classes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
from unittest import TestCase

import sys
from betamax import Betamax

from pykechain import Client
Expand Down Expand Up @@ -33,3 +34,11 @@ def setUp(self):

def tearDown(self):
self.recorder.stop()

def assertRaisesRegex(self, expected_exception, expected_regex,
*args, **kwargs):
if sys.version_info.major < 3:
return self.assertRaisesRegexp(expected_exception, expected_regex, *args, **kwargs)
else:
return super(__class__,self).assertRaisesRegex(expected_exception, expected_regex,
*args, **kwargs)
11 changes: 9 additions & 2 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from unittest import TestCase

from pykechain.client import Client
from pykechain.exceptions import ForbiddenError
from pykechain.exceptions import ForbiddenError, ClientError
from tests.classes import TestBetamax


class TestClient(object):
class TestClient(TestCase):

def test_init_default_url(self):
client = Client()
Expand Down Expand Up @@ -42,6 +44,11 @@ def test_init_no_ssl(self):

assert client.session.verify is False

# 1.12
def test_client_raises_error_with_false_url(self):
with self.assertRaises(ClientError):
Client(url='wrongurl')


class TestClientLive(TestBetamax):

Expand Down
38 changes: 38 additions & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import os

from pykechain import get_project
from pykechain.exceptions import ClientError, APIError
from tests.classes import TestBetamax

from tests.utils import TEST_TOKEN, TEST_URL, TEST_SCOPE_NAME


class TestGetProjectHelper(TestBetamax):
def test_get_project(self):
project = get_project(TEST_URL, token=TEST_TOKEN, scope=TEST_SCOPE_NAME)
self.assertEqual(project.name, TEST_SCOPE_NAME)

def test_get_project_not_enough_args_raises_error(self):
with self.assertRaisesRegex(ClientError, "sufficient arguments"):
get_project(url=TEST_URL)

def test_get_project_from_env(self):
# setup
saved_environment = dict(os.environ)
os.environ['KECHAIN_URL'] = TEST_URL
os.environ['KECHAIN_TOKEN'] = TEST_TOKEN
os.environ['KECHAIN_SCOPE'] = TEST_SCOPE_NAME

# do test
project = get_project()
self.assertEqual(project.name, os.environ['KECHAIN_SCOPE'])

# teardown
os.unsetenv('KECHAIN_URL')
os.unsetenv('KECHAIN_TOKEN')
os.unsetenv('KECHAIN_SCOPE')
for k,v in saved_environment.items():
os.environ[k]=v



0 comments on commit 78cf6c3

Please sign in to comment.