Skip to content

Commit

Permalink
Merge pull request #44 from unmade/update-docs
Browse files Browse the repository at this point in the history
Update documentation with recent changes
  • Loading branch information
unmade authored Mar 7, 2020
2 parents c99bbfb + fb76ab4 commit dd1a5a8
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 96 deletions.
65 changes: 49 additions & 16 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ Features
========

- **DRY** - support both regular and async code with one implementation
- **Customizable** - middleware mechanism to customize request/response
- **Flexible** - middleware mechanism to customize request/response
- **Typed** - library is fully typed and it's relatively easy
to get fully typed wrapper
to get fully typed wrappers
- **Modern** - decode JSON with no effort using dataclasses and type annotations
- **Unified interface** - work with different python HTTP client libraries
in the same way. Currently supported:

Expand All @@ -62,15 +63,39 @@ Installation
*Note: extras are optional and mainly needed for the final
user of your future API wrapper*

Getting Started
===============
QuickStart
==========

With *apiwrappers* you can bootstrap clients for different API
pretty fast and easily.
Making request is rather straightforward:

.. code-block:: python
from dataclasses import dataclass
from typing import List
from apiwrappers import Request, fetch, make_driver
@dataclass
class Repo:
name: str
url = "https://api.github.com/users/unmade/repos"
request = Request("GET", url)
driver = make_driver("requests")
fetch(driver, request) # Response(..., status_code=200, ...)
fetch(driver, request, model=List[Repo]) # [Repo(name='am-date-picker'), ...]
driver = make_driver("aiohttp")
await fetch(driver, request) # Response(..., status_code=200, ...)
await fetch(driver, request, model=List[Repo]) # [Repo(name='am-date-picker'), ...]
Writing a Simple API Client
---------------------------

With *apiwrappers* you can bootstrap clients for different API
pretty fast and easily.

Here is how a typical API client would look like:

.. code-block:: python
Expand All @@ -91,21 +116,17 @@ Here is how a typical API client would look like:
name: str
class Github(Generic[T]):
class GitHub(Generic[T]):
def __init__(self, host: str, driver: T):
self.url = Url(host)
self.driver: T = driver
@overload
def get_repos(
self: Github[Driver], username: str
) -> List[Repo]:
def get_repos(self: Github[Driver], username: str) -> List[Repo]:
...
@overload
def get_repos(
self: Github[AsyncDriver], username: str
) -> Awaitable[List[Repo]]:
def get_repos(self: Github[AsyncDriver], username: str) -> Awaitable[List[Repo]]:
...
def get_repos(self, username: str):
Expand All @@ -121,6 +142,16 @@ Here we defined ``Repo`` dataclass that describes what we want
to get from response and pass it to the ``fetch`` function.
``fetch`` will then make a request and will cast response to that type.

Note how we create URL:

.. code-block:: python
url = self.url("/users/{username}/repos", username=username)
Sometimes, it's useful to have an URL template, for example, for logging
or for aggregating metrics, so instead of formatting immediately, we
provide a template and replacement fields.

Using the API Client
--------------------

Expand All @@ -130,7 +161,7 @@ Here how we can use it:
>>> from apiwrappers import make_driver
>>> driver = make_driver("requests")
>>> github = Github("https://api.github.com", driver=driver)
>>> github = GitHub("https://api.github.com", driver=driver)
>>> github.get_repos("unmade")
[Repo(id=47463599, name='am-date-picker'),
Repo(id=231653904, name='apiwrappers'),
Expand All @@ -148,7 +179,7 @@ to try this code interactively*
>>> from apiwrappers import make_driver
>>> driver = make_driver("aiohttp")
>>> github = Github("https://api.github.com", driver=driver)
>>> github = GitHub("https://api.github.com", driver=driver)
>>> await github.get_repos("unmade")
[Repo(id=47463599, name='am-date-picker'),
Repo(id=231653904, name='apiwrappers'),
Expand All @@ -162,6 +193,8 @@ Documentation
Documentation for *apiwrappers* can be found at
`Read The Docs <https://apiwrappers.readthedocs.io/>`_.

Check out `Extended Client Example <example/README.md>`_.

Contributing
============

Expand All @@ -170,6 +203,6 @@ little bit helps, and credit will always be given.

See `contributing guide <CONTRIBUTING.rst>`_ to learn more.

Currently the code and the issues are hosted on Github.
Currently the code and the issues are hosted on GitHub.

The project is licensed under MIT.
36 changes: 25 additions & 11 deletions docs/building-an-api-client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ Making a Request
Each wrapper needs a HTTP client to make requests to the API.

You can easily use one of the :doc:`drivers <drivers>` to make requests, but
``driver.fetch`` call returns a ``Response`` object, which is not
always suitable for building good API clients.
:py:meth:`Driver.fetch() <apiwrappers.Driver.fetch>` call returns a
:py:class:`Response <apiwrappers.Response>` object, which is not always
suitable for building good API clients.

For API client it can be better to return typed data,
such as dataclasses, than let the final user deal with json.

*apiwrappers* provides a ``fetch`` function,
*apiwrappers* provides a :py:func:`fetch() <apiwrappers.fetch>` function,
which takes driver as a first argument, and all other
arguments are the same as with ``driver.fetch``.
arguments are the same as with
:py:meth:`Driver.fetch() <apiwrappers.Driver.fetch>`.
Giving that, it behaves exactly like if you are working with driver:

.. code-block:: python
Expand All @@ -35,8 +37,8 @@ You can also provide two additional arguments:
- ``source`` - optional key name in the json, which value will be passed
to the ``model``. You may use dotted notation to traverse keys - ``key1.key2``

With these arguments, ``fetch`` function acts like a factory,
returning new instance of the type provided to the ``model`` argument:
With these arguments, :py:func:`fetch() <apiwrappers.fetch>` function acts like
a factory, returning new instance of the type provided to the ``model`` argument:

.. code-block:: python
Expand Down Expand Up @@ -99,6 +101,16 @@ or a coroutine - ``Awaitable[List[Repo]]``
*You never want to await the fetch call here,
just return it immediately and let the final user await it if needed*

Another thing to notice is how we create URL:

.. code-block:: python
url = self.url("/users/{username}/repos", username=username)
Sometimes, it's useful to have an URL template, for example, for logging
or for aggregating metrics, so instead of formatting immediately, we
provide a template and replacement fields.

The wrapper above is good enough to satisfy most cases,
however it lacks one of the important features nowadays - type annotations.

Expand All @@ -115,12 +127,13 @@ We can simply specify return type as:
Union[List[Repo], Awaitable[List[Repo]]
and that will be enough to have a good auto-completion,
but what we want here to do is a little bit trickier.
but what we want precise type annotations.
We want to tell mypy,
that when driver corresponds to ``Driver`` protocol
We want to tell mypy, that when driver corresponds to
:py:class:`Driver <apiwrappers.Driver>` protocol
``.get_repos`` has return type ``List[Repo]``
and for ``AsyncDriver`` protocol - ``Awaitable[List[Repo]]``.
and for :py:class:`AsyncDriver <apiwrappers.AsyncDriver>` protocol -
``Awaitable[List[Repo]]``.
It can be done like that:
Expand Down Expand Up @@ -165,7 +178,8 @@ It can be done like that:
return fetch(self.driver, request, model=List[Repo])
Here, we defined a ``T`` type variable, constrained to
``Driver`` and ``AsyncDriver`` protocols.
:py:class:`Driver <apiwrappers.Driver>`
and :py:class:`AsyncDriver <apiwrappers.AsyncDriver>` protocols.
Our wrapper is now a generic class of that variable.
We also used :py:func:`overload <typing.overload>` with self-type to define return type based on
the driver provided to our wrapper.
Expand Down
64 changes: 14 additions & 50 deletions docs/drivers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ Out of the box *apiwrappers* provides drivers for
`aiohttp <https://docs.aiohttp.org/en/stable/client.html>`_
libraries.

You can create them with a ``make_driver`` factory.
Let's learn how to make a simple request using a driver
You can create them with a :py:func:`make_driver() <apiwrappers.make_driver>`
factory. Let's learn how to make a simple request using a driver
for `requests <https://requests.readthedocs.io/en/master/>`_ library:

.. code-block:: python
Expand Down Expand Up @@ -57,48 +57,10 @@ follows, rather than what library it uses underneath.
Driver protocols
================

All drivers should follow either `Driver` or `AsyncDriver` protocols,
depending on which HTTP client is used.
Protocols also help to abstract away from concrete driver implementations
and ease type checking and annotation.

Driver
------

To be compliant with ``Driver`` protocol
driver should have this fields and methods:

.. code-block:: python
timeout: Timeout
verify: bool
middleware: List[Type[Middleware]]
def fetch(
self,
request: Request,
timeout: Union[Timeout, NoValue] = NoValue(),
) -> Response:
...
AsyncDriver
-----------

To be compliant with ``AsyncDriver`` protocol
driver should have this fields and methods:

.. code-block:: python
timeout: Timeout
verify: bool
middleware: List[Type[AsyncMiddleware]]
async def fetch(
self,
request: Request,
timeout: Union[Timeout, NoValue] = NoValue(),
) -> Response:
...
All drivers should follow either :py:class:`Driver <apiwrappers.Driver>`
or :py:class:`AsyncDriver <apiwrappers.AsyncDriver>` protocols, depending on
which HTTP client is used.Protocols also help to abstract away from concrete
driver implementations and ease type checking and annotation.

Timeouts
========
Expand Down Expand Up @@ -130,8 +92,9 @@ In case timeout value is exceeded ``Timeout`` error will be raised
SSL Verification
================

You can enable/disable SSL verification when creating a driver or
when making a request. The later will take precedences over driver settings.
You can enable/disable SSL verification or provide custom SSL certs
when creating a driver. Note that default trusted CAs depends on a
driver you're using.

By default SSL verification is enabled.

Expand All @@ -144,11 +107,12 @@ Here is how you can change it:
# disable SSL verification
driver = make_driver("requests", verify=False)
# making a request without SSL verification
driver.fetch(request)
# custom SSL with trusted CAs
driver = make_driver("requests", verify="/path/to/ca-bundle.crt")
# making a request with SSL verification
driver.fetch(request)
# custom Client Side Certificates
certs = ('/path/to/client.cert', '/path/to/client.key')
driver = make_driver("requests", cert=certs)
Writing your own driver
=======================
Expand Down
10 changes: 6 additions & 4 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ Features
========

- **DRY** - support both regular and async code with one implementation
- **Customizable** - middleware mechanism to customize request/response
- **Flexible** - middleware mechanism to customize request/response
- **Typed** - library is fully typed and it's relatively easy
to get fully typed wrapper
to get fully typed wrappers
- **Modern** - decode JSON with no effort using dataclasses and type annotations
- **Unified interface** - work with different python HTTP client libraries
in the same way. Currently supported:

Expand Down Expand Up @@ -86,8 +87,9 @@ This is small, but fully typed, API client for one of the
by username:

Here we defined ``Repo`` dataclass that describes what we want
to get from response and pass it to the ``fetch`` function.
``fetch`` will then make a request and will cast response to that type.
to get from response and pass it to the :py:func:`fetch() <apiwrappers.fetch>`
function. :py:func:`fetch() <apiwrappers.fetch>` will then make a request and
cast response to that type.

And here how we can use it:

Expand Down
23 changes: 12 additions & 11 deletions docs/middleware.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Writing your own middleware

A middleware factory is a callable that takes a callable and returns
a middleware. A middleware is a callable that takes same argument as
``driver.fetch`` and returns a response.
:py:class:`Driver.fetch() <apiwrappers.Driver.fetch>` and returns a response.

The most simple way is to write a middleware as function:

Expand Down Expand Up @@ -93,31 +93,32 @@ it still can be used like that:

.. code-block:: python
from apiwrappers import make_driver
driver = make_driver("requests", SimpleMiddleware)
# RequestsDriver(Authorization, SimpleMiddleware, ...)
>>> from apiwrappers import make_driver
>>> driver = make_driver("requests", SimpleMiddleware)
>>> driver
# RequestsDriver(Authorization, SimpleMiddleware, ...
*Note, that even we provide only ``SimpleMiddleware`` the driver also has
``Authorization`` middleware. That's because some drivers have middleware
that should always be present.*

You can also change driver middleware after creation by simply reassigning
``driver.middleware`` attribute:
:py:attr:`Driver.middleware <apiwrappers.Driver.middleware>` attribute:

.. code-block:: python
driver.middleware = []
# RequestsDriver(Authorization, ...)
>>> driver.middleware = []
>>> driver
# RequestsDriver(Authorization, ...
The order of the default middleware can be overridden by explicitly
specifying it:

.. code-block:: python
driver.middleware = [SimpleMiddleware, Authorization]
# RequestsDriver(SimpleMiddleware, Authorization, ...)
>>> driver.middleware = [SimpleMiddleware, Authorization]
>>> driver
# RequestsDriver(SimpleMiddleware, Authorization, ...
Middleware order
================
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "apiwrappers"
version = "0.1.0-beta.2"
version = "0.1.0"
description = "apiwrappers is a library for building API clients that work both with regular and async code"
keywords = ["api", "wrapper", "http", "client"]
readme = "README.rst"
Expand Down
Loading

0 comments on commit dd1a5a8

Please sign in to comment.