diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index d5b831302d169f0ce8adbc8835d2909eee7fd51b..0000000000000000000000000000000000000000 --- a/.pylintrc +++ /dev/null @@ -1,9 +0,0 @@ -[MAIN] -extension-pkg-whitelist=pydantic - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -# Note that it does not contain TODO, only the default FIXME and XXX -notes=FIXME, - XXX diff --git a/docs/dump-openapi-spec.py b/docs/dump-openapi-spec.py index 944838ab2f2f496a4e4b1855f6f379ada7a5b376..8e35e71e44214222f67ea4cd65c331c57c4b7f01 100644 --- a/docs/dump-openapi-spec.py +++ b/docs/dump-openapi-spec.py @@ -4,21 +4,19 @@ import os from fastapi.testclient import TestClient import lso -config_filename = os.path.join( - os.path.dirname(__file__), - '..', 'config.json.example') +config_filename = os.path.join(os.path.dirname(__file__), "..", "config.json.example") output_filename = os.path.join( - os.path.dirname(__file__), - 'source', '_static', 'openapi.json') + os.path.dirname(__file__), "source", "_static", "openapi.json" +) -os.environ['SETTINGS_FILENAME'] = config_filename +os.environ["SETTINGS_FILENAME"] = config_filename app = lso.create_app() client = TestClient(app) -rsp = client.get('/openapi.json') +rsp = client.get("/openapi.json") openapi_doc = json.dumps(rsp.json(), indent=2) -with open(output_filename, 'w') as f: +with open(output_filename, "w") as f: f.write(openapi_doc) -print(f'wrote {output_filename}') +print(f"wrote {output_filename}") diff --git a/docs/source/conf.py b/docs/source/conf.py index 645bdfea854b6ce8886333e0a6a1805b94cc5087..1be1927aa3d4a5927e8aa07d234c9a79fa93f784 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -16,10 +16,9 @@ import json import os import sys -sys.path.insert(0, os.path.abspath( - os.path.join( - os.path.dirname(__file__), - '..', '..', 'lso'))) +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "lso")) +) class RenderAsJSON(Directive): @@ -28,56 +27,56 @@ class RenderAsJSON(Directive): required_arguments = 1 def run(self): - module_path, member_name = self.arguments[0].rsplit('.', 1) + module_path, member_name = self.arguments[0].rsplit(".", 1) member_data = getattr(import_module(module_path), member_name) code = json.dumps(member_data, indent=2) literal = nodes.literal_block(code, code) - literal['language'] = 'json' + literal["language"] = "json" return [ - addnodes.desc_name(text=member_name), - addnodes.desc_content('', literal) + addnodes.desc_name(text=member_name), + addnodes.desc_content("", literal), ] def setup(app): - app.add_directive('asjson', RenderAsJSON) + app.add_directive("asjson", RenderAsJSON) # -- Project information ----------------------------------------------------- -project = 'Lightweight Service Orchestrator' -copyright = '2023, GÉANT Vereniging' -author = 'GÉANT Orchestration & Automation Team' +project = "Lightweight Service Orchestrator" +copyright = "2023, GÉANT Vereniging" +author = "GÉANT Orchestration & Automation Team" # -- General configuration --------------------------------------------------- extensions = [ - 'sphinx_rtd_theme', - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.todo' + "sphinx_rtd_theme", + "sphinx.ext.autodoc", + "sphinx.ext.coverage", + "sphinx.ext.todo", ] -templates_path = ['templates'] +templates_path = ["templates"] exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- -html_theme = 'sphinx_rtd_theme' -html_static_path = ['_static'] +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] html_theme_options = { - 'style_nav_header_background': 'rgb(0 63 95)', + "style_nav_header_background": "rgb(0 63 95)", } -html_css_files = ['custom.css'] -html_logo = '_static/geant_logo_white.svg' +html_css_files = ["custom.css"] +html_logo = "_static/geant_logo_white.svg" # Both the class' and the ``__init__`` method's docstring are concatenated and inserted. -autoclass_content = 'both' -autodoc_typehints = 'none' +autoclass_content = "both" +autodoc_typehints = "none" # Display todos by setting to True todo_include_todos = True diff --git a/lso/__init__.py b/lso/__init__.py index 1faa2bee7490a776f3c3b3f0c5fa6b9c9c34ba01..03ff9e5912c06932103808428157e7774dcd4853 100644 --- a/lso/__init__.py +++ b/lso/__init__.py @@ -1,4 +1,5 @@ """Automatically invoked app factory.""" + import logging from fastapi import FastAPI @@ -13,7 +14,6 @@ def create_app() -> FastAPI: :return: a new flask app instance """ - app = FastAPI() # app = FastAPI(dependencies=[Depends(get_query_token)]) diff --git a/lso/app.py b/lso/app.py index 1c28f7639848a7e2a07c041338078f38390643bf..8ca6862c01e8e596b67ae3bac94f45599d5e5a52 100644 --- a/lso/app.py +++ b/lso/app.py @@ -1,4 +1,5 @@ """Default app creation.""" + import lso app = lso.create_app() diff --git a/lso/environment.py b/lso/environment.py index bd547e7521a5cbe40a9e62b2a9d3d0af9330eb46..5d02d1689d133750034095cd3781fea997064a50 100644 --- a/lso/environment.py +++ b/lso/environment.py @@ -7,16 +7,22 @@ import os LOGGING_DEFAULT_CONFIG = { "version": 1, "disable_existing_loggers": False, - "formatters": {"simple": {"format": "%(asctime)s - %(name)s " "(%(lineno)d) - %(levelname)s - %(message)s"}}, + "formatters": {"simple": {"format": "%(asctime)s - %(name)s (%(lineno)d) - %(levelname)s - %(message)s"}}, "handlers": { "console": { "class": "logging.StreamHandler", "level": "DEBUG", "formatter": "simple", "stream": "ext://sys.stdout", - } + }, + }, + "loggers": { + "resource_management": { + "level": "DEBUG", + "handlers": ["console"], + "propagate": False, + }, }, - "loggers": {"resource_management": {"level": "DEBUG", "handlers": ["console"], "propagate": False}}, "root": {"level": "INFO", "handlers": ["console"]}, } diff --git a/lso/playbook.py b/lso/playbook.py index 9650985340496fd7e166e8e9dea740f8b2ba6bbb..d04737cbbd5041a3fda24c88fc686927aaf8813d 100644 --- a/lso/playbook.py +++ b/lso/playbook.py @@ -1,4 +1,5 @@ """Module that gathers common API responses and data models.""" + import enum import json import logging @@ -137,7 +138,13 @@ def _process_json_output(runner: ansible_runner.Runner) -> list[dict[Any, Any]]: return parsed_output -def _run_playbook_proc(job_id: str, playbook_path: str, extra_vars: dict, inventory: list[str], callback: str) -> None: +def _run_playbook_proc( + job_id: str, + playbook_path: str, + extra_vars: dict, + inventory: list[str], + callback: str, +) -> None: """Run a playbook, internal function. :param str job_id: Identifier of the job that's executed. @@ -177,7 +184,6 @@ def run_playbook(playbook_path: str, extra_vars: dict, inventory: str, callback: :return: Result of playbook launch, this could either be successful or unsuccessful. :rtype: :class:`PlaybookLaunchResponse` """ - job_id = str(uuid.uuid4()) thread = threading.Thread( target=_run_playbook_proc, diff --git a/lso/routes/__init__.py b/lso/routes/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..6df1dee1f6483ed379ac3d7f753f8b8a654b3928 100644 --- a/lso/routes/__init__.py +++ b/lso/routes/__init__.py @@ -0,0 +1 @@ +"""Module of all routes that are available in LSO.""" diff --git a/lso/routes/default.py b/lso/routes/default.py index 2d7ddb158c2131140234fc672a3d0fffedee4457..d6f7292b47ab2eb44bd89e6addcdc8a412648146 100644 --- a/lso/routes/default.py +++ b/lso/routes/default.py @@ -2,6 +2,7 @@ For now only includes a single endpoint that responds with the current version of the API and LSO. """ + from importlib import metadata from fastapi import APIRouter diff --git a/lso/routes/ip_trunk.py b/lso/routes/ip_trunk.py index 3531cdc8d627c08690072a8afe74add3538fb890..086ec21ceb42055ab72ac8f0f4b56affd450cf2a 100644 --- a/lso/routes/ip_trunk.py +++ b/lso/routes/ip_trunk.py @@ -28,7 +28,7 @@ class IPTrunkProvisioningParams(IPTrunkParams): #: also making it an optional parameter. dry_run: bool | None = True #: The type of object that is changed. - object: str + object: str # noqa: A003 class IPTrunkModifyParams(IPTrunkParams): diff --git a/pyproject.toml b/pyproject.toml index 99627a66183656a1fbb927f932c5c07c4a1f1b54..4be30a8e9a06f11a435b2b94df7eede70816d420 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,29 +1,7 @@ -[tool.isort] -profile = "black" -line_length = 120 -skip = ["venv", ".tox", "docs"] -known_third_party = ["pydantic", "migrations"] -known_first_party = ["test", "docs"] - -[tool.black] -line-length = 120 -target-version = ["py310"] -exclude = ''' -( - /( - geant_service_orchestrator\.egg-info # exclude a few common directories in the - | \.git # root of the project - | \.*_cache - | \.tox - | venv - | docs - )/ -) -''' - [tool.mypy] exclude = [ "venv", + "test/*", "docs" ] ignore_missing_imports = true @@ -44,64 +22,81 @@ show_error_codes = true show_column_numbers = true # Suppress "note: By default the bodies of untyped functions are not checked" disable_error_code = "annotation-unchecked" +# Forbid the use of a generic "type: ignore" without specifying the exact error that is ignored +enable_error_code = "ignore-without-code" [tool.ruff] -exclude = [ - ".git", - ".*_cache", - ".tox", - "*.egg-info", - "__pycache__", +extend-exclude = [ "htmlcov", - "venv", - "docs" + "docs", ] ignore = [ - "C417", - "D100", - "D101", - "D102", - "D103", - "D104", - "D105", - "D106", - "D107", - "D202", "D203", "D213", - "E501", - "N806", - "B905", "N805", - "B904", - "N803", - "N801", - "N815", - "N802", - "S101", - "S104" + "PLR0913", + "PLR0904", + "PLW1514" ] line-length = 120 select = [ + "A", + "ARG", "B", + "BLE", "C", + "COM", + "C4", + "C90", "D", + "DTZ", "E", + "EM", + "ERA", "F", + "FA", + "FBT", + "FLY", + "FURB", + "G", "I", + "ICN", + "INP", + "ISC", + "LOG", "N", + "PERF", + "PGH", + "PIE", + "PL", + "PT", + "PTH", + "PYI", + "Q", + "RET", + "R", "RET", + "RSE", + "RUF", "S", + "SIM", + "SLF", "T", + "T20", + "TID", + "TRY", + "UP", "W", + "YTT" ] -target-version = "py310" +target-version = "py311" [tool.ruff.flake8-tidy-imports] ban-relative-imports = "all" [tool.ruff.per-file-ignores] -"test/*" = ["B033", "N816", "N802"] +"test/*" = ["D", "S101", "PLR2004"] +"setup.py" = ["D100"] [tool.ruff.isort] known-third-party = ["pydantic", "migrations"] diff --git a/requirements.txt b/requirements.txt index ce50fc8a1259acf7d6bc9ddfc4f05c4b2fc26d3f..6b03e3182ba74825e1adac2d44843412dc839111 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,9 +25,6 @@ sphinx~=7.2.6 sphinx-rtd-theme~=1.3.0 requests~=2.31.0 docutils~=0.18.1 -isort~=5.12.0 -black~=23.11.0 -flake8~=6.1.0 mypy~=1.7.0 ruff~=0.1.6 types-setuptools~=68.2.0.1 diff --git a/setup.py b/setup.py index 24315c22f4fc64bd2ade4886cdffe2f61f5f8910..72087cc503ed509714333f6380ddef0e92347f79 100644 --- a/setup.py +++ b/setup.py @@ -4,9 +4,9 @@ setup( name="goat-lso", version="0.1", author="GÉANT Orchestration & Automation Team", - author_email="TBD", + author_email="goat@geant.org", description="Lightweight Service Orchestrator", - url="https://gitlab.geant.org/goat/gap/lso", + url="https://gitlab.software.geant.org/goat/gap/lso", packages=find_packages(), install_requires=[ "jsonschema~=4.18.0", @@ -34,11 +34,10 @@ setup( license_files=("LICENSE.txt",), classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Development Status :: 2 - Pre-Alpha", ], - python_requires=">=3.10", + python_requires=">=3.11", ) diff --git a/test/conftest.py b/test/conftest.py index 2e3586910cb413d536185eb6e0dde86a70186157..cb4725e2ae2f4665bd72dc220cb46c3390d034a1 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,8 +1,9 @@ import json import os import tempfile +from collections.abc import Callable, Generator from io import StringIO -from typing import Any, Callable, Generator +from typing import Any import pytest from faker import Faker @@ -11,7 +12,7 @@ from fastapi.testclient import TestClient import lso -@pytest.fixture +@pytest.fixture() def mocked_ansible_runner_run() -> Callable: class Runner: def __init__(self) -> None: @@ -19,7 +20,7 @@ def mocked_ansible_runner_run() -> Callable: self.rc = 0 self.stdout = StringIO("[{'step one': 'results'}, {'step two': 2}]") - def run(*args: Any, **kwargs: Any) -> Runner: + def run(*args: Any, **kwargs: Any) -> Runner: # noqa: ARG001 return Runner() return run @@ -44,11 +45,11 @@ def data_config_filename(configuration_data: dict[str, str]) -> Generator[str, A @pytest.fixture(scope="session") -def client(data_config_filename: str) -> Generator[TestClient, Any, None]: +def client(data_config_filename: str) -> TestClient: """Return a client that can be used to test the server.""" os.environ["SETTINGS_FILENAME"] = data_config_filename app = lso.create_app() - yield TestClient(app) # wait here until calling context ends + return TestClient(app) # wait here until calling context ends @pytest.fixture(scope="session") diff --git a/test/routes/test_ip_trunk.py b/test/routes/test_ip_trunk.py index 21f74cae880a5aea2b40aedd381a4b4281094a83..9b931bba283e6033817d0acdf07b18481d7b7598 100644 --- a/test/routes/test_ip_trunk.py +++ b/test/routes/test_ip_trunk.py @@ -1,5 +1,5 @@ import time -from typing import Callable +from collections.abc import Callable from unittest.mock import patch import jsonschema @@ -66,7 +66,10 @@ def subscription_object(faker: Faker) -> dict: "iptrunk_side_ae_members": ["ge-0/0/0", "ge-0/0/1"], "subscription_instance_id": faker.uuid4(), "iptrunk_side_ae_geant_a_sid": "SID-11112", - "iptrunk_side_ae_members_description": [faker.pystr(), faker.pystr()], + "iptrunk_side_ae_members_description": [ + faker.pystr(), + faker.pystr(), + ], }, { "name": "IptrunkSideBlock", @@ -110,7 +113,10 @@ def subscription_object(faker: Faker) -> dict: "iptrunk_side_ae_members": ["ge-0/0/0", "ge-0/0/1"], "subscription_instance_id": faker.uuid4(), "iptrunk_side_ae_geant_a_sid": "SID-11112", - "iptrunk_side_ae_members_description": [faker.pystr(), faker.pystr()], + "iptrunk_side_ae_members_description": [ + faker.pystr(), + faker.pystr(), + ], }, ], }, @@ -160,7 +166,7 @@ def migration_object(faker: Faker) -> dict: @responses.activate def test_ip_trunk_provisioning( - client: TestClient, subscription_object: dict, mocked_ansible_runner_run: Callable + client: TestClient, subscription_object: dict, mocked_ansible_runner_run: Callable, ) -> None: responses.post(url=TEST_CALLBACK_URL, status=200) @@ -189,7 +195,7 @@ def test_ip_trunk_provisioning( @responses.activate def test_ip_trunk_modification( - client: TestClient, subscription_object: dict, mocked_ansible_runner_run: Callable + client: TestClient, subscription_object: dict, mocked_ansible_runner_run: Callable, ) -> None: responses.post(url=TEST_CALLBACK_URL, status=200) @@ -244,7 +250,10 @@ def test_ip_trunk_deletion(client: TestClient, subscription_object: dict, mocked @responses.activate def test_ip_trunk_migration( - client: TestClient, subscription_object: dict, migration_object: dict, mocked_ansible_runner_run: Callable + client: TestClient, + subscription_object: dict, + migration_object: dict, + mocked_ansible_runner_run: Callable, ) -> None: responses.post(url=TEST_CALLBACK_URL, status=204) diff --git a/test/routes/test_router.py b/test/routes/test_router.py index 6a9f2fffa21e847a1e3294f1658ce312a6fe3a10..f0706b66c0cef72bd942e6bd5b4ae08cbb08cbc8 100644 --- a/test/routes/test_router.py +++ b/test/routes/test_router.py @@ -1,5 +1,5 @@ import time -from typing import Callable +from collections.abc import Callable from unittest.mock import patch import jsonschema @@ -31,7 +31,10 @@ def test_router_provisioning(client: TestClient, faker: Faker, mocked_ansible_ru "lo_iso_address": "1.2.3.4.5.6", "snmp_location": "city,country[1.2,3.4]", "si_ipv4_network": faker.ipv4() + "/24", - "ias_lt_network": {"v4": faker.ipv4() + "/24", "v6": faker.ipv6() + "/64"}, + "ias_lt_network": { + "v4": faker.ipv4() + "/24", + "v6": faker.ipv6() + "/64", + }, "site_country_code": faker.country_code(), "site_city": faker.city(), "site_latitude": float(faker.latitude()), diff --git a/test/test_config.py b/test/test_config.py index bb724c7f74638f079491d895675674676454b6e2..c1d54d8a34be8639386a6ade376564b9961131fb 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -1,4 +1,5 @@ """Set of tests that verify correct config is accepted and incorrect config is not.""" + import io import json import os @@ -20,7 +21,12 @@ def test_validate_testenv_config(data_config_filename: str) -> None: @pytest.mark.parametrize( - "bad_config", [{"name": "bad version", "version": 123}, {"name": "missing version"}, {"version": "missing name"}] + "bad_config", + [ + {"name": "bad version", "version": 123}, + {"name": "missing version"}, + {"version": "missing name"}, + ], ) def test_bad_config(bad_config: dict) -> None: with io.StringIO(json.dumps(bad_config)) as file: diff --git a/tox.ini b/tox.ini index 927b8974a48b33a3031b4b28dce6c342837cf1eb..3468ba6afcc02e1acc5fa1a67ca29078a42d2fc9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,6 @@ [tox] envlist = py311 -[flake8] -ignore = W503 -exclude = .git,.*_cache,.eggs,*.egg-info,__pycache__,venv,.tox,docs -enable-extensions = G -select = B,C,D,E,F,G,I,N,S,T,W,B902,B903,R -max-line-length = 120 -ban-relative-imports = true -per-file-ignores = - # Allow first argument to be cls instead of self for pydantic validators - gso/*: B902 - [testenv] passenv = XDG_CACHE_HOME,USE_COMPOSE setenv = @@ -21,11 +10,9 @@ deps = -r requirements.txt commands = - isort -c . - ruff . - black --check . + ruff --respect-gitignore --preview . + ruff format --respect-gitignore --preview --check . mypy . - flake8 coverage erase coverage run --source lso -m pytest coverage xml