Skip to content

Commit

Permalink
Cuenca python (#1)
Browse files Browse the repository at this point in the history
* Name in transfers

* Unit test

* more test and cassettes

* reciever -> recipient

* Travis + coveralls

* coveralls in setup

* Lint

* count and all

* Increase coverage

* Fix Test

* Fix Test

* full .travis.yml

* fix

* github action

* ci and cd

* remove travis

* implicitly return None

* full typing for Generator

* api should _always_ return the network

* configure once

* Fix Tests

* Fix Tests

* up coverage

* codecov

* fix

* Remove ApiKey.roll and modify test

* fix lint

* CD on release event

* CD action

* Minor changes

* Format CD

* Enocde enum values in query url

* Coverage

* better way to pre-process for Queryable.all()

* fix lint

* More transfer tests

* Remove Id in tests

* Add Query validators

* change version

* more descriptive exceptions

* most style changes and cleaning up a few items

* these file names make more sense

* useful comment

* update Makefile and add mypy

* more reasonable names

* resolve method signature issue and improve docstring

* cleaner

* block mypy for now. will add it back later

Co-authored-by: Felipe López <[email protected]>
Co-authored-by: Matin Tamizi <[email protected]>
  • Loading branch information
3 people authored May 22, 2020
1 parent 04689e1 commit 1381e45
Show file tree
Hide file tree
Showing 39 changed files with 2,033 additions and 117 deletions.
9 changes: 9 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
codecov:
require_ci_to_pass: yes

coverage:
precision: 2
range: [95, 100]

comment:
layout: 'header, diff, flags, files, footer'
22 changes: 22 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: release

on: release

jobs:
publish-pypi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
run: pip install -qU setuptools wheel twine
- name: Generating distribution archives
run: python setup.py sdist bdist_wheel
- name: Publish distribution 📦 to PyPI
if: startsWith(github.event.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@master
with:
password: ${{ secrets.pypi_password }}
26 changes: 23 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: test

on: [push, pull_request]
on: push

jobs:
lint:
Expand All @@ -10,7 +10,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: '3.8'
python-version: 3.8
- name: Install dependencies
run: make install-test
- name: Lint
Expand All @@ -30,4 +30,24 @@ jobs:
- name: Install dependencies
run: make install-test
- name: Run tests
run: pytest --vcr-record=none
run: pytest

coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Setup Python
uses: actions/setup-python@master
with:
python-version: 3.8
- name: Install dependencies
run: make install-test
- name: Generate coverage report
run: pytest --cov-report=xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: true
48 changes: 29 additions & 19 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,41 +1,51 @@
SHELL := bash
PATH := ./venv/bin:${PATH}
PYTHON=python3.7
PROJECT=cuenca
PYTHON = python3.7
PROJECT = cuenca
isort = isort -rc -ac $(PROJECT) tests setup.py
black = black -S -l 79 --target-version py37 $(PROJECT) tests setup.py
black = black -S -l 79 --target-version py38 $(PROJECT) tests setup.py


all: test

venv:
$(PYTHON) -m venv --prompt $(PROJECT) venv
pip install -qU pip
$(PYTHON) -m venv --prompt $(PROJECT) venv
pip install -qU pip

install-test:
pip install -q .[test]
pip install -q .[test]

test: clean install-test lint
python setup.py test
python setup.py test

format:
$(isort)
$(black)
$(isort)
$(black)

lint:
flake8 $(PROJECT) tests setup.py
$(isort) --check-only
$(black) --check
flake8 $(PROJECT) tests setup.py
$(isort) --check-only
$(black) --check
# mypy $(PROJECT) tests

clean:
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
rm -rf build dist $(PROJECT).egg-info
rm -rf `find . -name __pycache__`
rm -f `find . -type f -name '*.py[co]' `
rm -f `find . -type f -name '*~' `
rm -f `find . -type f -name '.*~' `
rm -rf .cache
rm -rf .pytest_cache
rm -rf .mypy_cache
rm -rf htmlcov
rm -rf *.egg-info
rm -f .coverage
rm -f .coverage.*
rm -rf build
rm -rf dist

release: clean
python setup.py sdist bdist_wheel
twine upload dist/*
python setup.py sdist bdist_wheel
twine upload dist/*


.PHONY: all install-test test format lint clean release
.PHONY: all install-test test format lint clean release
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,15 @@ count = cuenca.Transfer.count(status=Status.succeeded)

## Api Keys

### Roll the `ApiKey`

### Create new `ApiKey` and deactivate old
```python
import cuenca

# create new key and deactive old key in 60 mins
old_key, new_key = cuenca.ApiKey.roll(60)
```
# Create new ApiKey
new = cuenca.ApiKey.create()

# Have to use the new key to deactivate the old key
old_id = cuenca.session.auth[0]
cuenca.session.configure(new.id, new.secret)
cuenca.ApiKey.deactivate(old_id, minutes)
```
4 changes: 2 additions & 2 deletions cuenca/exc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ class CuencaException(Exception):


class NoResultFound(CuencaException):
...
"""No results were found"""


class MultipleResultsFound(CuencaException):
...
"""One result was expected but multiple were returned"""
18 changes: 8 additions & 10 deletions cuenca/http/client.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import os
from typing import Any, Dict, Optional, Tuple
from typing import Optional, Tuple

import requests
from requests import Response

from ..types import OptionalDict
from ..typing import ClientRequestParams, DictStrAny, OptionalDict
from ..version import API_VERSION, CLIENT_VERSION

API_URL = 'https://api.cuenca.com'
Expand Down Expand Up @@ -52,26 +52,24 @@ def configure(
self.base_url = SANDBOX_URL

def get(
self, endpoint: str, params: OptionalDict = None
) -> Dict[str, Any]:
self, endpoint: str, params: ClientRequestParams = None,
) -> DictStrAny:
return self.request('get', endpoint, params=params)

def post(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]:
def post(self, endpoint: str, data: DictStrAny) -> DictStrAny:
return self.request('post', endpoint, data=data)

def delete(
self, endpoint: str, data: OptionalDict = None
) -> Dict[str, Any]:
def delete(self, endpoint: str, data: OptionalDict = None) -> DictStrAny:
return self.request('delete', endpoint, data=data)

def request(
self,
method: str,
endpoint: str,
params: OptionalDict = None,
params: ClientRequestParams = None,
data: OptionalDict = None,
**kwargs,
) -> Dict[str, Any]:
) -> DictStrAny:
resp = self.session.request(
method=method,
url=self.base_url + endpoint,
Expand Down
22 changes: 4 additions & 18 deletions cuenca/resources/api_keys.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import datetime as dt
from typing import ClassVar, Optional, Tuple
from typing import ClassVar, Optional

from pydantic.dataclasses import dataclass

from ..http import session
from ..validators import ApiKeyQuery
from .base import Creatable, Queryable, Retrievable


@dataclass
class ApiKey(Creatable, Queryable, Retrievable):
_endpoint: ClassVar = '/api_keys'
_query_params: ClassVar = set()
_query_params: ClassVar = ApiKeyQuery

id: str
secret: str
Expand All @@ -26,22 +27,7 @@ def active(self) -> bool:

@classmethod
def create(cls) -> 'ApiKey':
return super().create()

@classmethod
def roll(cls, minutes: int = 0) -> Tuple['ApiKey', 'ApiKey']:
"""
1. create a new ApiKey
2. configure client with new ApiKey
3. deactivate prior ApiKey in a certain number of minutes
4. return both ApiKeys
"""
old_id = session.auth[0]
new = cls.create()
# have to use the new key to deactivate the old key
session.configure(new.id, new.secret)
old = cls.deactivate(old_id, minutes)
return old, new
return cls._create()

@classmethod
def deactivate(cls, api_key_id: str, minutes: int = 0) -> 'ApiKey':
Expand Down
46 changes: 19 additions & 27 deletions cuenca/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

from ..exc import MultipleResultsFound, NoResultFound
from ..http import session
from .utils import DictFactory
from ..types import SantizedDict
from ..validators import QueryParams


@dataclass
Expand All @@ -28,7 +29,7 @@ def _filter_excess_fields(cls, obj_dict):
del obj_dict[f]

def to_dict(self):
return asdict(self, dict_factory=DictFactory)
return asdict(self, dict_factory=SantizedDict)


class Retrievable(Resource):
Expand All @@ -45,19 +46,18 @@ def refresh(self):

class Creatable(Resource):
@classmethod
def create(cls, **data) -> Resource:
def _create(cls, **data) -> Resource:
resp = session.post(cls._endpoint, data)
return cls._from_dict(resp)


class Queryable(Resource):
_query_params: ClassVar[set]
_query_params: ClassVar = QueryParams

@classmethod
def one(cls, **query_params) -> Resource:
cls._check_query_params(query_params)
query_params['limit'] = 2
resp = session.get(cls._endpoint, query_params)
q = cls._query_params(limit=2, **query_params)
resp = session.get(cls._endpoint, q.dict())
items = resp['items']
len_items = len(items)
if not len_items:
Expand All @@ -68,35 +68,27 @@ def one(cls, **query_params) -> Resource:

@classmethod
def first(cls, **query_params) -> Optional[Resource]:
cls._check_query_params(query_params)
query_params['limit'] = 1
resp = session.get(cls._endpoint, query_params)
q = cls._query_params(limit=1, **query_params)
resp = session.get(cls._endpoint, q.dict())
try:
item = resp['items'][0]
except IndexError:
item = None
return cls._from_dict(item)
rv = None
else:
rv = cls._from_dict(item)
return rv

@classmethod
def count(cls, **query_params) -> int:
cls._check_query_params(query_params)
query_params['count'] = 1
resp = session.get(cls._endpoint, query_params)
q = cls._query_params(count=True, **query_params)
resp = session.get(cls._endpoint, q.dict())
return resp['count']

@classmethod
def all(cls, **query_params) -> Generator[Resource]:
cls._check_query_params(query_params)
next_page_url = f'{cls._endpoint}?{urlencode(query_params)}'
def all(cls, **query_params) -> Generator[Resource, None, None]:
q = cls._query_params(**query_params)
next_page_url = f'{cls._endpoint}?{urlencode(q.dict())}'
while next_page_url:
page = session.get(next_page_url)
yield from (cls._from_dict(item) for item in page['items'])
next_page_url = page['next']

@classmethod
def _check_query_params(cls, query_params):
if not query_params:
return
unaccepted = set(query_params.keys()) - cls._query_params
if unaccepted:
raise ValueError(f'{unaccepted} are not accepted query parameters')
next_page_url = page['next_page_url']
Loading

0 comments on commit 1381e45

Please sign in to comment.