Skip to content

Commit

Permalink
Versioning potion-client solves simplejson issue (#111)
Browse files Browse the repository at this point in the history
  • Loading branch information
polyatail committed Nov 16, 2018
1 parent df71f9f commit a4b9eb4
Show file tree
Hide file tree
Showing 23 changed files with 947 additions and 59 deletions.
8 changes: 4 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
name: Run tests
command: |
. venv/bin/activate
py.test --ignore tests/test_minimal.py --ignore tests/test_simplejson.py tests/
py.test --ignore tests/test_minimal.py tests/
test-py2:
docker:
- image: circleci/python:2.7-stretch
Expand All @@ -44,7 +44,7 @@ jobs:
name: Run tests
command: |
. venv/bin/activate
py.test --ignore tests/test_minimal.py --ignore tests/test_simplejson.py tests/
py.test --ignore tests/test_minimal.py tests/
test-minimal:
docker:
- image: circleci/python:3.4-stretch
Expand Down Expand Up @@ -79,7 +79,7 @@ jobs:
name: Run tests
command: |
. venv/bin/activate
py.test tests/test_simplejson.py
py.test --ignore tests/test_minimal.py tests/
lint:
docker:
- image: circleci/python:2.7-stretch
Expand Down Expand Up @@ -112,6 +112,6 @@ jobs:
name: Run flake8
command: |
. venv/bin/activate
py.test --cov-report=html --cov=onecodex --ignore tests/test_minimal.py --ignore tests/test_simplejson.py tests/
py.test --cov-report=html --cov=onecodex --ignore tests/test_minimal.py tests/
coveralls
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ ignore_errors = True

omit =
onecodex/version.py
onecodex/vendored/*

[html]
directory = htmlcov
6 changes: 3 additions & 3 deletions onecodex/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
import warnings
import errno

from potion_client import Client as PotionClient
from potion_client.converter import PotionJSONSchemaDecoder, PotionJSONDecoder, PotionJSONEncoder
from potion_client.utils import upper_camel_case
from requests.auth import HTTPBasicAuth

from onecodex.vendored.potion_client import Client as PotionClient
from onecodex.vendored.potion_client.converter import PotionJSONSchemaDecoder, PotionJSONDecoder, PotionJSONEncoder
from onecodex.vendored.potion_client.utils import upper_camel_case
from onecodex.lib.auth import BearerTokenAuth
from onecodex.models import _model_lookup
from onecodex.utils import ModuleAlias, get_raven_client, collapse_user
Expand Down
7 changes: 1 addition & 6 deletions onecodex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@

from onecodex.utils import (cli_resource_fetcher, download_file_helper,
valid_api_key, OPTION_HELP, pprint, pretty_errors,
warn_if_insecure_platform, is_simplejson_installed,
warn_simplejson, telemetry)
warn_if_insecure_platform, telemetry)
from onecodex.api import Api
from onecodex.exceptions import (ValidationWarning,
ValidationError, UploadException)
Expand Down Expand Up @@ -63,10 +62,6 @@ def onecodex(ctx, api_key, no_pprint, verbose, telemetry):
if verbose:
log.setLevel(logging.INFO)

# Show a warning if simplejson is installed
if is_simplejson_installed():
warn_simplejson()

# handle checking insecure platform, we let upload command do it by itself
if ctx.invoked_subcommand != "upload":
warn_if_insecure_platform()
Expand Down
4 changes: 2 additions & 2 deletions onecodex/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from dateutil.parser import parse
import pytz
from requests.exceptions import HTTPError
from potion_client.converter import PotionJSONEncoder
from potion_client.resource import Resource
import six

from onecodex.vendored.potion_client.converter import PotionJSONEncoder
from onecodex.vendored.potion_client.resource import Resource
from onecodex.exceptions import MethodNotSupported, PermissionDenied, ServerError
from onecodex.models.helpers import (check_bind, generate_potion_sort_clause,
generate_potion_keyword_where)
Expand Down
29 changes: 1 addition & 28 deletions onecodex/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@

import requests
from click import BadParameter, Context, echo
from potion_client.converter import PotionJSONEncoder

from onecodex.vendored.potion_client.converter import PotionJSONEncoder
from onecodex.exceptions import OneCodexException, UploadException
from onecodex.version import __version__

Expand Down Expand Up @@ -187,33 +187,6 @@ def warn_if_insecure_platform():
return False


def is_simplejson_installed():
try:
import simplejson # noqa
except ImportError:
return False
else:
return True


def warn_simplejson():
"""
Right now, potion-client is incompatible with requests when simplejson is
installed. Until this is patched upstream, we display this warning to
users when simplejson is installed.
"""
m = ("\n"
"######################################################################################\n" # noqa
"# #\n" # noqa
"# You currently have simplejson installed. Unfortunately, this library does not #\n" # noqa
"# work properly alongside simplejson. Please install this library in a separate #\n" # noqa
"# virtual environment using a tool such as virtualenv or uninstall simplejson. #\n" # noqa
"# For more information, see https://virtualenv.pypa.io/en/stable/. #\n" # noqa
"# #\n" # noqa
"######################################################################################\n") # noqa)
echo(m, err=True)


def get_download_dest(input_path, url):
original_filename = urlparse(url).path.split("/")[-1]
if os.path.isdir(input_path):
Expand Down
5 changes: 5 additions & 0 deletions onecodex/vendored/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
Modules created by others. Versioned and distributed by One Codex
"""

__all__ = ['potion_client']
18 changes: 18 additions & 0 deletions onecodex/vendored/potion_client/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Copyright (c) 2016 The Novo Nordisk Foundation Center for Biosustainability

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
86 changes: 86 additions & 0 deletions onecodex/vendored/potion_client/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@

=============
Potion client
=============


.. image:: https://img.shields.io/travis/biosustain/potion-client/new-potion-client.svg?style=flat-square
:target: https://travis-ci.org/biosustain/potion-client

.. image:: https://img.shields.io/coveralls/biosustain/potion-client/new-potion-client.svg?style=flat-square
:target: https://coveralls.io/r/biosustain/potion-client

.. image:: https://img.shields.io/pypi/v/Potion-Client.svg?style=flat-square
:target: https://pypi.python.org/pypi/Potion-Client

.. image:: https://img.shields.io/pypi/l/Potion-Client.svg?style=flat-square
:target: https://pypi.python.org/pypi/Potion-Client

.. image:: https://badges.gitter.im/Join%20Chat.svg
:alt: Join the chat at https://gitter.im/biosustain/potion
:target: https://gitter.im/biosustain/potion?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge

Description
===========

This is a Python client for APIs written in `Flask-Potion <https://github.com/biosustain/potion>`_ (a powerful Flask extension for self-documenting JSON APIs).

The package uses `Requests <https://github.com/kennethreitz/requests>`_ to provide a super-simple interface to Potion APIs that
works with all common authentication methods. It generates classes for each of the resources in the API and automatically handles pagination
and resolving and serializing references. It also has some basic `IPython Notebook <http://ipython.org/notebook.html>`_ support.

Example
=======

.. code-block:: python
from potion_client import Client
from potion_client.auth import HTTPBearerAuth
from potion_client.exceptions import ItemNotFound
client = Client('http://localhost/api', auth=HTTPBearerAuth('79054025255fb1a26e4bc422aef54eb4'))
u123 = client.User(123)
chomp = client.Animal()
chomp.owner = u123
chomp.name = "Chomp"
chomp.species = "hamster"
chomp.save()
pets = client.Animal.instances(where={"owner": u123}, sort={"created_at": True})
print("{} has {} pet(s)".format(u123.first_name, len(pets))
for pet in pets:
if pet is not chomp:
pet.add_friend(chomp)
print("{} is now friends with Chomp".format(pet.name)))
try:
foo = client.User.first(where={"username": "foo"})
except ItemNotFound:
print("User 'foo' does not exist!")
else:
chomp.update(owner=foo)
print("Chomp has been sold to {}".format(foo.name))
chomp.destroy()
print("RIP, Chomp. You lived a happy life.")
Installation
============

To install ``potion-client``, run:

::

pip install potion-client




Authors
=======

Potion-client was written by `João Cardoso <https://github.com/joaocardoso>`_ and `Lars Schöning <https://github.com/lyschoening>`_.
148 changes: 148 additions & 0 deletions onecodex/vendored/potion_client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# flake8: noqa
from functools import partial
from operator import getitem, delitem, setitem
from six.moves.urllib.parse import urlparse, urljoin
from weakref import WeakValueDictionary
import collections
import requests

from .converter import PotionJSONDecoder, PotionJSONSchemaDecoder
from .resource import Reference, Resource, uri_for
from .links import Link
from .utils import upper_camel_case, snake_case


class Client(object):
# TODO optional HTTP/2 support: this makes multiple queries simultaneously.

def __init__(self, api_root_url, schema_path='/schema', fetch_schema=True, **session_kwargs):
self._instances = WeakValueDictionary()
self._resources = {}

self.session = session = requests.Session()
for key, value in session_kwargs.items():
setattr(session, key, value)

parse_result = urlparse(api_root_url)
self._root_url = '{}://{}'.format(parse_result.scheme, parse_result.netloc)
self._api_root_url = api_root_url # '{}://{}'.format(parse_result.scheme, parse_result.netloc)
self._root_path = parse_result.path
self._schema_url = api_root_url + schema_path

if fetch_schema:
self._fetch_schema()

def _fetch_schema(self):
schema = self.session \
.get(self._schema_url) \
.json(cls=PotionJSONSchemaDecoder,
referrer=self._schema_url,
client=self)

# NOTE these should perhaps be definitions in Flask-Potion
for name, resource_schema in schema['properties'].items():
resource = self.resource_factory(name, resource_schema)
setattr(self, upper_camel_case(name), resource)

def instance(self, uri, cls=None, default=None, **kwargs):
instance = self._instances.get(uri, None)

if instance is None:
if cls is None:
try:
cls = self._resources[uri[:uri.rfind('/')]]
except KeyError:
cls = Reference

if isinstance(default, Resource) and default._uri is None:
default._status = 200
default._uri = uri
instance = default
else:
instance = cls(uri=uri, **kwargs)
self._instances[uri] = instance
return instance

def fetch(self, uri, cls=PotionJSONDecoder, **kwargs):
# TODO handle URL fragments (#properties/id etc.)
response = self.session \
.get(urljoin(self._root_url, uri, True))

response.raise_for_status()

return response.json(cls=cls,
client=self,
referrer=uri,
**kwargs)

def resource_factory(self, name, schema, resource_cls=None):
"""
Registers a new resource with a given schema. The schema must not have any unresolved references
(such as `{"$ref": "#"}` for self-references, or otherwise). A subclass of :class:`Resource`
may be provided to add specific functionality to the resulting :class:`Resource`.
:param str name:
:param dict schema:
:param Resource resource_cls: a subclass of :class:`Resource` or None
:return: The new :class:`Resource`.
"""
cls = type(str(upper_camel_case(name)), (resource_cls or Resource, collections.MutableMapping), {
'__doc__': schema.get('description', '')
})

cls._schema = schema
cls._client = self
cls._links = links = {}

for link_schema in schema['links']:
link = Link(self,
rel=link_schema['rel'],
href=link_schema['href'],
method=link_schema['method'],
schema=link_schema.get('schema', None),
target_schema=link_schema.get('targetSchema', None))

# Set Resource._self, etc. for the special methods as they are managed by the Resource class
if link.rel in ('self', 'instances', 'create', 'update', 'destroy'):
setattr(cls, '_{}'.format(link.rel), link)
links[link.rel] = link

if link.rel != 'update': # 'update' is a special case because of MutableMapping.update()
setattr(cls, snake_case(link.rel), link)

# TODO routes (instance & non-instance)

for property_name, property_schema in schema.get('properties', {}).items():
# skip $uri and $id as these are already implemented in Resource and overriding them causes unnecessary
# fetches.
if property_name.startswith('$'):
continue

if property_schema.get('readOnly', False):
# TODO better error message. Raises AttributeError("can't set attribute")
setattr(cls,
property_name,
property(fget=partial((lambda name, obj: getitem(obj, name)), property_name),
doc=property_schema.get('description', None)))
else:
setattr(cls,
property_name,
property(fget=partial((lambda name, obj: getitem(obj, name)), property_name),
fset=partial((lambda name, obj, value: setitem(obj, name, value)), property_name),
fdel=partial((lambda name, obj: delitem(obj, name)), property_name),
doc=property_schema.get('description', None)))

root = None
if 'instances' in links:
root = cls._instances.href
elif 'self' in links:
root = cls._self.href[:cls._self.href.rfind('/')]
else:
root = self._root_path + '/' + name.replace('_', '-')

self._resources[root] = cls
return cls


ASC = ASCENDING = False
DESC = DESCENDING = True
Loading

0 comments on commit a4b9eb4

Please sign in to comment.