diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac80941..47e6df7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,5 +30,8 @@ jobs: cache: 'poetry' - name: Install dependencies run: poetry install - - name: test code + - name: Type checking + # Mypy configuration defined in pyproject.toml + run: poetry run mypy + - name: Run tests run: poetry run pytest diff --git a/CHANGELOG.md b/CHANGELOG.md index 67bc43c..a2d2432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - improved the documentation. - Added github action to release the package. +- Added types to IdApi, ServerError, Utilities, and WebApi classes ## [1.0.9] - 2022-11-01 ### Added @@ -52,4 +53,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed python semantics. - Moved zipfile out from webapi class. -## [0.0.12] - 2022-08-19 \ No newline at end of file +## [0.0.12] - 2022-08-19 diff --git a/poetry.lock b/poetry.lock index bd73600..27f397c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -124,6 +124,25 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "mypy" +version = "0.971" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mypy-extensions = ">=0.4.3" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} +typing-extensions = ">=3.10" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -285,6 +304,25 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "types-requests" +version = "2.28.11.2" +description = "Typing stubs for requests" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +types-urllib3 = "<1.27" + +[[package]] +name = "types-urllib3" +version = "1.26.25.1" +description = "Typing stubs for urllib3" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "typing-extensions" version = "4.1.1" @@ -321,7 +359,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black ( [metadata] lock-version = "1.1" python-versions = "^3.6.2" -content-hash = "4f0a9a2749e56acc74c8e9a97f5d5e5a4648238210117c287d21981e6f21d86c" +content-hash = "0825c887afa64a32bc2375a56c1529d86d69e52cd5ff6ba6b0887652c2cdc2ee" [metadata.files] atomicwrites = [ @@ -388,6 +426,31 @@ iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] +mypy = [ + {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, + {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, + {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, + {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, + {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, + {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, + {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, + {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, + {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, + {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, + {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, + {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, + {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, + {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, + {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, + {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, + {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, + {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, + {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, + {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, + {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, + {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, + {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, +] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, @@ -499,6 +562,14 @@ typed-ast = [ {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] +types-requests = [ + {file = "types-requests-2.28.11.2.tar.gz", hash = "sha256:fdcd7bd148139fb8eef72cf4a41ac7273872cad9e6ada14b11ff5dfdeee60ed3"}, + {file = "types_requests-2.28.11.2-py3-none-any.whl", hash = "sha256:14941f8023a80b16441b3b46caffcbfce5265fd14555844d6029697824b5a2ef"}, +] +types-urllib3 = [ + {file = "types-urllib3-1.26.25.1.tar.gz", hash = "sha256:a948584944b2412c9a74b9cf64f6c48caf8652cb88b38361316f6d15d8a184cd"}, + {file = "types_urllib3-1.26.25.1-py3-none-any.whl", hash = "sha256:f6422596cc9ee5fdf68f9d547f541096a20c2dcfd587e37c804c9ea720bf5cb2"}, +] typing-extensions = [ {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, diff --git a/pyproject.toml b/pyproject.toml index 80bbcc1..f54fb25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,19 @@ pycryptodome = "3.9.8" pytest = "^7.0.0" responses = "0.13.3" black = "22.8.0" +mypy = "^0.971" +types-requests = "^2.28.11.2" + +[tool.mypy] +files = "smile_id_core,tests" +pretty = true +show_error_codes = true +strict_equality = true +warn_unused_configs = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true [build-system] requires = ["poetry-core"] diff --git a/smile_id_core/IdApi.py b/smile_id_core/IdApi.py index 8096414..6177c89 100644 --- a/smile_id_core/IdApi.py +++ b/smile_id_core/IdApi.py @@ -1,5 +1,7 @@ import json -from typing import Dict +from typing import Optional, Dict, Union + +from requests import Response from smile_id_core.Utilities import ( Utilities, @@ -14,7 +16,7 @@ class IdApi: - def __init__(self, partner_id: str, api_key: str, sid_server: str or int): + def __init__(self, partner_id: str, api_key: str, sid_server: Union[str, int]): if not partner_id or not api_key: raise ValueError("partner_id or api_key cannot be null or empty") self.partner_id = partner_id @@ -22,15 +24,15 @@ def __init__(self, partner_id: str, api_key: str, sid_server: str or int): if sid_server in [0, 1, "0", "1"]: self.url = sid_server_map[int(sid_server)] else: - self.url = sid_server + self.url = str(sid_server) def submit_job( self, partner_params: Dict, id_params: Dict, use_validation_api=True, - options_params: Dict = None, - ): + options_params: Optional[Dict] = None, + ) -> Response: if not options_params: options_params = {} @@ -59,7 +61,9 @@ def submit_job( ) return response - def __configure_json(self, partner_params, id_params, sec_key): + def __configure_json( + self, partner_params: Dict, id_params: Dict, sec_key: Dict + ) -> Dict: validate_sec_params(sec_key) payload = { **sec_key, @@ -69,7 +73,7 @@ def __configure_json(self, partner_params, id_params, sec_key): payload.update(id_params) return payload - def __execute_http(self, payload): + def __execute_http(self, payload: Dict) -> Response: data = json.dumps(payload) resp = requests.post( url=f"{self.url}/id_verification", diff --git a/smile_id_core/ServerError.py b/smile_id_core/ServerError.py index 154429a..992478f 100644 --- a/smile_id_core/ServerError.py +++ b/smile_id_core/ServerError.py @@ -2,5 +2,5 @@ class ServerError(Exception): - def __init__(self, message): + def __init__(self, message: str): self.message = message diff --git a/smile_id_core/Utilities.py b/smile_id_core/Utilities.py index 8fb7f92..957b413 100644 --- a/smile_id_core/Utilities.py +++ b/smile_id_core/Utilities.py @@ -1,7 +1,8 @@ import json -from typing import Dict +from typing import Union, Optional, Dict import requests +from requests import Response from smile_id_core.Signature import Signature from smile_id_core.ServerError import ServerError @@ -15,18 +16,23 @@ class Utilities: - def __init__(self, partner_id, api_key, sid_server): + def __init__(self, partner_id: str, api_key: str, sid_server: Union[int, str]): if not partner_id or not api_key: raise ValueError("partner_id or api_key cannot be null or empty") self.partner_id = partner_id self.api_key = api_key self.sid_server = sid_server - if sid_server in [0, 1]: - self.url = sid_server_map[sid_server] + if sid_server in [0, 1, "0", "1"]: + self.url = sid_server_map[int(sid_server)] else: - self.url = sid_server - - def get_job_status(self, partner_params, option_params, sec_params=None): + self.url = str(sid_server) + + def get_job_status( + self, + partner_params: Dict, + option_params: Dict, + sec_params: Optional[Dict] = None, + ) -> Response: if sec_params is None: sec_params = get_signature( self.partner_id, self.api_key, option_params.get("signature") @@ -47,13 +53,15 @@ def get_job_status(self, partner_params, option_params, sec_params=None): else: options = option_params return self.__query_job_status( - partner_params.get("user_id"), - partner_params.get("job_id"), + str(partner_params.get("user_id")), + str(partner_params.get("job_id")), options, sec_params, ) - def __query_job_status(self, user_id, job_id, option_params, sec_params): + def __query_job_status( + self, user_id: str, job_id: str, option_params: Dict, sec_params: Dict + ) -> Response: job_status = Utilities.execute_post( f"{self.url}/job_status", self.__configure_job_query(user_id, job_id, option_params, sec_params), @@ -77,7 +85,9 @@ def __query_job_status(self, user_id, job_id, option_params, sec_params): ) return job_status - def __configure_job_query(self, user_id, job_id, options, sec_params): + def __configure_job_query( + self, user_id: str, job_id: str, options: Dict, sec_params: Dict + ) -> Dict: return { **sec_params, "partner_id": self.partner_id, @@ -88,7 +98,7 @@ def __configure_job_query(self, user_id, job_id, options, sec_params): } @staticmethod - def validate_partner_params(partner_params): + def validate_partner_params(partner_params: Dict) -> None: if not partner_params: raise ValueError("Please ensure that you send through partner params") @@ -110,8 +120,11 @@ def validate_partner_params(partner_params): @staticmethod def validate_id_params( - sid_server, id_info_params, partner_params, use_validation_api=False - ): + sid_server: Union[str, int], + id_info_params: Dict, + partner_params: Dict, + use_validation_api=False, + ) -> None: job_type = partner_params.get("job_type") if job_type != 6 and not id_info_params.get("entered"): return @@ -161,11 +174,11 @@ def validate_id_params( raise ValueError(f"key {key} cannot be empty") @staticmethod - def get_smile_id_services(sid_server): - if sid_server in [0, 1]: - url = sid_server_map[sid_server] + def get_smile_id_services(sid_server: Union[str, int]) -> Response: + if sid_server in [0, 1, "0", "1"]: + url = sid_server_map[int(sid_server)] else: - url = sid_server + url = str(sid_server) response = Utilities.execute_get(f"{url}/services") if response.status_code != 200: raise ServerError( @@ -174,7 +187,7 @@ def get_smile_id_services(sid_server): return response @staticmethod - def execute_get(url): + def execute_get(url: str) -> Response: resp = requests.get( url=url, headers={ @@ -185,7 +198,7 @@ def execute_get(url): return resp @staticmethod - def execute_post(url, payload): + def execute_post(url: str, payload: Dict) -> Response: data = json.dumps(payload) resp = requests.post( url=url, @@ -199,14 +212,14 @@ def execute_post(url, payload): return resp -def validate_sec_params(sec_key_dict: Dict): +def validate_sec_params(sec_key_dict: Dict) -> None: if not sec_key_dict.get("sec_key") and not sec_key_dict.get("signature"): raise Exception("Missing key, must provide a 'sec_key' or 'signature' field") if not sec_key_dict.get("timestamp"): raise Exception("Missing 'timestamp' field") -def get_signature(partner_id, api_key, is_signature): +def get_signature(partner_id: str, api_key: str, is_signature) -> Dict[str, str]: sec_key_gen = Signature(partner_id, api_key) sec_key_object = ( sec_key_gen.generate_signature() diff --git a/smile_id_core/WebApi.py b/smile_id_core/WebApi.py index 5f4c2d1..3e7a96f 100644 --- a/smile_id_core/WebApi.py +++ b/smile_id_core/WebApi.py @@ -1,9 +1,10 @@ import json import time from datetime import datetime -from typing import Dict +from typing import Any, Optional, Union, Dict, List import requests +from requests import Response from smile_id_core.image_upload import generate_zip_file, validate_images from smile_id_core.IdApi import IdApi @@ -20,28 +21,34 @@ class WebApi: - def __init__(self, partner_id, call_back_url, api_key, sid_server): + def __init__( + self, + partner_id: str, + call_back_url: str, + api_key: str, + sid_server: Union[str, int], + ): if not partner_id or not api_key: raise ValueError("partner_id or api_key cannot be null or empty") self.partner_id = partner_id self.call_back_url = call_back_url self.api_key = api_key self.sid_server = sid_server - self.utilities = None + self.utilities: Optional[Utilities] = None - if sid_server in [0, 1]: - self.url = sid_server_map[sid_server] + if sid_server in [0, 1, "0", "1"]: + self.url = sid_server_map[int(sid_server)] else: - self.url = sid_server + self.url = str(sid_server) def submit_job( self, - partner_params, - images_params, - id_info_params, - options_params, + partner_params: Dict, + images_params: List[Dict], + id_info_params: Dict, + options_params: Dict, use_validation_api=True, - ): + ) -> Union[Response, Dict]: Utilities.validate_partner_params(partner_params) job_type = partner_params.get("job_type") @@ -100,8 +107,8 @@ def submit_job( ) else: prep_upload_json_resp = prep_upload.json() - upload_url = prep_upload_json_resp["upload_url"] - smile_job_id = prep_upload_json_resp["smile_job_id"] + upload_url: str = prep_upload_json_resp["upload_url"] + smile_job_id: str = prep_upload_json_resp["smile_job_id"] zip_stream = generate_zip_file( partner_id=self.partner_id, callback_url=self.call_back_url, @@ -129,11 +136,18 @@ def submit_job( return {"success": True, "smile_job_id": smile_job_id} def get_web_token( - self, user_id: str, job_id: str, product: str, timestamp=None, callback_url=None - ): + self, + user_id: str, + job_id: str, + product: str, + timestamp: Optional[str] = None, + callback_url: Optional[str] = None, + ) -> Response: + timestamp = timestamp or datetime.now().isoformat() + callback_url = callback_url or self.call_back_url sec_params = Signature(self.partner_id, self.api_key).generate_signature( - timestamp or datetime.now().isoformat() + timestamp ) return WebApi.execute_http( @@ -143,12 +157,12 @@ def get_web_token( "user_id": user_id, "job_id": job_id, "product": product, - "callback_url": callback_url or self.call_back_url, + "callback_url": callback_url, "partner_id": self.partner_id, }, ) - def _get_security_key_params(self, options_params): + def _get_security_key_params(self, options_params: Dict) -> Dict[str, str]: return get_signature( self.partner_id, self.api_key, options_params.get("signature") ) @@ -159,13 +173,13 @@ def __call_id_api( id_info_params: Dict, use_validation_api: bool, options_params: Dict, - ): + ) -> Response: id_api = IdApi(self.partner_id, self.api_key, self.sid_server) return id_api.submit_job( partner_params, id_info_params, use_validation_api, options_params ) - def __validate_options(self, options_params): + def __validate_options(self, options_params: Dict) -> None: if not self.call_back_url and not options_params: raise ValueError( "Please choose to either get your response via the callback or job status query" @@ -176,19 +190,19 @@ def __validate_options(self, options_params): if key != "optional_callback" and not type(options_params[key]) == bool: raise ValueError(f"{key} needs to be a boolean") - def __validate_return_data(self, options): + def __validate_return_data(self, options: Dict) -> None: if not self.call_back_url and not options["return_job_status"]: raise ValueError( "Please choose to either get your response via the callback or job status query" ) - def __get_sec_key(self): + def __get_sec_key(self) -> Dict: sec_key_gen = Signature(self.partner_id, self.api_key) return sec_key_gen.generate_sec_key() def __prepare_prep_upload_payload( - self, partner_params: Dict, sec_params: Dict, use_enrolled_image - ): + self, partner_params: Dict, sec_params: Dict, use_enrolled_image: bool + ) -> Dict: validate_sec_params(sec_params) return { @@ -202,8 +216,12 @@ def __prepare_prep_upload_payload( } def poll_job_status( - self, counter, partner_params, options_params, sec_params: Dict - ): + self, + counter: int, + partner_params: Dict, + options_params: Dict, + sec_params: Optional[Dict], + ) -> Response: if sec_params is None: sec_params = self._get_security_key_params(options_params) @@ -213,7 +231,8 @@ def poll_job_status( time.sleep(2) else: time.sleep(4) - + if not isinstance(self.utilities, Utilities): + raise ValueError("Utilities not initialized") job_status = self.utilities.get_job_status( partner_params, options_params, sec_params ) @@ -226,7 +245,7 @@ def poll_job_status( return job_status @staticmethod - def execute_http(url, payload): + def execute_http(url: str, payload: Dict) -> Response: data = json.dumps(payload) resp = requests.post( url=url, @@ -240,7 +259,7 @@ def execute_http(url, payload): return resp @staticmethod - def upload(url, file): + def upload(url: str, file: Any) -> Response: resp = requests.put( url=url, data=file, headers={"Content-type": "application/zip"} ) diff --git a/smile_id_core/__init__.py b/smile_id_core/__init__.py index c97bd57..c4069af 100644 --- a/smile_id_core/__init__.py +++ b/smile_id_core/__init__.py @@ -1,7 +1,8 @@ -# import importlib.metadata if available, otherwise importlib_metadata (for Python < 3.8) -try: +import sys + +if sys.version_info >= (3, 8): import importlib.metadata as importlib_metadata -except ModuleNotFoundError: +else: import importlib_metadata from smile_id_core.IdApi import IdApi