From 9cd4160e1be8cc333d0dcc39fd42280f08806aac Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Thu, 17 Apr 2025 16:29:55 +0200
Subject: [PATCH 1/7] Add configuration file to the project

---
 mapping_provider/config.py | 43 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 43 insertions(+)
 create mode 100644 mapping_provider/config.py

diff --git a/mapping_provider/config.py b/mapping_provider/config.py
new file mode 100644
index 0000000..978b201
--- /dev/null
+++ b/mapping_provider/config.py
@@ -0,0 +1,43 @@
+"""Configuration file for the Mapping Provider."""
+
+import json
+
+import jsonschema
+
+CONFIG_SCHEMA = {
+    '$schema': 'http://json-schema.org/draft-07/schema#',
+    'type': 'object',
+    'properties': {
+        'aai': {
+            'type': 'object',
+            'properties': {
+                'discovery_endpoint_url': {'type': 'string'},
+                'client_id': {'type': 'string'},
+                'secret': {'type': 'string'},
+            },
+            'required': ['client_id', 'secret', 'discovery_endpoint_url'],
+            'additionalProperties': False
+        },
+        'orchestrator': {
+            'type': 'object',
+            'properties': {
+                'url': {'type': 'string'},
+            },
+            'required': ['url'],
+            'additionalProperties': False
+        },
+    },
+    'required': ['aai', 'orchestrator'],
+    'additionalProperties': False
+}
+
+
+def load(f):
+    """loads, validates and returns configuration parameters.
+
+    :param f: file-like object that produces the config file
+    :return:
+    """
+    config = json.loads(f.read())
+    jsonschema.validate(config, CONFIG_SCHEMA)
+    return config
-- 
GitLab


From 7f35eef10ff247940bdb7d3c4dd12fbe59206cdf Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Fri, 18 Apr 2025 11:25:04 +0200
Subject: [PATCH 2/7] Load configuration file in app.config

---
 mapping_provider/__init__.py | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/mapping_provider/__init__.py b/mapping_provider/__init__.py
index aa309f9..32f6920 100644
--- a/mapping_provider/__init__.py
+++ b/mapping_provider/__init__.py
@@ -1,7 +1,9 @@
 """Initializes the FastAPI application."""
+import os
 
 from fastapi import FastAPI
 
+from mapping_provider import config
 from mapping_provider.api.common import router as version_router
 
 
@@ -11,5 +13,10 @@ def create_app() -> FastAPI:
         title="Mapping provider",
         description="Mapping provider endpoints for GÉANT maps",
     )
+
+    config_file = os.environ.get("CONFIG_FILE_NAME", "config.json")
+    with config_file.open() as f:
+        app.state.config = config.load(f)
+
     app.include_router(version_router)
     return app
-- 
GitLab


From 320c9f0caa19f6fe1ce61c23754f8ca2b3ac2e31 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Mon, 28 Apr 2025 13:35:12 +0200
Subject: [PATCH 3/7] Restructure FAST API app and load configuration into app

---
 mapping_provider/__init__.py     | 16 ++++++++--------
 mapping_provider/dependencies.py | 20 ++++++++++++++++++++
 mapping_provider/main.py         |  6 ------
 3 files changed, 28 insertions(+), 14 deletions(-)
 create mode 100644 mapping_provider/dependencies.py
 delete mode 100644 mapping_provider/main.py

diff --git a/mapping_provider/__init__.py b/mapping_provider/__init__.py
index 32f6920..2fdc7a3 100644
--- a/mapping_provider/__init__.py
+++ b/mapping_provider/__init__.py
@@ -1,11 +1,9 @@
-"""Initializes the FastAPI application."""
-import os
+"""Create a FastAPI application for the mapping provider."""
 
 from fastapi import FastAPI
 
-from mapping_provider import config
 from mapping_provider.api.common import router as version_router
-
+from mapping_provider.api.network_graph import router as graph_router
 
 def create_app() -> FastAPI:
     """Create a FastAPI application."""
@@ -14,9 +12,11 @@ def create_app() -> FastAPI:
         description="Mapping provider endpoints for GÉANT maps",
     )
 
-    config_file = os.environ.get("CONFIG_FILE_NAME", "config.json")
-    with config_file.open() as f:
-        app.state.config = config.load(f)
+    # Force configuration to be loaded at startup to avoid issues with missing config
+    from mapping_provider.dependencies import load_config
+    _ = load_config()
 
     app.include_router(version_router)
-    return app
+    app.include_router(graph_router)
+
+    return app
\ No newline at end of file
diff --git a/mapping_provider/dependencies.py b/mapping_provider/dependencies.py
new file mode 100644
index 0000000..a776e79
--- /dev/null
+++ b/mapping_provider/dependencies.py
@@ -0,0 +1,20 @@
+"""FastAPI project dependencies."""
+
+import os
+from functools import lru_cache
+from typing import Annotated
+
+from fastapi import Depends
+from mapping_provider import config
+
+
+@lru_cache()
+def load_config() -> dict:
+    """Load and cache the application configuration."""
+    config_file = os.environ.get("CONFIG_FILE_NAME", "config.json")
+    with open(config_file) as f:
+        return config.load(f)
+
+
+# Dependency for injecting config into routes/services
+config_dep = Annotated[dict, Depends(load_config)]
\ No newline at end of file
diff --git a/mapping_provider/main.py b/mapping_provider/main.py
deleted file mode 100644
index dfcd3b4..0000000
--- a/mapping_provider/main.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""The main module that runs the application."""
-
-from mapping_provider import create_app
-
-app = create_app()
-
-- 
GitLab


From c41975a4297f73756e3368c4d476dabb2080fbe5 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Mon, 28 Apr 2025 13:36:02 +0200
Subject: [PATCH 4/7] Create app.py

---
 mapping_provider/app.py | 9 +++++++++
 1 file changed, 9 insertions(+)
 create mode 100644 mapping_provider/app.py

diff --git a/mapping_provider/app.py b/mapping_provider/app.py
new file mode 100644
index 0000000..3889cb0
--- /dev/null
+++ b/mapping_provider/app.py
@@ -0,0 +1,9 @@
+"""The main module that runs the application."""
+
+import uvicorn
+from mapping_provider import create_app
+
+app = create_app()
+
+if __name__ == "__main__":
+    uvicorn.run("mapping_provider.app:app", host="127.0.0.1", port=8000, reload=True)
\ No newline at end of file
-- 
GitLab


From 9c801a1be220b1230a6a64e01523e859846d1767 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Mon, 28 Apr 2025 13:36:56 +0200
Subject: [PATCH 5/7] Add network-graph endpoint

---
 mapping_provider/api/network_graph.py | 13 +++++++++++++
 1 file changed, 13 insertions(+)
 create mode 100644 mapping_provider/api/network_graph.py

diff --git a/mapping_provider/api/network_graph.py b/mapping_provider/api/network_graph.py
new file mode 100644
index 0000000..616bef7
--- /dev/null
+++ b/mapping_provider/api/network_graph.py
@@ -0,0 +1,13 @@
+from fastapi import APIRouter
+
+from mapping_provider.dependencies import config_dep
+from mapping_provider.services.gap import load_routers, load_trunks
+
+router = APIRouter()
+
+
+@router.get("/network-graph")
+def get_network_graph(config: config_dep):
+    routers = load_routers(config)
+    trunks = load_trunks(config)
+    return {"routers": routers, "trunks": trunks}
-- 
GitLab


From 34d60e170173c3b3e50456cb5bbda30ab792c51d Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Mon, 28 Apr 2025 15:21:54 +0200
Subject: [PATCH 6/7] Add GAP integration for getting the list of routers and
 trunks

---
 mapping_provider/api/network_graph.py |   4 +-
 mapping_provider/services/__init__.py |   1 +
 mapping_provider/services/gap.py      | 146 ++++++++++++++++++++++++++
 requirements.txt                      |   1 +
 4 files changed, 149 insertions(+), 3 deletions(-)
 create mode 100644 mapping_provider/services/__init__.py
 create mode 100644 mapping_provider/services/gap.py

diff --git a/mapping_provider/api/network_graph.py b/mapping_provider/api/network_graph.py
index 616bef7..fa968b1 100644
--- a/mapping_provider/api/network_graph.py
+++ b/mapping_provider/api/network_graph.py
@@ -8,6 +8,4 @@ router = APIRouter()
 
 @router.get("/network-graph")
 def get_network_graph(config: config_dep):
-    routers = load_routers(config)
-    trunks = load_trunks(config)
-    return {"routers": routers, "trunks": trunks}
+    return {"routers": load_routers(config), "trunks": load_trunks(config)}
diff --git a/mapping_provider/services/__init__.py b/mapping_provider/services/__init__.py
new file mode 100644
index 0000000..e2df2d7
--- /dev/null
+++ b/mapping_provider/services/__init__.py
@@ -0,0 +1 @@
+"""External services for the mapping provider."""
\ No newline at end of file
diff --git a/mapping_provider/services/gap.py b/mapping_provider/services/gap.py
new file mode 100644
index 0000000..42010fb
--- /dev/null
+++ b/mapping_provider/services/gap.py
@@ -0,0 +1,146 @@
+import logging
+from collections.abc import Callable
+from typing import Any
+
+import requests
+
+from mapping_provider.dependencies import config_dep
+
+logger = logging.getLogger(__name__)
+
+GRANT_TYPE = "client_credentials"
+SCOPE = "openid profile email aarc"
+
+
+def get_token_endpoint(discovery_endpoint_url: str, session: requests.Session) -> str:
+    """Fetch the token endpoint URL from discovery document."""
+    response = session.get(discovery_endpoint_url)
+    response.raise_for_status()
+    return response.json()["token_endpoint"]
+
+
+def get_token(config: dict, session: requests.Session) -> str:
+    """Retrieve an access token using client credentials flow."""
+    aai_config = config["aai"]
+    token_endpoint = get_token_endpoint(aai_config["discovery_endpoint_url"], session)
+    response = session.post(
+        token_endpoint,
+        data={
+            "grant_type": GRANT_TYPE,
+            "scope": SCOPE,
+            "client_id": aai_config["client_id"],
+            "client_secret": aai_config["secret"],
+        },
+    )
+    response.raise_for_status()
+    return response.json()["access_token"]
+
+
+def make_request(query: str, token: str, config: dict, session: requests.Session) -> dict:
+    """Make a GraphQL request to the orchestrator API."""
+    api_url = f"{config['orchestrator']['url']}/api/graphql"
+    headers = {"Authorization": f"Bearer {token}"}
+    response = session.post(api_url, headers=headers, json={"query": query})
+    response.raise_for_status()
+
+    data = response.json()
+    if "errors" in data:
+        logger.error(f"GraphQL query returned errors: {data['errors']}. Query: {query}")
+        raise ValueError(f"GraphQL query errors: {data['errors']}")
+
+    return data
+
+
+def extract_router(product_block_instances: list[dict]) -> str | None:
+    """Extract router FQDNs from productBlockInstances."""
+    for instance in product_block_instances:
+        for value in instance.get("productBlockInstanceValues", []):
+            if value.get("field") == "routerFqdn" and value.get("value"):
+                return value["value"]
+    return None
+
+
+def extract_trunk(product_block_instances: list[dict]) -> list[str] | None:
+    """Extract trunks from productBlockInstances."""
+    fqdns = []
+    for instance in product_block_instances:
+        for value in instance.get("productBlockInstanceValues", []):
+            if value.get("field") == "routerFqdn" and value.get("value"):
+                fqdns.append(value["value"])
+                break
+
+    if len(fqdns) >= 2:
+        return [fqdns[0], fqdns[1]]
+    return None
+
+
+def load_inventory(
+        config: dict,
+        token: str,
+        tag: str,
+        session: requests.Session,
+        extractor: Callable[[list[dict]], Any | None],
+) -> list[Any]:
+    """
+    Generic function to load inventory items based on tag and extractor function.
+
+    The extractor receives a list of productBlockInstances and returns parsed output.
+    """
+    results = []
+    end_cursor = 0
+    has_next_page = True
+
+    while has_next_page:
+        query = f"""
+           query {{
+               subscriptions(
+                   filterBy: {{field: "status", value: "PROVISIONING|ACTIVE"}},
+                   first: 100,
+                   after: {end_cursor},
+                   query: "tag:({tag})"
+               ) {{
+                   pageInfo {{
+                       hasNextPage
+                       endCursor
+                   }}
+                   page {{
+                       subscriptionId
+                       product {{
+                           tag
+                       }}
+                       productBlockInstances {{
+                           productBlockInstanceValues
+                       }}
+                   }}
+               }}
+           }}
+           """
+
+        data = make_request(query, token, config, session)
+        page_data = data.get("data", {}).get("subscriptions", {}).get("page", [])
+        page_info = data.get("data", {}).get("subscriptions", {}).get("pageInfo", {})
+
+        for item in page_data:
+            instances = item.get("productBlockInstances", [])
+            extracted = extractor(instances)
+            if extracted:
+                results.append(extracted)
+
+        has_next_page = page_info.get("hasNextPage", False)
+        end_cursor = page_info.get("endCursor", 0)
+
+    return results
+
+
+def load_routers(config: config_dep) -> list[str]:
+    """Load routers (nodes) from orchestrator."""
+    with requests.Session() as session:
+        token = get_token(config, session)
+        return load_inventory(config, token, tag="RTR", session=session, extractor=extract_router)
+
+
+def load_trunks(config: config_dep) -> list[list[str]]:
+    """Load trunks (edges) from orchestrator."""
+    with requests.Session() as session:
+        token = get_token(config, session)
+        return load_inventory(config, token, tag="IPTRUNK", session=session, extractor=extract_trunk)
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 8cedf04..2f0ae84 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,6 @@
 fastapi
 uvicorn[standard]
+requests
 
 sphinx
 sphinx-rtd-theme
-- 
GitLab


From 4762a014eb90d0019144470b22b639c89e206baa Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Tue, 29 Apr 2025 10:53:58 +0200
Subject: [PATCH 7/7] Make ruff and mypy happy

---
 mapping_provider/__init__.py          |  4 ++-
 mapping_provider/api/network_graph.py |  3 +-
 mapping_provider/app.py               |  3 +-
 mapping_provider/config.py            | 43 ++++++++++++++-------------
 mapping_provider/dependencies.py      |  9 +++---
 mapping_provider/services/__init__.py |  2 +-
 mapping_provider/services/gap.py      | 33 ++++++++++----------
 pyproject.toml                        | 14 +++++++--
 requirements.txt                      |  2 ++
 setup.py                              |  6 ++--
 tox.ini                               | 27 +++++------------
 11 files changed, 78 insertions(+), 68 deletions(-)

diff --git a/mapping_provider/__init__.py b/mapping_provider/__init__.py
index 2fdc7a3..e21cff9 100644
--- a/mapping_provider/__init__.py
+++ b/mapping_provider/__init__.py
@@ -5,6 +5,7 @@ from fastapi import FastAPI
 from mapping_provider.api.common import router as version_router
 from mapping_provider.api.network_graph import router as graph_router
 
+
 def create_app() -> FastAPI:
     """Create a FastAPI application."""
     app = FastAPI(
@@ -14,9 +15,10 @@ def create_app() -> FastAPI:
 
     # Force configuration to be loaded at startup to avoid issues with missing config
     from mapping_provider.dependencies import load_config
+
     _ = load_config()
 
     app.include_router(version_router)
     app.include_router(graph_router)
 
-    return app
\ No newline at end of file
+    return app
diff --git a/mapping_provider/api/network_graph.py b/mapping_provider/api/network_graph.py
index fa968b1..3dcc926 100644
--- a/mapping_provider/api/network_graph.py
+++ b/mapping_provider/api/network_graph.py
@@ -7,5 +7,6 @@ router = APIRouter()
 
 
 @router.get("/network-graph")
-def get_network_graph(config: config_dep):
+def get_network_graph(config: config_dep) -> dict[str, list[str] | list[list[str]]]:
+    """Network graph endpoint."""
     return {"routers": load_routers(config), "trunks": load_trunks(config)}
diff --git a/mapping_provider/app.py b/mapping_provider/app.py
index 3889cb0..44befa2 100644
--- a/mapping_provider/app.py
+++ b/mapping_provider/app.py
@@ -1,9 +1,10 @@
 """The main module that runs the application."""
 
 import uvicorn
+
 from mapping_provider import create_app
 
 app = create_app()
 
 if __name__ == "__main__":
-    uvicorn.run("mapping_provider.app:app", host="127.0.0.1", port=8000, reload=True)
\ No newline at end of file
+    uvicorn.run("mapping_provider.app:app", host="127.0.0.1", port=8000, reload=True)
diff --git a/mapping_provider/config.py b/mapping_provider/config.py
index 978b201..181badc 100644
--- a/mapping_provider/config.py
+++ b/mapping_provider/config.py
@@ -1,43 +1,44 @@
 """Configuration file for the Mapping Provider."""
 
 import json
+from typing import Any, TextIO
 
 import jsonschema
 
 CONFIG_SCHEMA = {
-    '$schema': 'http://json-schema.org/draft-07/schema#',
-    'type': 'object',
-    'properties': {
-        'aai': {
-            'type': 'object',
-            'properties': {
-                'discovery_endpoint_url': {'type': 'string'},
-                'client_id': {'type': 'string'},
-                'secret': {'type': 'string'},
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "type": "object",
+    "properties": {
+        "aai": {
+            "type": "object",
+            "properties": {
+                "discovery_endpoint_url": {"type": "string"},
+                "client_id": {"type": "string"},
+                "secret": {"type": "string"},
             },
-            'required': ['client_id', 'secret', 'discovery_endpoint_url'],
-            'additionalProperties': False
+            "required": ["client_id", "secret", "discovery_endpoint_url"],
+            "additionalProperties": False,
         },
-        'orchestrator': {
-            'type': 'object',
-            'properties': {
-                'url': {'type': 'string'},
+        "orchestrator": {
+            "type": "object",
+            "properties": {
+                "url": {"type": "string"},
             },
-            'required': ['url'],
-            'additionalProperties': False
+            "required": ["url"],
+            "additionalProperties": False,
         },
     },
-    'required': ['aai', 'orchestrator'],
-    'additionalProperties': False
+    "required": ["aai", "orchestrator"],
+    "additionalProperties": False,
 }
 
 
-def load(f):
+def load(f: TextIO) -> dict[str, Any]:
     """loads, validates and returns configuration parameters.
 
     :param f: file-like object that produces the config file
     :return:
     """
-    config = json.loads(f.read())
+    config: dict[str, Any] = json.loads(f.read())
     jsonschema.validate(config, CONFIG_SCHEMA)
     return config
diff --git a/mapping_provider/dependencies.py b/mapping_provider/dependencies.py
index a776e79..ac00f54 100644
--- a/mapping_provider/dependencies.py
+++ b/mapping_provider/dependencies.py
@@ -2,14 +2,15 @@
 
 import os
 from functools import lru_cache
-from typing import Annotated
+from typing import Annotated, Any
 
 from fastapi import Depends
+
 from mapping_provider import config
 
 
-@lru_cache()
-def load_config() -> dict:
+@lru_cache
+def load_config() -> dict[str, Any]:
     """Load and cache the application configuration."""
     config_file = os.environ.get("CONFIG_FILE_NAME", "config.json")
     with open(config_file) as f:
@@ -17,4 +18,4 @@ def load_config() -> dict:
 
 
 # Dependency for injecting config into routes/services
-config_dep = Annotated[dict, Depends(load_config)]
\ No newline at end of file
+config_dep = Annotated[dict[str, Any], Depends(load_config)]
diff --git a/mapping_provider/services/__init__.py b/mapping_provider/services/__init__.py
index e2df2d7..450b588 100644
--- a/mapping_provider/services/__init__.py
+++ b/mapping_provider/services/__init__.py
@@ -1 +1 @@
-"""External services for the mapping provider."""
\ No newline at end of file
+"""External services for the mapping provider."""
diff --git a/mapping_provider/services/gap.py b/mapping_provider/services/gap.py
index 42010fb..4b05242 100644
--- a/mapping_provider/services/gap.py
+++ b/mapping_provider/services/gap.py
@@ -3,6 +3,7 @@ from collections.abc import Callable
 from typing import Any
 
 import requests
+from requests import Session
 
 from mapping_provider.dependencies import config_dep
 
@@ -12,14 +13,15 @@ GRANT_TYPE = "client_credentials"
 SCOPE = "openid profile email aarc"
 
 
-def get_token_endpoint(discovery_endpoint_url: str, session: requests.Session) -> str:
+def get_token_endpoint(discovery_endpoint_url: str, session: Session) -> str:
     """Fetch the token endpoint URL from discovery document."""
     response = session.get(discovery_endpoint_url)
     response.raise_for_status()
-    return response.json()["token_endpoint"]
+    data: dict[str, Any] = response.json()
+    return str(data["token_endpoint"])
 
 
-def get_token(config: dict, session: requests.Session) -> str:
+def get_token(config: dict[str, Any], session: Session) -> str:
     """Retrieve an access token using client credentials flow."""
     aai_config = config["aai"]
     token_endpoint = get_token_endpoint(aai_config["discovery_endpoint_url"], session)
@@ -33,17 +35,18 @@ def get_token(config: dict, session: requests.Session) -> str:
         },
     )
     response.raise_for_status()
-    return response.json()["access_token"]
+    data: dict[str, Any] = response.json()
+    return str(data["access_token"])
 
 
-def make_request(query: str, token: str, config: dict, session: requests.Session) -> dict:
+def make_request(query: str, token: str, config: dict[str, Any], session: Session) -> dict[Any, Any]:
     """Make a GraphQL request to the orchestrator API."""
     api_url = f"{config['orchestrator']['url']}/api/graphql"
     headers = {"Authorization": f"Bearer {token}"}
     response = session.post(api_url, headers=headers, json={"query": query})
     response.raise_for_status()
 
-    data = response.json()
+    data: dict[str, Any] = response.json()
     if "errors" in data:
         logger.error(f"GraphQL query returned errors: {data['errors']}. Query: {query}")
         raise ValueError(f"GraphQL query errors: {data['errors']}")
@@ -51,16 +54,16 @@ def make_request(query: str, token: str, config: dict, session: requests.Session
     return data
 
 
-def extract_router(product_block_instances: list[dict]) -> str | None:
+def extract_router(product_block_instances: list[dict[str, Any]]) -> str | None:
     """Extract router FQDNs from productBlockInstances."""
     for instance in product_block_instances:
         for value in instance.get("productBlockInstanceValues", []):
             if value.get("field") == "routerFqdn" and value.get("value"):
-                return value["value"]
+                return str(value["value"])
     return None
 
 
-def extract_trunk(product_block_instances: list[dict]) -> list[str] | None:
+def extract_trunk(product_block_instances: list[dict[str, Any]]) -> list[str] | None:
     """Extract trunks from productBlockInstances."""
     fqdns = []
     for instance in product_block_instances:
@@ -75,11 +78,11 @@ def extract_trunk(product_block_instances: list[dict]) -> list[str] | None:
 
 
 def load_inventory(
-        config: dict,
-        token: str,
-        tag: str,
-        session: requests.Session,
-        extractor: Callable[[list[dict]], Any | None],
+    config: dict[str, Any],
+    token: str,
+    tag: str,
+    session: Session,
+    extractor: Callable[[list[dict[str, Any]]], Any | None],
 ) -> list[Any]:
     """
     Generic function to load inventory items based on tag and extractor function.
@@ -143,4 +146,4 @@ def load_trunks(config: config_dep) -> list[list[str]]:
     """Load trunks (edges) from orchestrator."""
     with requests.Session() as session:
         token = get_token(config, session)
-        return load_inventory(config, token, tag="IPTRUNK", session=session, extractor=extract_trunk)
\ No newline at end of file
+        return load_inventory(config, token, tag="IPTRUNK", session=session, extractor=extract_trunk)
diff --git a/pyproject.toml b/pyproject.toml
index cdeac56..b3e6aa6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.ruff]
 line-length = 120
-target-version = "py313"
+target-version = "py312"
 select = ["E", "F", "I", "B", "UP", "N"]
 fixable = ["ALL"]
 exclude = ["tests", "docs", "build"]
@@ -9,4 +9,14 @@ exclude = ["tests", "docs", "build"]
 python_version = "3.13"
 strict = true
 warn_unused_ignores = true
-warn_return_any = true
\ No newline at end of file
+warn_return_any = true
+allow_untyped_decorators = true
+ignore_missing_imports = true
+exclude = [
+    "venv",
+    "test/*",
+    "docs"
+]
+[[tool.mypy.overrides]]
+module = ["requests.*"]
+ignore_missing_imports = true
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 2f0ae84..05d5101 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,8 @@
 fastapi
 uvicorn[standard]
 requests
+jsonschema
+setuptools
 
 sphinx
 sphinx-rtd-theme
diff --git a/setup.py b/setup.py
index 5e5209c..32b9b6f 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,9 @@ setup(
     python_requires=">=3.10",
     install_requires=[
         "fastapi",
-        "uvicorn[standard]"
+        "uvicorn[standard]",
+        "jsonschema",
+        "requests",
     ],
     long_description=open("README.md", encoding="utf-8").read(),
     long_description_content_type="text/markdown",
@@ -23,4 +25,4 @@ setup(
         "License :: OSI Approved :: MIT License",
         "Operating System :: OS Independent",
     ],
-)
\ No newline at end of file
+)
diff --git a/tox.ini b/tox.ini
index c34930b..23c347f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,24 +1,11 @@
 [tox]
-envlist = lint, typecheck, docs
+envlist = py313
 
-[testenv:lint]
-description = Lint code with Ruff
-deps = ruff
-commands = ruff check mapping_provider
-
-[testenv:typecheck]
-description = Type-check code with mypy
-deps = mypy
-commands = mypy mapping_provider
-
-[testenv:docs]
-description = Build docs
+[testenv]
 deps =
-    sphinx
-    sphinx-rtd-theme
-    sphinxcontrib-plantuml
-    sphinxcontrib-drawio
-    sphinxcontrib-openapi
-commands = sphinx-build -b html docs/source docs/build
-
+    -r requirements.txt
+commands =
+    ruff check --respect-gitignore --preview .
+    ruff format --respect-gitignore --preview --check .
+    mypy .
 
-- 
GitLab