diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e90806a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: ci + +on: + pull_request: + paths-ignore: + - '*.md' + - 'alice/VERSION' + branches: + - master + +env: + ONBOARDING_API_KEY: ${{ secrets.ONBOARDING_API_KEY }} + ONBOARDING_SANDBOX_TOKEN: ${{ secrets.ONBOARDING_SANDBOX_TOKEN }} + CONCURRENT_TESTING: True + +jobs: + ci: + strategy: + max-parallel: 4 + matrix: + os: [ macOS-latest, ubuntu-latest ] + python-version: [3.7, 3.8, 3.9] + runs-on: ${{matrix.os}} + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - uses: actions/cache@v2 + with: + path: ${{ env.pythonLocation }} + key: ${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('dev-requirements.txt') }} + - name: Install Requirements + run: | + pip install lume + lume -install + - name: Lint + run: lume -lint + - name: Check Requirements + run: lume -check-requirements + - name: Test [pytest] + run: lume -test + - name: Example [Onboarding] + run: python examples/onboarding.py + - name: Example [Onboarding with Identification] + run: python examples/onboarding_with_identification.py + - name: Example [Onboarding with Certificate] + run: python examples/onboarding_with_certificate.py + - name: Example [Onboarding with Screening] + run: python examples/onboarding_with_screening.py + - name: Example [Auth] + run: python examples/auth.py + - name: Example [Sandbox] + run: python examples/sandbox.py diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 8b5cc7f..644dc88 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -11,6 +11,7 @@ env: ONBOARDING_API_KEY: ${{ secrets.ONBOARDING_API_KEY }} ONBOARDING_SANDBOX_TOKEN: ${{ secrets.ONBOARDING_SANDBOX_TOKEN }} ALICE_GITHUB_ACCESS_TOKEN: ${{ secrets.ALICE_GITHUB_ACCESS_TOKEN }} + CONCURRENT_TESTING: True jobs: ci: @@ -18,48 +19,26 @@ jobs: max-parallel: 4 matrix: os: [ macOS-latest, ubuntu-latest ] - python-version: [3.8] + python-version: [3.7, 3.8, 3.9] runs-on: ${{matrix.os}} steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - - uses: actions/cache@v1 - if: startsWith(runner.os, 'Linux') + - uses: actions/cache@v2 with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - uses: actions/cache@v1 - if: startsWith(runner.os, 'macOS') - with: - path: ~/Library/Caches/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - uses: actions/cache@v1 - if: startsWith(runner.os, 'Windows') - with: - path: ~\AppData\Local\pip\Cache - key: ${{ runner.os }}-pip-${{ hashFiles('**/dev.txt') }} - restore-keys: | - ${{ runner.os }}-pip- - - name: Install requirements - run: | - pip install -r requirements/dev.txt - - name: Lint with black & flake8 + path: ${{ env.pythonLocation }} + key: ${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('dev-requirements.txt') }} + - name: Install Requirements run: | - black . - flake8 alice - - name: Install package - run: | - pip install -e . # Dependencies are also included in the setup.py + pip install lume + lume -install + - name: Lint + run: lume -lint - name: Test [pytest] - run: | - pytest --tb=no + run: lume -test - name: Example [Onboarding] run: python examples/onboarding.py - name: Example [Onboarding with Identification] @@ -96,7 +75,7 @@ jobs: - name: Git - Commit VERSION File run: | git config --global user.email "dev@alicebiometrics.com" - git config --global user.name "ALiCE Biometrics" + git config --global user.name "Alice Biometrics" git commit -m "Update version to ${RELEASE_VERSION}" - name: Push changes uses: alice-biometrics/github-push-action@master diff --git a/README.md b/README.md index ad261d9..30d2dc1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The aim of this Python package is to manage the authentication and backend operations against ALiCE Onboarding API. -If you want more information about how to integrate with ALiCE technology, please contact us at support@alicebiometrics.com. +If you want more information about how to integrate with Alice technology, please contact us at support@alicebiometrics.com. ## Table of Contents - [Requirements](#requirements) diff --git a/alice/auth/auth_errors.py b/alice/auth/auth_errors.py index f22f15f..a60445b 100644 --- a/alice/auth/auth_errors.py +++ b/alice/auth/auth_errors.py @@ -1,6 +1,6 @@ +from dataclasses import dataclass from typing import Dict -from dataclasses import dataclass from meiga import Error from requests import Response diff --git a/alice/config.py b/alice/config.py index 480be8c..9b7ae1c 100644 --- a/alice/config.py +++ b/alice/config.py @@ -1,8 +1,9 @@ -from dataclasses import dataclass +from meiga import Error +from pydantic.dataclasses import dataclass @dataclass -class Config: +class Config(Error): onboarding_url: str = "https://apis.alicebiometrics.com/onboarding" sandbox_url: str = "https://apis.alicebiometrics.com/onboarding/sandbox" api_key: str = None diff --git a/alice/onboarding/device_info.py b/alice/onboarding/device_info.py index d2bde4f..49c8d76 100644 --- a/alice/onboarding/device_info.py +++ b/alice/onboarding/device_info.py @@ -1,12 +1,9 @@ from typing import Optional -from dataclasses import dataclass -from dataclasses_json import dataclass_json +from pydantic import BaseModel -@dataclass_json -@dataclass -class DeviceInfo: +class DeviceInfo(BaseModel): device_platform: Optional[str] = None device_platform_version: Optional[str] = None device_model: Optional[str] = None diff --git a/alice/onboarding/onboarding.py b/alice/onboarding/onboarding.py index ba12f1c..4583a44 100644 --- a/alice/onboarding/onboarding.py +++ b/alice/onboarding/onboarding.py @@ -1080,7 +1080,6 @@ def screening_monitor_open_alerts( response = self.onboarding_client.screening_monitor_open_alerts( start_index=start_index, size=size, verbose=verbose ) - if response.status_code == 200: return Success(response.json()) else: diff --git a/alice/onboarding/onboarding_client.py b/alice/onboarding/onboarding_client.py index e984114..9a80f64 100644 --- a/alice/onboarding/onboarding_client.py +++ b/alice/onboarding/onboarding_client.py @@ -95,10 +95,10 @@ def create_user( data = None if user_info: data = data if data is not None else {} - data.update(json.loads(user_info.to_json())) + data.update(user_info.dict()) if device_info: data = data if data is not None else {} - data.update(json.loads(device_info.to_json())) + data.update(device_info.dict()) response = requests.post(f"{self.url}/user", headers=headers, data=data) diff --git a/alice/onboarding/onboarding_errors.py b/alice/onboarding/onboarding_errors.py index 4f70728..30eb405 100644 --- a/alice/onboarding/onboarding_errors.py +++ b/alice/onboarding/onboarding_errors.py @@ -1,7 +1,7 @@ -from typing import Dict +from typing import Any, Dict, Optional -from dataclasses import dataclass from meiga import Error +from pydantic.dataclasses import dataclass from requests import Response @@ -9,7 +9,7 @@ class OnboardingError(Error): operation: str code: int - message: Dict[str, str] + message: Optional[Dict[str, Any]] = None def __str__(self): return self.__repr__() diff --git a/alice/onboarding/user_info.py b/alice/onboarding/user_info.py index d51b0c2..690edaf 100644 --- a/alice/onboarding/user_info.py +++ b/alice/onboarding/user_info.py @@ -1,12 +1,9 @@ from typing import Optional -from dataclasses import dataclass -from dataclasses_json import dataclass_json +from pydantic import BaseModel -@dataclass_json -@dataclass -class UserInfo: +class UserInfo(BaseModel): first_name: Optional[str] = None last_name: Optional[str] = None email: Optional[str] = None diff --git a/alice/public_api.py b/alice/public_api.py index b155293..98d4d97 100644 --- a/alice/public_api.py +++ b/alice/public_api.py @@ -1,7 +1,7 @@ # coding=utf-8 # Copyright (C) 2019+ Alice, Vigo, Spain -"""Public API of ALiCE Onboarding Python SDK""" +"""Public API of Alice Onboarding Python SDK""" # Modules from alice.webhooks.webhook import Webhook @@ -10,18 +10,18 @@ modules = [] +from alice.auth.auth import Auth +from alice.auth.auth_client import AuthClient +from alice.config import Config +from alice.onboarding.decision import Decision +from alice.onboarding.device_info import DeviceInfo + # Classes from alice.onboarding.onboarding import Onboarding from alice.onboarding.onboarding_client import OnboardingClient from alice.onboarding.user_info import UserInfo -from alice.onboarding.device_info import DeviceInfo -from alice.onboarding.decision import Decision -from alice.auth.auth import Auth -from alice.auth.auth_client import AuthClient from alice.sandbox.sandbox import Sandbox from alice.sandbox.sandbox_client import SandboxClient -from alice.config import Config - classes = [ "Onboarding", diff --git a/alice/sandbox/sandbox_client.py b/alice/sandbox/sandbox_client.py index 70b6a79..da915f6 100644 --- a/alice/sandbox/sandbox_client.py +++ b/alice/sandbox/sandbox_client.py @@ -1,12 +1,8 @@ -import json +from requests import Response, request -from requests import request, Response - -from alice.onboarding.tools import timeit, print_intro, print_response - -from alice.onboarding.user_info import UserInfo from alice.onboarding.device_info import DeviceInfo - +from alice.onboarding.tools import print_intro, print_response, timeit +from alice.onboarding.user_info import UserInfo DEFAULT_URL = "https://apis.alicebiometrics.com/onboarding/sandbox" @@ -76,10 +72,10 @@ def create_user( data = None if user_info: data = data if data is not None else {} - data.update(json.loads(user_info.to_json())) + data.update(user_info.dict()) if device_info: data = data if data is not None else {} - data.update(json.loads(device_info.to_json())) + data.update(device_info.dict()) response = request("POST", self.url + "/user", headers=headers, data=data) diff --git a/alice/sandbox/sandbox_errors.py b/alice/sandbox/sandbox_errors.py index c3fd3c4..ea516a9 100644 --- a/alice/sandbox/sandbox_errors.py +++ b/alice/sandbox/sandbox_errors.py @@ -1,7 +1,7 @@ -from typing import Dict +from typing import Any, Dict, Optional -from dataclasses import dataclass from meiga import Error +from pydantic.dataclasses import dataclass from requests import Response @@ -9,7 +9,7 @@ class SandboxError(Error): operation: str code: int - message: Dict[str, str] + message: Optional[Dict[str, Any]] = None def __repr__(self): return f"[SandboxError: [operation: {self.operation} | code: {self.code} | message: {self.message}]]" diff --git a/alice/webhooks/webhooks.py b/alice/webhooks/webhooks.py index 4afd8b4..eb79e4a 100644 --- a/alice/webhooks/webhooks.py +++ b/alice/webhooks/webhooks.py @@ -1,10 +1,10 @@ from typing import Dict, List -from meiga import Result, Success, Failure, isSuccess +from meiga import Failure, Result, Success, isSuccess +from alice.auth.auth import Auth from alice.config import Config from alice.onboarding.onboarding_errors import OnboardingError -from alice.auth.auth import Auth from alice.webhooks.webhook import Webhook from alice.webhooks.webhooks_client import WebhooksClient @@ -63,7 +63,7 @@ def get_available_events( def create_webhook( self, webhook: Webhook, verbose: bool = False - ) -> Result[Dict, OnboardingError]: + ) -> Result[str, OnboardingError]: """ It creates a new Webhook in the onboarding service. diff --git a/lume.yml b/lume.yml new file mode 100644 index 0000000..ca30a84 --- /dev/null +++ b/lume.yml @@ -0,0 +1,23 @@ +name: onboarding-python + +install: + run: + - pip install --upgrade --upgrade-strategy eager -r requirements/dev-requirements.txt -r requirements/requirements.txt + - pip install -e . +steps: + clean: + run: + - rm -f .coverage + - rm -rf output + - rm -rf .pytest_cache + - find . | grep -E "(__pycache__|\.pyc|\.pyo$)" | xargs rm -rf + lint: + run: + - black --check . + - flake8 alice + check-requirements: + run: safety check -r requirements/requirements.txt + static-analysis: + run: mypy --namespace-packages alice + test: + run: pytest \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..0377058 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,16 @@ +# Configuration of py.test +[pytest] +markers=unit +addopts=tests + -v + --color=yes + --durations=10 +filterwarnings = + error + ignore::DeprecationWarning + +python_files=test_*.py +python_classes=Test* +python_functions=test_* should_ + +norecursedirs = examples alice requirements *.egg-info .git resources \ No newline at end of file diff --git a/requirements/dev-requirements.txt b/requirements/dev-requirements.txt new file mode 100644 index 0000000..7c25035 --- /dev/null +++ b/requirements/dev-requirements.txt @@ -0,0 +1,12 @@ +black==21.11b1 +flake8==4.0.1 +isort==5.6.4 +pytest==5.0.1 +pytest-cov==2.5.1 +pytest-mock==1.7.1 +pytest-env==0.6.2 +pytest-variables[yaml]==1.9.0 +pytest-clarity==1.0.1 +pre-commit==2.15.0 +mypy==0.910 +safety==1.10.3 \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index c680918..0000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,11 +0,0 @@ -pytest -flake8==3.7.7 -black==21.6b0 -isort==5.6.4 -pre-commit==2.13.0 -pytest-clarity==0.2.0a1 -pyjwt==2.1.0 -requests>=2.18.0 -dataclasses>=0.6 -dataclasses-json>=0.2.14 -meiga>=1.2.0 \ No newline at end of file diff --git a/requirements/requirements.txt b/requirements/requirements.txt new file mode 100644 index 0000000..3814e41 --- /dev/null +++ b/requirements/requirements.txt @@ -0,0 +1,4 @@ +pyjwt==2.3.0 +pydantic==1.8.2 +requests==2.26.0 +meiga==1.2.12 \ No newline at end of file diff --git a/setup.py b/setup.py index 7105570..ff28d50 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,23 @@ +import os + from setuptools import setup +CURRENT_DIR = os.path.abspath(os.path.dirname(__file__)) +PACKAGE_NAME = "alice-onboarding" VERSION = open("alice/VERSION", "r").read() -REQUIRES = [ - "pyjwt==2.1.0", - "requests>=2.18.0", - "dataclasses>=0.6", - "dataclasses-json>=0.2.14", - "meiga==1.2.12", -] + +with open(os.path.join(CURRENT_DIR, "README.md")) as fid: + README = fid.read() + +with open("requirements/requirements.txt") as f: + REQUIRES = f.read().splitlines() setup( - name="alice-onboarding", + name=PACKAGE_NAME, version=VERSION, + long_description=README, + long_description_content_type="text/markdown", + keywords=["onboarding", "biometrics", "kyc", "alice"], description="Alice Onboarding Python SDK", url="https://github.com/alice-biometrics/onboarding-python", author="Alice Biometrics", diff --git a/tests/test_integration_auth.py b/tests/test_integration_auth.py index 40d9740..819e65d 100644 --- a/tests/test_integration_auth.py +++ b/tests/test_integration_auth.py @@ -1,8 +1,10 @@ +import pytest from meiga.assertions import assert_failure from alice import Auth, Config +@pytest.mark.unit def test_should_return_an_error_when_the_api_key_is_not_configured(): config = Config() @@ -13,6 +15,7 @@ def test_should_return_an_error_when_the_api_key_is_not_configured(): assert_failure(result) +@pytest.mark.unit def test_should_create_a_valid_backend_token(given_valid_api_key): config = Config(api_key=given_valid_api_key) @@ -23,6 +26,7 @@ def test_should_create_a_valid_backend_token(given_valid_api_key): assert backend_token is not None +@pytest.mark.unit def test_should_create_a_valid_backend_token_with_user(given_valid_api_key): config = Config(api_key=given_valid_api_key) @@ -33,6 +37,7 @@ def test_should_create_a_valid_backend_token_with_user(given_valid_api_key): assert backend_token_with_user is not None +@pytest.mark.unit def test_should_create_a_valid_user_token(given_valid_api_key): config = Config(api_key=given_valid_api_key) diff --git a/tests/test_integration_onboarding.py b/tests/test_integration_onboarding.py index 30eb89b..0adf77e 100644 --- a/tests/test_integration_onboarding.py +++ b/tests/test_integration_onboarding.py @@ -1,12 +1,14 @@ from typing import Dict +import pytest from meiga import Error, Result, Success from meiga.assertions import assert_failure, assert_success from meiga.decorators import meiga -from alice import Config, Onboarding +from alice import Config, DeviceInfo, Onboarding, UserInfo +@pytest.mark.unit def test_should_return_an_error_when_the_api_key_is_not_configured(): config = Config() @@ -17,6 +19,7 @@ def test_should_return_an_error_when_the_api_key_is_not_configured(): assert_failure(result) +@pytest.mark.unit def test_should_do_complete_onboarding_process( given_valid_api_key, given_any_selfie_image_media_data, @@ -30,7 +33,10 @@ def do_complete_onboarding() -> Result[dict, Error]: onboarding = Onboarding.from_config(config) - user_id = onboarding.create_user().unwrap_or_return() + user_id = onboarding.create_user( + user_info=UserInfo(first_name="Alice", last_name="Biometrics"), + device_info=DeviceInfo(device_platform="Android"), + ).unwrap_or_return() onboarding.add_selfie( user_id=user_id, media_data=given_any_selfie_image_media_data ).unwrap_or_return() diff --git a/tests/test_integration_sandbox.py b/tests/test_integration_sandbox.py index e1bb518..5bfbd6a 100644 --- a/tests/test_integration_sandbox.py +++ b/tests/test_integration_sandbox.py @@ -1,8 +1,10 @@ +import pytest from meiga.assertions import assert_failure, assert_success -from alice import Sandbox, Config, UserInfo +from alice import Config, DeviceInfo, Sandbox, UserInfo +@pytest.mark.unit def test_should_return_an_error_when_the_sandbox_token_is_not_configured(): config = Config() @@ -13,6 +15,7 @@ def test_should_return_an_error_when_the_sandbox_token_is_not_configured(): assert_failure(result) +@pytest.mark.unit def test_should_create_a_user_and_get_user_token_and_delete_it( given_valid_sandbox_token, given_any_valid_mail ): @@ -21,7 +24,8 @@ def test_should_create_a_user_and_get_user_token_and_delete_it( sandbox = Sandbox.from_config(config) result_create_user = sandbox.create_user( - user_info=UserInfo(email=given_any_valid_mail) + user_info=UserInfo(email=given_any_valid_mail), + device_info=DeviceInfo(device_platform="Android"), ) assert_success(result_create_user) diff --git a/tests/test_integration_webhooks.py b/tests/test_integration_webhooks.py index 1e3fab5..4311364 100644 --- a/tests/test_integration_webhooks.py +++ b/tests/test_integration_webhooks.py @@ -1,11 +1,14 @@ +import os import secrets from time import sleep +import pytest from meiga.assertions import assert_failure, assert_success -from alice import Config, Webhooks, Webhook +from alice import Config, Webhook, Webhooks +@pytest.mark.unit def test_should_return_an_error_when_the_api_key_is_not_configured(): config = Config() @@ -16,6 +19,7 @@ def test_should_return_an_error_when_the_api_key_is_not_configured(): assert_failure(result) +@pytest.mark.unit def test_should_execute_all_webhook_lifecycle(given_valid_api_key): config = Config(api_key=given_valid_api_key) webhooks_client = Webhooks.from_config(config) @@ -92,6 +96,13 @@ def test_should_execute_all_webhook_lifecycle(given_valid_api_key): sleep(3.0) - # Expected not found error when Retrieve all webhook results of an deleted webhook - result = webhooks_client.get_webhook_results(webhook_id) - assert_failure(result) + assert_webhook_results_status(webhooks_client, webhook_id) + + +def assert_webhook_results_status(webhooks_client: Webhooks, webhook_id: str): + if os.getenv("CONCURRENT_TESTING", False): + webhooks_client.get_webhook_results(webhook_id) + else: + # Expected not found error when Retrieve all webhook results of an deleted webhook + result = webhooks_client.get_webhook_results(webhook_id) + assert_failure(result)