From f4c8e1062bd26352c3a66f086d47b25b221593be Mon Sep 17 00:00:00 2001
From: Karel van Klink <karel.vanklink@geant.org>
Date: Tue, 21 Nov 2023 17:41:31 +0100
Subject: [PATCH] rework pipeline to use ruff instead of flake, isort, and
 black

---
 .pylintrc                    |   9 ----
 docs/dump-openapi-spec.py    |  16 +++---
 docs/source/conf.py          |  47 ++++++++--------
 lso/__init__.py              |   2 +-
 lso/app.py                   |   1 +
 lso/environment.py           |  12 +++--
 lso/playbook.py              |  10 +++-
 lso/routes/__init__.py       |   1 +
 lso/routes/default.py        |   1 +
 lso/routes/ip_trunk.py       |   2 +-
 pyproject.toml               | 101 +++++++++++++++++------------------
 requirements.txt             |   3 --
 setup.py                     |   7 ++-
 test/conftest.py             |  11 ++--
 test/routes/test_ip_trunk.py |  21 +++++---
 test/routes/test_router.py   |   7 ++-
 test/test_config.py          |   8 ++-
 tox.ini                      |  17 +-----
 18 files changed, 138 insertions(+), 138 deletions(-)
 delete mode 100644 .pylintrc

diff --git a/.pylintrc b/.pylintrc
deleted file mode 100644
index d5b8313..0000000
--- 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 944838a..8e35e71 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 645bdfe..1be1927 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 1faa2be..03ff9e5 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 1c28f76..8ca6862 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 bd547e7..5d02d16 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 9650985..d04737c 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 e69de29..6df1dee 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 2d7ddb1..d6f7292 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 3531cdc..086ec21 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 99627a6..4be30a8 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 ce50fc8..6b03e31 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 24315c2..72087cc 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 2e35869..cb4725e 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 21f74ca..9b931bb 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 6a9f2ff..f0706b6 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 bb724c7..c1d54d8 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 927b897..3468ba6 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
-- 
GitLab