Skip to content

Commit

Permalink
Merge branch 'release/0.2.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
alexhayes authored and Alex Hayes committed Nov 24, 2016
2 parents bf9c336 + c0ca89a commit 5f648e9
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 86 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# Release 0.2.0 - Thursday 24 November 11:04:08 AEDT 2016

- Lots of documentation updates.
- HTTPEater.request_cls now defaults to None
- Added dynamic URL formatting.
- Use conda to build Python 3.5 in readthedocs

# Release 0.1.0 - Wednesday 23 November 00:37:41 AEDT 2016

- Initial release
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0
0.2.0
2 changes: 1 addition & 1 deletion docs/developer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Running pylint is easy and is part of the CI;

.. code-block:: pylint
pylint
pylint eater
Creating Documentation
Expand Down
183 changes: 114 additions & 69 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,67 +13,51 @@ of communicating with.

.. code-block:: python
from schematics import Model, IntegerType, StringType, BooleanType, ListType, ModelType
from schematics import Model
from schematics.types import ListType, ModelType, StringType
class Book(Model):
title = StringType(required=True, min_length=3)
class Request(Model):
"""
Represents, as a Python object, the JSON data required by the API.
"""
in_print = BooleanType()
class Response(Model):
class BookListResponse(Model):
"""
Represents, as a Python object, the JSON response returned by the API.
"""
books = ListType(ModelType(Book))
Once you've defined models that represent the APIs input/output you then create
a class that inherits ``eater.HTTPEater``. Your class must define a ``url``,
``request_cls`` and ``response_cls``.

.. code-block:: python
import eater
class BooksAPI(eater.HTTPEater):
url = 'https://example.com/books/'
request_cls = Request
response_cls = Response
class BookListAPI(eater.HTTPEater):
url = 'http://example.com/books/'
response_cls = BookListResponse
You can then consume the API;

.. code-block:: python
api = BooksAPI()
response = api(in_print=True)
api = BookListAPI()
response = api()
for book in response.books:
print(book.title)
Note that you *call* the actual instance and can pass in ``kwargs`` that are
used to create an instance of your ``Request`` model.

It's also possible to pass in an instance of your request model;

.. code-block:: python
request_model = Request({'in_print': True})
response = api(request_model=request_model)
Note that you *call* the actual instance of your API class.


Holding the API to account
--------------------------

That's right, we were concerned our API wasn't going to do what it said it
would. That would be hard to imagine for the trivial example we have above
however accidents do happen, developers are only human.
however accidents do happen, developers are only human right?!

Remember our definition of a book?

Expand All @@ -93,7 +77,7 @@ For example;
from schematics.exceptions import DataError
try:
response = api(in_print=True)
response = api()
except DataError as e:
# Oh no, our API provider didn't give us back what they said they would
# e would now contain something like:
Expand All @@ -103,99 +87,155 @@ For example;
HTTP request type
-----------------

By default ``HTTPEater`` performs a HTTP ``GET`` request, you can change this
by setting ``method`` on your API class;
By default ``HTTPEater`` performs a HTTP ``GET`` request however you can change
this by setting ``method`` on your API class;

.. code-block:: python
class BooksAPI(eater.HTTPEater):
class BookCreateAPI(eater.HTTPEater):
method = 'post'
...
Or alternatively at runtime;
Any request method supported by requests_ is supported, ie... ``get, post, put,
delete, head, options``.


Post Data
---------

You can send a JSON object over the wire by defining a ``request_cls`` on your API
class, as follows;

.. code-block:: python
class BookCreateAPI(eater.HTTPEater):
url = 'http://example.com/books/'
method = 'post'
request_cls = Book
response_cls = Book
You can then call your API as follows;

.. code-block:: python
api = BooksAPI()
api.method = 'post'
api = BookCreateAPI()
response = api(name='Awesome Book')
Any request method supported by requests_ is supported, ie... ``PUT, DELETE,
HEAD, OPTIONS``.
Which would result in the following JSON payload being sent to the server;

.. code-block:: json
{
name: "Awesome Book"
}
It's also possible to pass in an instance of your ``request_cls`` as the first
(and only) parameter.

.. code-block:: python
book = Book({'name': 'Awesome Book'})
response = api(book)
Dynamic URL
-----------

The ``url`` is just a property, thus you can define it dynamically;
The ``url`` can contain string formatting that refers the request model, like so;

.. code-block:: python
class BooksAPI(eater.HTTPEater):
class GetBookRequest(Model):
id = IntType(required=True, min_value=1)
@property
def url(self):
return 'http://path.to.awesome/'
class GetBookAPI(eater.HTTPEater):
url = 'http://path.to.awesome/{request_model.id}'
request_cls = GetBookRequest
response_cls = Book
Control Request Parameters
--------------------------
For more control you can also override the ``get_url`` method;

.. code-block:: python
class GetBookAPI(eater.HTTPEater):
url = 'http://path.to.awesome/'
request_cls = GetBookRequest
response_cls = Book
You can control the params, or any kwarg supplied to requests_ by defining a
``get_request_kwargs`` method in your class.
def get_url(self, request_model: Model) -> str:
if request_model.id < 100:
return 'http://path.to.less.awesome/'
else:
return self.url
More Control
------------

You can control any kwarg supplied to requests_ by defining a
``get_request_kwargs`` method in your API class.

For instance, if you want to `pass some parameters in the URL <http://docs.python-requests.org/en/master/user/quickstart/#passing-parameters-in-urls>`_;

.. code-block:: python
class BooksAPI(eater.HTTPEater):
class BookListAPI(eater.HTTPEater):
def get_request_kwargs(self, request_model: Request, **kwargs) -> dict:
def get_request_kwargs(self, request_model: BookListRequest, **kwargs) -> dict:
"""
Retrieve a dict of kwargs to supply to requests.
Returns a dict of kwargs to supply to requests.
"""
kwargs['params'] = {
'in_print': request_model
'in_print': request_model.in_print
}
return kwargs
However, a better way of settings ``kwargs['params']`` above would be;
However, a better way of setting ``kwargs['params']`` above would be;

.. code-block:: python
kwargs['params'] = request_model.to_primitive()
Calling ``to_primitive()`` on your model returns a dict of native python types
suitable for sending over the wire. See the `schematics docs <http://schematics.readthedocs.io/en/latest/usage/exporting.html#primitive-types>`_
for more information.


Auth, Headers & Sessions
------------------------

Under the covers HTTPEater automatically creates a requests.Session for you.
Under the covers ``HTTPEater`` automatically creates a ``requests.Session`` for
you.

When you create an instance of HTTPEater you can pass through kwargs that will
be applied to this generated session, or optionally you can pass in a session
object of your creation.
When you create an instance of your API class that inherits ``HTTPEater`` you can
pass through kwargs that will be applied to this generated session, or optionally
you can pass in a session object of your creation.

.. code-block:: python
api = BooksAPI(auth=('john', 's3cr3t'))
api = BookListAPI(auth=('john', 's3cr3t'))
Need to set a custom header?

.. code-block:: python
api = BooksAPI(headers={'EGGS': 'Sausage'})
api = BookListAPI(headers={'EGGS': 'Sausage'})
Or do something really special with your own custom session?
Or need to do something really special with your own custom session?

.. code-block:: python
session = requests.Session()
api = BooksAPI(session=session)
api = BookListAPI(session=session)
Alternatively you can override the ``create_session`` method on your ``BooksAPI``
Alternatively you can override the ``create_session`` method on your ``BookListAPI``
class;

.. code-block:: python
class BooksAPI(eater.HTTPEater):
class BookListAPI(eater.HTTPEater):
url = 'https://example.com/books/'
request_cls = Request
request_cls = BookListRequest
response_cls = Response
def create_session(self, auth: tuple=None, headers: requests.structures.CaseInsensitiveDict=None) -> requests.Session:
Expand All @@ -211,12 +251,17 @@ class;
return self.session
More Control
------------
Control everything!
-------------------

You can break into all aspects of eater's lifecycle by overriding methods on your
API class;

You can break into all aspects of eater's lifecycle, simply by overriding any
one of the methods it uses to call and parse the response from your friendly
API.
- :py:meth:`.HTTPEater.get_url` - Modify the URL
- :py:meth:`.HTTPEater.create_request_model` - Modify the creation of your ``request_model``
- :py:meth:`.HTTPEater.get_request_kwargs` - Modify the kwargs supplied to requests_
- :py:meth:`.HTTPEater.create_response_model` - Modify the creation of the ``response_model`` from the requests response.
- :py:meth:`.HTTPEater.create_session` - Modify the creation of the session.

See the :doc:`internals/reference/index` for more details.

Expand Down
2 changes: 1 addition & 1 deletion eater/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
'VersionInfo', ('major', 'minor', 'micro', 'releaselevel', 'serial'),
)

VERSION = VersionInfo(0, 1, 0, '', '')
VERSION = VersionInfo(0, 2, 0, '', '')
__version__ = '{0.major}.{0.minor}.{0.micro}{0.releaselevel}'.format(VERSION)
__author__ = 'Alex Hayes'
__contact__ = 'alex@alution.com'
Expand Down

0 comments on commit 5f648e9

Please sign in to comment.