diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 000000000..3fcc4517a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +# See https://pre-commit.com for more information +repos: + - repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + # Required to make flake8 read from pyproject.toml for now :( + additional_dependencies: ["flake8-pyproject"] + - repo: https://github.com/psf/black + rev: 23.9.1 + hooks: + - id: black + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.6.0 + hooks: + - id: mypy + language_version: "3.10" + args: [--explicit-package-bases,--config-file,pyproject.toml] + additional_dependencies: + [ + "types-requests", + "sqlalchemy-stubs", + "types-pyYAML", + "types-pkg-resources", + ] diff --git a/bin/wazo-dird-init-db b/bin/wazo-dird-init-db index a0a5e67a3..b0031c0d6 100755 --- a/bin/wazo-dird-init-db +++ b/bin/wazo-dird-init-db @@ -13,18 +13,41 @@ from xivo.user_rights import change_user def _parse_cli_args(args): parser = argparse.ArgumentParser() - parser.add_argument('--user', action='store', - help="The system user to use to connect to postgresql and create the user and database") - parser.add_argument('--pg_db_uri', action='store', default='postgresql:///postgres', - help="The DSN to connect to the postgres DB as an superuser") - parser.add_argument('--dird_db_uri', action='store', default='postgresql:///asterisk', - help="The DSN to connect to the dird DB as an superuser") - parser.add_argument('--db', action='store', default='asterisk', - help="The database name that will be created") - parser.add_argument('--owner', action='store', default='asterisk', - help="The database user that will be created and that will own the database") - parser.add_argument('--password', action='store', default='proformatique', - help="The password that will be assigned to the created user") + parser.add_argument( + '--user', + action='store', + help="The system user to use to connect to postgresql and create the user and database", + ) + parser.add_argument( + '--pg_db_uri', + action='store', + default='postgresql:///postgres', + help="The DSN to connect to the postgres DB as an superuser", + ) + parser.add_argument( + '--dird_db_uri', + action='store', + default='postgresql:///asterisk', + help="The DSN to connect to the dird DB as an superuser", + ) + parser.add_argument( + '--db', + action='store', + default='asterisk', + help="The database name that will be created", + ) + parser.add_argument( + '--owner', + action='store', + default='asterisk', + help="The database user that will be created and that will own the database", + ) + parser.add_argument( + '--password', + action='store', + default='proformatique', + help="The password that will be assigned to the created user", + ) return parser.parse_args(args) diff --git a/integration_tests/assets/confd_data/asset.wazo_users_two_working_one_timeout/run_confd_timeout b/integration_tests/assets/confd_data/asset.wazo_users_two_working_one_timeout/run_confd_timeout index 54e00ec1e..90dd2362e 100755 --- a/integration_tests/assets/confd_data/asset.wazo_users_two_working_one_timeout/run_confd_timeout +++ b/integration_tests/assets/confd_data/asset.wazo_users_two_working_one_timeout/run_confd_timeout @@ -30,39 +30,39 @@ app = Flask(__name__) @app.route("/1.1/infos") def infos(): time.sleep(timeout) - return jsonify({ - "uuid": "6fa459ea-ee8a-3ca4-894e-db77e1europe" - }) + return jsonify({"uuid": "6fa459ea-ee8a-3ca4-894e-db77e1europe"}) @app.route("/1.1/users") def users(): time.sleep(timeout) - return jsonify({ - "total": 2, - "items": [ - { - "id": 42, - "line_id": 3, - "agent_id": 2, - "firstname": "Bob", - "lastname": "Dylan", - "exten": "1632", - "email": "bob@dylan.com", - "mobile_phone_number": "0634321243" - }, - { - "id": 100, - "line_id": 42, - "agent_id": None, - "firstname": "Charles", - "lastname": "European", - "exten": "9012", - "email": "", - "mobile_phone_number": "" - } - ] - }) + return jsonify( + { + "total": 2, + "items": [ + { + "id": 42, + "line_id": 3, + "agent_id": 2, + "firstname": "Bob", + "lastname": "Dylan", + "exten": "1632", + "email": "bob@dylan.com", + "mobile_phone_number": "0634321243", + }, + { + "id": 100, + "line_id": 42, + "agent_id": None, + "firstname": "Charles", + "lastname": "European", + "exten": "9012", + "email": "", + "mobile_phone_number": "", + }, + ], + } + ) if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..518b2f99f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[tool.black] +skip-string-normalization = true + +[tool.flake8] +show-source = true +max-line-length = 99 +application-import-names = "wazo_dird" +exclude = [ + "build", + ".tox", + ".eggs", + "integration_tests/assets/scripts" +] + +ignore = [ + "E203", # whitespace before ':' warnings + "E501", # line too long + "W503", # line break before binary operator +] + +[tool.mypy] +python_version = "3.10" + +show_error_codes = true +explicit_package_bases = true +check_untyped_defs = true +warn_unused_configs = true +follow_imports = "skip" +ignore_missing_imports = true + +plugins = [ + "sqlmypy" +] +exclude = [ + "build", + "integration_tests/docker/broken-plugins/setup.py" +] +packages = ["wazo_dird"] diff --git a/tox.ini b/tox.ini index b8b32a350..61541e546 100644 --- a/tox.ini +++ b/tox.ini @@ -17,39 +17,15 @@ deps = [testenv:black] skip_install = true -deps = black -commands = black --skip-string-normalization . -exclude = - integration_tests/assets/scripts - -[testenv:pycodestyle] -# E501: line too long (80 chars) -commands = - -sh -c 'pycodestyle --ignore=E501 wazo_dird > pycodestyle.txt' deps = - pycodestyle -allowlist_externals = - sh - -[testenv:pylint] -commands = - -sh -c 'pylint --rcfile=/usr/share/xivo-ci/pylintrc wazo_dird > pylint.txt' -deps = - -rrequirements.txt - -rtest-requirements.txt - pylint -allowlist_externals = - sh + pre-commit +commands = pre-commit run black --all-files [testenv:linters] skip_install = true basepython = python3.10 -deps = flake8 - flake8-colors - black -commands = - black --skip-string-normalization --check . - flake8 +deps = pre-commit +commands = pre-commit run --all-files [testenv:integration] use_develop = true @@ -65,15 +41,3 @@ commands = pytest {posargs} allowlist_externals = make - -[flake8] -exclude = - .tox - .eggs - integration_tests/assets/scripts -show-source = true -max-line-length = 99 -application-import-names = wazo_dird -# W503: line break before binary operator -# E203: whitespace before ':' warnings -ignore = E203, E501, W503 diff --git a/wazo_dird/exception.py b/wazo_dird/exception.py index 64a5cdeaf..3006b9db1 100644 --- a/wazo_dird/exception.py +++ b/wazo_dird/exception.py @@ -158,7 +158,9 @@ def __init__(self, location_path, msg): class InvalidSourceConfigError(InvalidConfigError): - def __init__(self, source_info: dict, details: dict = None, details_fmt: str = ''): + def __init__( + self, source_info: dict, details: dict | None = None, details_fmt: str = '' + ): assert 'backend' in source_info, repr(source_info) super().__init__( location_path=f'/backends/{source_info["backend"]}/sources', @@ -172,7 +174,7 @@ def __init__(self, source_info: dict, details: dict = None, details_fmt: str = ' class InvalidSourceConfigAPIError(APIException): - def __init__(self, source_info: dict, details: dict = None) -> None: + def __init__(self, source_info: dict, details: dict | None = None) -> None: details = details or {} details.update(source_info=source_info) super().__init__( diff --git a/wazo_dird/plugin_helpers/tests/test_confd_client_registry.py b/wazo_dird/plugin_helpers/tests/test_confd_client_registry.py index d2921a6b1..e0e0cb037 100644 --- a/wazo_dird/plugin_helpers/tests/test_confd_client_registry.py +++ b/wazo_dird/plugin_helpers/tests/test_confd_client_registry.py @@ -43,6 +43,6 @@ def test_set_tenant_with_key_file(self): def test_set_tenant_without_key_file(self): config = deepcopy(SOURCE_CONFIG) - del config['auth']['key_file'] + del config['auth']['key_file'] # type: ignore[attr-defined] self.registry.get(config) assert self.confd_client.tenant_uuid != SOURCE_CONFIG['tenant_uuid'] diff --git a/wazo_dird/plugin_manager.py b/wazo_dird/plugin_manager.py index 183f8e010..db5ef3ed6 100644 --- a/wazo_dird/plugin_manager.py +++ b/wazo_dird/plugin_manager.py @@ -43,7 +43,7 @@ def load_services( controller: Controller, ): global services_extension_manager - dependencies: ServiceDependencies = { + dependencies = { 'config': config, 'source_manager': source_manager, 'bus': bus, @@ -90,7 +90,7 @@ def load_views( rest_api: CoreRestApi, ): global views_extension_manager - dependencies: ViewDependencies = { + dependencies = { 'config': config, 'services': services, 'auth_client': auth_client, diff --git a/wazo_dird/plugins/config/http.py b/wazo_dird/plugins/config/http.py index 077924fa1..69598f4c8 100644 --- a/wazo_dird/plugins/config/http.py +++ b/wazo_dird/plugins/config/http.py @@ -3,13 +3,14 @@ from wazo_dird.auth import required_acl, required_master_tenant from wazo_dird.http import AuthResource +from wazo_dird.plugins.config_service.plugin import Service as ConfigService class Config(AuthResource): - _config_service = None + _config_service: ConfigService @classmethod - def configure(cls, config_service): + def configure(cls, config_service: ConfigService): cls._config_service = config_service @required_master_tenant() diff --git a/wazo_dird/plugins/office365_backend/services.py b/wazo_dird/plugins/office365_backend/services.py index f05e50e63..c35b16c35 100644 --- a/wazo_dird/plugins/office365_backend/services.py +++ b/wazo_dird/plugins/office365_backend/services.py @@ -1,8 +1,10 @@ # Copyright 2019-2023 The Wazo Authors (see the AUTHORS file) # SPDX-License-Identifier: GPL-3.0-or-later +from __future__ import annotations import itertools import logging +from typing import TypedDict import uuid import requests @@ -132,10 +134,19 @@ def get_microsoft_access_token(user_uuid, wazo_token, **auth_config): logger.error('Error occured while connecting to wazo-auth, error :%s', e) -def get_first_email(contact_information): - return next(iter(contact_information.get('emailAddresses') or []), {}).get( - 'address' +class EmailAddress(TypedDict, total=False): + address: str + + +class ContactInformation(TypedDict, total=False): + emailAddresses: list[EmailAddress] + + +def get_first_email(contact_information: ContactInformation) -> str | None: + first_email: EmailAddress = next( + iter(contact_information.get('emailAddresses') or ()), {} ) + return first_email.get('address') def aggregate_numbers(contact): diff --git a/wazo_dird/plugins/personal/http.py b/wazo_dird/plugins/personal/http.py index ce621ca8f..d72b55e2b 100644 --- a/wazo_dird/plugins/personal/http.py +++ b/wazo_dird/plugins/personal/http.py @@ -5,6 +5,7 @@ import io import logging import re +from typing import ClassVar from flask import request from flask import Response @@ -16,6 +17,7 @@ from wazo_dird import auth from wazo_dird.auth import required_acl from wazo_dird.http import LegacyAuthResource +from wazo_dird.plugins.personal_service.plugin import _PersonalService logger = logging.getLogger(__name__) @@ -37,10 +39,10 @@ def _get_calling_user_uuid(): class PersonalAll(LegacyAuthResource): - personal_service = None + personal_service: ClassVar[_PersonalService] @classmethod - def configure(cls, personal_service): + def configure(cls, personal_service: _PersonalService): cls.personal_service = personal_service @required_acl('dird.personal.create') @@ -119,10 +121,10 @@ def format_json(contacts): class PersonalOne(LegacyAuthResource): - personal_service = None + personal_service: ClassVar[_PersonalService] @classmethod - def configure(cls, personal_service): + def configure(cls, personal_service: _PersonalService): cls.personal_service = personal_service @required_acl('dird.personal.{contact_id}.read') @@ -170,10 +172,10 @@ def delete(self, contact_id): class PersonalImport(LegacyAuthResource): - personal_service = None + personal_service: ClassVar[_PersonalService] @classmethod - def configure(cls, personal_service): + def configure(cls, personal_service: _PersonalService): cls.personal_service = personal_service @required_acl('dird.personal.import.create') diff --git a/wazo_dird/plugins/phonebook_backend/schemas.py b/wazo_dird/plugins/phonebook_backend/schemas.py index f5eda8f50..5cdcb5e4a 100644 --- a/wazo_dird/plugins/phonebook_backend/schemas.py +++ b/wazo_dird/plugins/phonebook_backend/schemas.py @@ -40,7 +40,7 @@ class CountParams(TypedDict): class ContactListSchema(_ListSchema): - searchable_columns = [] + searchable_columns: list[str] = [] sort_columns = ['firstname', 'lastname', 'number'] default_sort_column = None diff --git a/wazo_dird/plugins/service_discovery_service/tests/test_service_discovery_service.py b/wazo_dird/plugins/service_discovery_service/tests/test_service_discovery_service.py index 130a41131..aa9f34bf4 100644 --- a/wazo_dird/plugins/service_discovery_service/tests/test_service_discovery_service.py +++ b/wazo_dird/plugins/service_discovery_service/tests/test_service_discovery_service.py @@ -3,6 +3,7 @@ import os import tempfile +from typing import Any import unittest from hamcrest import assert_that, equal_to @@ -86,15 +87,14 @@ def test_that_the_service_looks_for_remote_servers_when_starting(self): def new_template_file(content): - f = tempfile.NamedTemporaryFile(delete=False) - with open(f.name, 'w') as f: + with tempfile.NamedTemporaryFile(mode='w', delete=False) as f: f.write(content) dir_, name = os.path.split(f.name) return f, dir_, name # TODO fix when the config will be generated by events -@unittest.skip +@unittest.skip('fix when the config will be generated by events') class TestSourceConfigGenerator(unittest.TestCase): def setUp(self): ( @@ -110,7 +110,7 @@ def tearDown(self): return def test_generate_with_an_unknown_service(self): - service_discovery_config = { + service_discovery_config: dict[str, Any] = { 'template_path': None, 'services': {}, 'hosts': { @@ -186,7 +186,7 @@ def test_generate_with_a_service(self): # TODO fix when the config will be generated by events -@unittest.skip +@unittest.skip("fix when the config will be generated by events") class TestProfileConfigUpdater(unittest.TestCase): def setUp(self): self.config = dict(CONFIG) diff --git a/wazo_dird/plugins/source_result.py b/wazo_dird/plugins/source_result.py index fe7845c4e..c6ed49f02 100644 --- a/wazo_dird/plugins/source_result.py +++ b/wazo_dird/plugins/source_result.py @@ -37,6 +37,8 @@ class _SourceResult: backend: str source: str _format_columns: dict[str, str] = {} + is_deletable: bool + is_personal: bool fields: dict[str, Any | None] relations: dict[str, Any | None] diff --git a/wazo_dird/plugins/wazo_user_backend/tests/test_schemas.py b/wazo_dird/plugins/wazo_user_backend/tests/test_schemas.py index 32e548c23..45f0b18f9 100644 --- a/wazo_dird/plugins/wazo_user_backend/tests/test_schemas.py +++ b/wazo_dird/plugins/wazo_user_backend/tests/test_schemas.py @@ -60,7 +60,7 @@ def test_that_username_password_or_keyfile_is_present(self): 'key_file': '/var/lib/wazo-auth-keys/wazo-dird-wazo-backend-key.yml' } username_and_key_file = {'username': 'foo', 'key_file': 'bar'} - no_auth_info = {} + no_auth_info: dict = {} assert_that( calling(source_schema.load).with_args( diff --git a/wazo_dird/schemas.py b/wazo_dird/schemas.py index 366f172c9..a5163990e 100644 --- a/wazo_dird/schemas.py +++ b/wazo_dird/schemas.py @@ -8,9 +8,7 @@ ) from xivo.mallow import fields from xivo.mallow.validate import Length, Range, validate_string_dict -from xivo.mallow_helpers import Schema - -BaseSchema = Schema +from xivo.mallow_helpers import Schema as BaseSchema class VerifyCertificateField(fields.Field): diff --git a/wazo_dird/source_manager.py b/wazo_dird/source_manager.py index f9dbf3a9c..b6da76e1b 100644 --- a/wazo_dird/source_manager.py +++ b/wazo_dird/source_manager.py @@ -49,7 +49,8 @@ def __init__( self._source_service: SourceServiceProtocol | None = None self._source_lock = threading.Lock() - def get(self, source_uuid: str) -> BaseSourcePlugin | None: + def get(self, source_uuid): + assert self._source_service with self._source_lock: source = self._sources.get(source_uuid) if not source: