Skip to content

Commit 1381e45

Browse files
rogelioLpzfelipao-mxmatin
authored
Cuenca python (#1)
* 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 <flh.1989@gmail.com> Co-authored-by: Matin Tamizi <matin@cuenca.com>
1 parent 04689e1 commit 1381e45

39 files changed

Lines changed: 2033 additions & 117 deletions

.codecov.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
codecov:
2+
require_ci_to_pass: yes
3+
4+
coverage:
5+
precision: 2
6+
range: [95, 100]
7+
8+
comment:
9+
layout: 'header, diff, flags, files, footer'

.github/workflows/release.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: release
2+
3+
on: release
4+
5+
jobs:
6+
publish-pypi:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@master
10+
- name: Set up Python 3.8
11+
uses: actions/setup-python@v1
12+
with:
13+
python-version: 3.8
14+
- name: Install dependencies
15+
run: pip install -qU setuptools wheel twine
16+
- name: Generating distribution archives
17+
run: python setup.py sdist bdist_wheel
18+
- name: Publish distribution 📦 to PyPI
19+
if: startsWith(github.event.ref, 'refs/tags')
20+
uses: pypa/gh-action-pypi-publish@master
21+
with:
22+
password: ${{ secrets.pypi_password }}

.github/workflows/test.yml

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: test
22

3-
on: [push, pull_request]
3+
on: push
44

55
jobs:
66
lint:
@@ -10,7 +10,7 @@ jobs:
1010
- name: Set up Python
1111
uses: actions/setup-python@v1
1212
with:
13-
python-version: '3.8'
13+
python-version: 3.8
1414
- name: Install dependencies
1515
run: make install-test
1616
- name: Lint
@@ -30,4 +30,24 @@ jobs:
3030
- name: Install dependencies
3131
run: make install-test
3232
- name: Run tests
33-
run: pytest --vcr-record=none
33+
run: pytest
34+
35+
coverage:
36+
runs-on: ubuntu-latest
37+
steps:
38+
- uses: actions/checkout@master
39+
- name: Setup Python
40+
uses: actions/setup-python@master
41+
with:
42+
python-version: 3.8
43+
- name: Install dependencies
44+
run: make install-test
45+
- name: Generate coverage report
46+
run: pytest --cov-report=xml
47+
- name: Upload coverage to Codecov
48+
uses: codecov/codecov-action@v1
49+
with:
50+
file: ./coverage.xml
51+
flags: unittests
52+
name: codecov-umbrella
53+
fail_ci_if_error: true

Makefile

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,51 @@
11
SHELL := bash
22
PATH := ./venv/bin:${PATH}
3-
PYTHON=python3.7
4-
PROJECT=cuenca
3+
PYTHON = python3.7
4+
PROJECT = cuenca
55
isort = isort -rc -ac $(PROJECT) tests setup.py
6-
black = black -S -l 79 --target-version py37 $(PROJECT) tests setup.py
6+
black = black -S -l 79 --target-version py38 $(PROJECT) tests setup.py
77

88

99
all: test
1010

1111
venv:
12-
$(PYTHON) -m venv --prompt $(PROJECT) venv
13-
pip install -qU pip
12+
$(PYTHON) -m venv --prompt $(PROJECT) venv
13+
pip install -qU pip
1414

1515
install-test:
16-
pip install -q .[test]
16+
pip install -q .[test]
1717

1818
test: clean install-test lint
19-
python setup.py test
19+
python setup.py test
2020

2121
format:
22-
$(isort)
23-
$(black)
22+
$(isort)
23+
$(black)
2424

2525
lint:
26-
flake8 $(PROJECT) tests setup.py
27-
$(isort) --check-only
28-
$(black) --check
26+
flake8 $(PROJECT) tests setup.py
27+
$(isort) --check-only
28+
$(black) --check
29+
# mypy $(PROJECT) tests
2930

3031
clean:
31-
find . -name '*.pyc' -exec rm -f {} +
32-
find . -name '*.pyo' -exec rm -f {} +
33-
find . -name '*~' -exec rm -f {} +
34-
rm -rf build dist $(PROJECT).egg-info
32+
rm -rf `find . -name __pycache__`
33+
rm -f `find . -type f -name '*.py[co]' `
34+
rm -f `find . -type f -name '*~' `
35+
rm -f `find . -type f -name '.*~' `
36+
rm -rf .cache
37+
rm -rf .pytest_cache
38+
rm -rf .mypy_cache
39+
rm -rf htmlcov
40+
rm -rf *.egg-info
41+
rm -f .coverage
42+
rm -f .coverage.*
43+
rm -rf build
44+
rm -rf dist
3545

3646
release: clean
37-
python setup.py sdist bdist_wheel
38-
twine upload dist/*
47+
python setup.py sdist bdist_wheel
48+
twine upload dist/*
3949

4050

41-
.PHONY: all install-test test format lint clean release
51+
.PHONY: all install-test test format lint clean release

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,15 @@ count = cuenca.Transfer.count(status=Status.succeeded)
6161

6262
## Api Keys
6363

64-
### Roll the `ApiKey`
65-
64+
### Create new `ApiKey` and deactivate old
6665
```python
6766
import cuenca
6867

69-
# create new key and deactive old key in 60 mins
70-
old_key, new_key = cuenca.ApiKey.roll(60)
71-
```
68+
# Create new ApiKey
69+
new = cuenca.ApiKey.create()
70+
71+
# Have to use the new key to deactivate the old key
72+
old_id = cuenca.session.auth[0]
73+
cuenca.session.configure(new.id, new.secret)
74+
cuenca.ApiKey.deactivate(old_id, minutes)
75+
```

cuenca/exc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ class CuencaException(Exception):
33

44

55
class NoResultFound(CuencaException):
6-
...
6+
"""No results were found"""
77

88

99
class MultipleResultsFound(CuencaException):
10-
...
10+
"""One result was expected but multiple were returned"""

cuenca/http/client.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import os
2-
from typing import Any, Dict, Optional, Tuple
2+
from typing import Optional, Tuple
33

44
import requests
55
from requests import Response
66

7-
from ..types import OptionalDict
7+
from ..typing import ClientRequestParams, DictStrAny, OptionalDict
88
from ..version import API_VERSION, CLIENT_VERSION
99

1010
API_URL = 'https://api.cuenca.com'
@@ -52,26 +52,24 @@ def configure(
5252
self.base_url = SANDBOX_URL
5353

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

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

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

6765
def request(
6866
self,
6967
method: str,
7068
endpoint: str,
71-
params: OptionalDict = None,
69+
params: ClientRequestParams = None,
7270
data: OptionalDict = None,
7371
**kwargs,
74-
) -> Dict[str, Any]:
72+
) -> DictStrAny:
7573
resp = self.session.request(
7674
method=method,
7775
url=self.base_url + endpoint,

cuenca/resources/api_keys.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import datetime as dt
2-
from typing import ClassVar, Optional, Tuple
2+
from typing import ClassVar, Optional
33

44
from pydantic.dataclasses import dataclass
55

66
from ..http import session
7+
from ..validators import ApiKeyQuery
78
from .base import Creatable, Queryable, Retrievable
89

910

1011
@dataclass
1112
class ApiKey(Creatable, Queryable, Retrievable):
1213
_endpoint: ClassVar = '/api_keys'
13-
_query_params: ClassVar = set()
14+
_query_params: ClassVar = ApiKeyQuery
1415

1516
id: str
1617
secret: str
@@ -26,22 +27,7 @@ def active(self) -> bool:
2627

2728
@classmethod
2829
def create(cls) -> 'ApiKey':
29-
return super().create()
30-
31-
@classmethod
32-
def roll(cls, minutes: int = 0) -> Tuple['ApiKey', 'ApiKey']:
33-
"""
34-
1. create a new ApiKey
35-
2. configure client with new ApiKey
36-
3. deactivate prior ApiKey in a certain number of minutes
37-
4. return both ApiKeys
38-
"""
39-
old_id = session.auth[0]
40-
new = cls.create()
41-
# have to use the new key to deactivate the old key
42-
session.configure(new.id, new.secret)
43-
old = cls.deactivate(old_id, minutes)
44-
return old, new
30+
return cls._create()
4531

4632
@classmethod
4733
def deactivate(cls, api_key_id: str, minutes: int = 0) -> 'ApiKey':

cuenca/resources/base.py

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
from ..exc import MultipleResultsFound, NoResultFound
66
from ..http import session
7-
from .utils import DictFactory
7+
from ..types import SantizedDict
8+
from ..validators import QueryParams
89

910

1011
@dataclass
@@ -28,7 +29,7 @@ def _filter_excess_fields(cls, obj_dict):
2829
del obj_dict[f]
2930

3031
def to_dict(self):
31-
return asdict(self, dict_factory=DictFactory)
32+
return asdict(self, dict_factory=SantizedDict)
3233

3334

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

4647
class Creatable(Resource):
4748
@classmethod
48-
def create(cls, **data) -> Resource:
49+
def _create(cls, **data) -> Resource:
4950
resp = session.post(cls._endpoint, data)
5051
return cls._from_dict(resp)
5152

5253

5354
class Queryable(Resource):
54-
_query_params: ClassVar[set]
55+
_query_params: ClassVar = QueryParams
5556

5657
@classmethod
5758
def one(cls, **query_params) -> Resource:
58-
cls._check_query_params(query_params)
59-
query_params['limit'] = 2
60-
resp = session.get(cls._endpoint, query_params)
59+
q = cls._query_params(limit=2, **query_params)
60+
resp = session.get(cls._endpoint, q.dict())
6161
items = resp['items']
6262
len_items = len(items)
6363
if not len_items:
@@ -68,35 +68,27 @@ def one(cls, **query_params) -> Resource:
6868

6969
@classmethod
7070
def first(cls, **query_params) -> Optional[Resource]:
71-
cls._check_query_params(query_params)
72-
query_params['limit'] = 1
73-
resp = session.get(cls._endpoint, query_params)
71+
q = cls._query_params(limit=1, **query_params)
72+
resp = session.get(cls._endpoint, q.dict())
7473
try:
7574
item = resp['items'][0]
7675
except IndexError:
77-
item = None
78-
return cls._from_dict(item)
76+
rv = None
77+
else:
78+
rv = cls._from_dict(item)
79+
return rv
7980

8081
@classmethod
8182
def count(cls, **query_params) -> int:
82-
cls._check_query_params(query_params)
83-
query_params['count'] = 1
84-
resp = session.get(cls._endpoint, query_params)
83+
q = cls._query_params(count=True, **query_params)
84+
resp = session.get(cls._endpoint, q.dict())
8585
return resp['count']
8686

8787
@classmethod
88-
def all(cls, **query_params) -> Generator[Resource]:
89-
cls._check_query_params(query_params)
90-
next_page_url = f'{cls._endpoint}?{urlencode(query_params)}'
88+
def all(cls, **query_params) -> Generator[Resource, None, None]:
89+
q = cls._query_params(**query_params)
90+
next_page_url = f'{cls._endpoint}?{urlencode(q.dict())}'
9191
while next_page_url:
9292
page = session.get(next_page_url)
9393
yield from (cls._from_dict(item) for item in page['items'])
94-
next_page_url = page['next']
95-
96-
@classmethod
97-
def _check_query_params(cls, query_params):
98-
if not query_params:
99-
return
100-
unaccepted = set(query_params.keys()) - cls._query_params
101-
if unaccepted:
102-
raise ValueError(f'{unaccepted} are not accepted query parameters')
94+
next_page_url = page['next_page_url']

0 commit comments

Comments
 (0)