Skip to content

Commit

Permalink
version: 1.2.0
Browse files Browse the repository at this point in the history
- Add support for marshalling/unmarshalling to/from json -> dataclasses.
- switch to using github actions
- remove deprecated features
- Allow header authentication to pass in extra headers required for
  authenticating the client.
  • Loading branch information
MikeWooster authored and MikeWooster committed Jun 28, 2020
1 parent 9d55ed2 commit 476f233
Show file tree
Hide file tree
Showing 23 changed files with 712 additions and 248 deletions.
103 changes: 0 additions & 103 deletions .circleci/config.yml

This file was deleted.

28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Unit Tests

on: push

jobs:

test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8]

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox
- name: Lint Checks
run: |
tox -e lint
- name: Unit Tests
run: |
tox -e unittest
58 changes: 58 additions & 0 deletions .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Python test and deploy

on:
release:
types: [created]


jobs:

test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8]

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox
- name: Lint Checks
run: |
tox -e lint
- name: Unit Tests
run: |
tox -e unittest
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set Up Build
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[deploy]
- name: Update RC Version
run: |
python scripts/update_version.py ${GITHUB_REF}
- name: Build Dist
run: |
rm -rf dist/*
python setup.py sdist
- name: Publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python scripts/upload_new_package.py
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
recursive-include apiclient *
include VERSION

recursive-exclude * __pycache__
recursive-exclude * *.py[co]
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ that code away from the clean abstraction you have designed.
7. [Correctly encoding your outbound request data](#Request-Formatters)
8. [Handling bad requests and responses](#Exceptions)
9. [Endpoints as code](#Endpoints)
10. [Marshalling requests/responses](#Marshalling)

## Installation

Expand Down Expand Up @@ -248,6 +249,20 @@ authentication_method=HeaderAuthentication(
{"token": "secret_value"}
```

Additional header values can be passed in as a dict here when API's require more than one
header to authenticate:
```
authentication_method=HeaderAuthentication(
token="secret_value"
parameter="token",
scheme=None,
extra={"more": "another_secret"}
)
# Constructs request header:
{"token": "secret_value", "more": "another_secret"}
```

### `BasicAuthentication`
This authentication method enables specifying a username and password to APIs
that require such.
Expand Down Expand Up @@ -454,6 +469,38 @@ class Endpoint:
"http://foo.com/search
```

## Marshalling

The following decorators have been provided to marshal request data as python dataclasses to json
and to unmarshal json directly into a python dataclass.

```
# Marshal dataclass -> json
@marshal_request(date_fmt: Optional[str] = None, datetime_fmt: Optional[str] = None)
# Unmarshal json -> dataclass
@unmarshal_response(schema: T, date_fmt: Optional[str] = None, datetime_fmt: Optional[str] = None)
```

Usage:
1. Define the schema for your api in python dataclasses.
2. Add the `@unmarshal_response` decorator to the api client method to transform the response
directly into your defined schema.
```
@unmarshal_response(List[Account])
def get_accounts():
...
```
3. Add the `@marshal_request` decorator to the api client method to translate the incoming dataclass
into the required json for the endpoint:
```
@marshal_request()
def create_account(account: Account):
...
```

The marshalling functionality has been provided by: https://github.com/MikeWooster/jsonmarshal
More usage examples can be found there.

## Extended Example
```
Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.2.0
3 changes: 2 additions & 1 deletion apiclient/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
NoAuthentication,
QueryParameterAuthentication,
)
from apiclient.client import APIClient, BaseClient
from apiclient.client import APIClient
from apiclient.decorates import endpoint
from apiclient.json import marshal_request, unmarshal_response
from apiclient.paginators import paginated
from apiclient.request_formatters import JsonRequestFormatter
from apiclient.response_handlers import (
Expand Down
20 changes: 15 additions & 5 deletions apiclient/authentication_methods.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import http.cookiejar
from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING, Dict, Optional, Union

from apiclient.utils.typing import BasicAuthType, OptionalStr

Expand Down Expand Up @@ -47,16 +47,26 @@ class HeaderAuthentication(BaseAuthenticationMethod):
"Authorization: Bearer <token>"
"""

def __init__(self, token: str, parameter: str = "Authorization", scheme: OptionalStr = "Bearer"):
def __init__(
self,
token: str,
parameter: str = "Authorization",
scheme: OptionalStr = "Bearer",
extra: Optional[Dict[str, str]] = None,
):
self._token = token
self._parameter = parameter
self._scheme = scheme
self._extra = extra

def get_headers(self):
def get_headers(self) -> Dict[str, str]:
if self._scheme:
return {self._parameter: f"{self._scheme} {self._token}"}
headers = {self._parameter: f"{self._scheme} {self._token}"}
else:
return {self._parameter: self._token}
headers = {self._parameter: self._token}
if self._extra:
headers.update(self._extra)
return headers


class BasicAuthentication(BaseAuthenticationMethod):
Expand Down
32 changes: 0 additions & 32 deletions apiclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
from apiclient.request_strategies import BaseRequestStrategy, RequestStrategy
from apiclient.response_handlers import BaseResponseHandler, RequestsResponseHandler
from apiclient.utils.typing import OptionalDict
from apiclient.utils.warnings import deprecation_warning

LOG = logging.getLogger(__name__)

Expand Down Expand Up @@ -106,26 +105,6 @@ def clone(self):
new_client.set_session(self.get_session())
return new_client

def create(self, endpoint: str, data: dict, params: OptionalDict = None):
"""Provide backwards compatibility adaptor from create() -> post()."""
deprecation_warning("'create()' will be deprecated in version 1.2. use 'post()' instead.")
return self.post(endpoint, data=data, params=params)

def read(self, endpoint: str, params: OptionalDict = None):
"""Provide backwards compatibility adaptor from read() -> get()."""
deprecation_warning("`read()` will be deprecated in version 1.2. use `get()` instead.")
return self.get(endpoint, params=params)

def replace(self, endpoint: str, data: dict, params: OptionalDict = None):
"""Provide backwards compatibility adaptor from replace() -> put()."""
deprecation_warning("`replace()` will be deprecated in version 1.2. use `put()` instead.")
return self.put(endpoint, data=data, params=params)

def update(self, endpoint: str, data: dict, params: OptionalDict = None):
"""Provide backwards compatibility adaptor from update() -> patch()."""
deprecation_warning("`update()` will be deprecated in version 1.2. use `patch()` instead.")
return self.patch(endpoint, data=data, params=params)

def post(self, endpoint: str, data: dict, params: OptionalDict = None, **kwargs):
"""Send data and return response data from POST endpoint."""
LOG.info("POST %s with %s", endpoint, data)
Expand All @@ -150,14 +129,3 @@ def delete(self, endpoint: str, params: OptionalDict = None, **kwargs):
"""Remove resource with DELETE endpoint."""
LOG.info("DELETE %s", endpoint)
return self.get_request_strategy().delete(endpoint, params=params, **kwargs)


class BaseClient(APIClient):
"""Provide backwards compatibility for BaseClient usage until it is removed."""

def __init__(self, *args, **kwargs):
deprecation_warning(
"`BaseClient` has been deprecated in version 1.1.4 and will be removed in version 1.2.0, "
"please use `APIClient` instead."
)
super().__init__(*args, **kwargs)
31 changes: 31 additions & 0 deletions apiclient/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import Optional, TypeVar

from jsonmarshal import marshal, unmarshal

T = TypeVar("T")


def unmarshal_response(schema: T, date_fmt: Optional[str] = None, datetime_fmt: Optional[str] = None):
"""Decorator to unmarshal the response json into the provided dataclass."""

def decorator(func) -> T:
def wrap(*args, **kwargs) -> T:
response = func(*args, **kwargs)
return unmarshal(response, schema, date_fmt=date_fmt, datetime_fmt=datetime_fmt)

return wrap

return decorator


def marshal_request(date_fmt: Optional[str] = None, datetime_fmt: Optional[str] = None):
"""Decorator to marshal the request from a dataclass into valid json."""

def decorator(func) -> T:
def wrap(endpoint: str, data: T, *args, **kwargs):
marshalled = marshal(data, date_fmt=date_fmt, datetime_fmt=datetime_fmt)
return func(endpoint, marshalled, *args, **kwargs)

return wrap

return decorator
Loading

0 comments on commit 476f233

Please sign in to comment.