From 811b45f8636ccae13030282e2b7cbf7c5cf351ca Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 8 Oct 2026 15:14:34 +0200 Subject: [PATCH 1/9] fix: drop Python 3.10 and add Python 3.15 support --- .github/workflows/testing.yml | 2 +- gns3server/agent/web_wireshark/docker_client.py | 5 +---- gns3server/compute/base_node.py | 12 ++---------- gns3server/controller/compute.py | 5 +---- gns3server/server.py | 6 +++--- pyproject.toml | 9 +++++---- 6 files changed, 13 insertions(+), 26 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 42916ecf5..f09857260 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] steps: - uses: actions/checkout@v7 diff --git a/gns3server/agent/web_wireshark/docker_client.py b/gns3server/agent/web_wireshark/docker_client.py index 00f62edd2..3fcbf0368 100644 --- a/gns3server/agent/web_wireshark/docker_client.py +++ b/gns3server/agent/web_wireshark/docker_client.py @@ -28,11 +28,8 @@ from typing import Any import aiohttp -if sys.version_info >= (3, 11): - from asyncio import timeout as async_timeout -else: - from async_timeout import timeout as async_timeout +from asyncio import timeout as async_timeout from gns3server.utils import parse_version logger = logging.getLogger(__name__) diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 033ef96d9..13458aef4 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -671,11 +671,7 @@ class BaseNode: ) # keep forwarding websocket data in both direction - if sys.version_info >= (3, 11, 0): - # Starting with Python 3.11, passing coroutine objects to wait() directly is forbidden. - aws = [asyncio.create_task(ws_forward(telnet_writer)), asyncio.create_task(telnet_forward(telnet_reader))] - else: - aws = [ws_forward(telnet_writer), telnet_forward(telnet_reader)] + aws = [asyncio.create_task(ws_forward(telnet_writer)), asyncio.create_task(telnet_forward(telnet_reader))] done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED) for task in done: @@ -762,11 +758,7 @@ class BaseNode: log.warning(f"Exception while forwarding VNC data to WebSocket: {e!r}") # Keep forwarding WebSocket data in both directions - if sys.version_info >= (3, 11, 0): - # Starting with Python 3.11, passing coroutine objects to wait() directly is forbidden. - aws = [asyncio.create_task(ws_forward(vnc_writer)), asyncio.create_task(vnc_forward(vnc_reader))] - else: - aws = [ws_forward(vnc_writer), vnc_forward(vnc_reader)] + aws = [asyncio.create_task(ws_forward(vnc_writer)), asyncio.create_task(vnc_forward(vnc_reader))] done, pending = await asyncio.wait(aws, return_when=asyncio.FIRST_COMPLETED) for task in done: diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index 1b3b65e38..2b4658c37 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -25,10 +25,7 @@ import sys import aiohttp from fastapi import HTTPException -if sys.version_info >= (3, 11): - from asyncio import timeout as asynctimeout -else: - from async_timeout import timeout as asynctimeout +from asyncio import timeout as asynctimeout import logging diff --git a/gns3server/server.py b/gns3server/server.py index 3ec16e456..099696f55 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -255,9 +255,9 @@ class Server: else: log.info(f"Compute authentication is enabled with username '{config.Server.compute_username}'") - # we only support Python 3 version >= 3.9 - if sys.version_info < (3, 9, 0): - raise SystemExit("Python 3.9 or higher is required") + # we only support Python 3 version >= 3.11 + if sys.version_info < (3, 11, 0): + raise SystemExit("Python 3.11 or higher is required") log.info( f"Running with Python {sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} and has PID {os.getpid()}" diff --git a/pyproject.toml b/pyproject.toml index ef7b82573..eda392946 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta" [project] name = "gns3-server" -description = "GNS3 graphical interface for the GNS3 server." +description = "GNS3 server (controller and compute) for GNS3 network simulator" license-files = ["LICENSE"] authors = [ { name = "Jeremy Grossmann", email = "developers@gns3.com" } ] readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", @@ -21,10 +21,11 @@ classifiers = [ "Natural Language :: English", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Implementation :: CPython" ] @@ -58,7 +59,7 @@ packages = ["gns3server"] [tool.ruff] -target-version = "py310" +target-version = "py311" line-length = 120 extend-exclude = [ "*.md", From 90a98e7a45474f44862767c93ce815ae0afd93f5 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 8 Oct 2026 15:16:58 +0200 Subject: [PATCH 2/9] Python 3.15 is not yet available on GA --- .github/workflows/testing.yml | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index f09857260..4f834576e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: - python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] + python-version: ["3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v7 diff --git a/pyproject.toml b/pyproject.toml index eda392946..f742b0c7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", - "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Implementation :: CPython" ] From 330741598a24da97a6b4837f4becd69d373f666d Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 8 Oct 2026 15:25:32 +0200 Subject: [PATCH 3/9] Set target-version = "py310" in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f742b0c7d..3fa83a397 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ packages = ["gns3server"] [tool.ruff] -target-version = "py311" +target-version = "py310" line-length = 120 extend-exclude = [ "*.md", From 059056e97f5721598ece75bb1cac7cb8475a900d Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 8 Oct 2026 15:27:53 +0200 Subject: [PATCH 4/9] Revert "Set target-version = "py310" in pyproject.toml" This reverts commit 330741598a24da97a6b4837f4becd69d373f666d. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3fa83a397..f742b0c7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ packages = ["gns3server"] [tool.ruff] -target-version = "py310" +target-version = "py311" line-length = 120 extend-exclude = [ "*.md", From 2642345b0a2ecb96fc190fc91b81fb81a7c7bafe Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 8 Oct 2026 15:33:53 +0200 Subject: [PATCH 5/9] fix: import errors --- gns3server/agent/web_wireshark/docker_client.py | 4 +--- gns3server/controller/compute.py | 6 ++---- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/gns3server/agent/web_wireshark/docker_client.py b/gns3server/agent/web_wireshark/docker_client.py index 3fcbf0368..e030eab35 100644 --- a/gns3server/agent/web_wireshark/docker_client.py +++ b/gns3server/agent/web_wireshark/docker_client.py @@ -23,13 +23,11 @@ This module handles Docker API operations via aiohttp Unix socket. import asyncio import logging -import sys +from asyncio import timeout as async_timeout from typing import Any import aiohttp - -from asyncio import timeout as async_timeout from gns3server.utils import parse_version logger = logging.getLogger(__name__) diff --git a/gns3server/controller/compute.py b/gns3server/controller/compute.py index 2b4658c37..fd136eb91 100644 --- a/gns3server/controller/compute.py +++ b/gns3server/controller/compute.py @@ -19,16 +19,14 @@ import asyncio import io import ipaddress import json +import logging import socket import sys +from asyncio import timeout as asynctimeout import aiohttp from fastapi import HTTPException -from asyncio import timeout as asynctimeout - -import logging - from ..controller.controller_error import ( ComputeConflictError, ComputeError, diff --git a/pyproject.toml b/pyproject.toml index f742b0c7d..3fa83a397 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ packages = ["gns3server"] [tool.ruff] -target-version = "py311" +target-version = "py310" line-length = 120 extend-exclude = [ "*.md", From 917b19d808a4ac8aaab5ea4aef0e6ba18d4649ec Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 8 Oct 2026 15:40:29 +0200 Subject: [PATCH 6/9] chore(deps): update dependencies --- requirements.txt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2094cab64..67fc621d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,18 @@ # GNS3 Server Core Dependencies uvicorn==0.54.0 pydantic==2.13.5 -fastapi==0.141.1 +fastapi==0.142.4 python-multipart==0.0.32 -websockets==16.1.1 # version 16.1.1 is the last version that supports Python 3.10 -aiohttp>=3.14.3,<3.15 +websockets==17.2 +aiohttp>=3.14.4,<3.15 aiofiles>=25.1.0,<26.0 Jinja2>=3.1.6,<3.2 sentry-sdk>=2.70.0,<3 # optional dependency psutil>=7.2.2 -async-timeout>=5.0.1,<5.1; python_version < '3.11' # this library has effectively been upstreamed into Python 3.11+ distro>=1.9.0 py-cpuinfo>=9.0.0,<10.0 greenlet==3.5.3; python_version >= '3.13' # necessary to run sqlalchemy on Python >= 3.13 -sqlalchemy==2.0.54 # version 2.0.54 is the last version that supports Python 3.10 +sqlalchemy==2.1.4 aiosqlite==0.22.1 alembic==1.20 bcrypt==5.0.0 @@ -22,7 +21,7 @@ email-validator==2.3.0 watchdog==6.0.0 zstandard==0.25.0 platformdirs>=2.4.0 # fastmcp-slim >=3.4 requires >=4.0.0; upper bound removed for compatibility -truststore>=0.10.4; python_version >= '3.10' +truststore>=0.10.4 # Shared dependencies (also used by AI Copilot) telnetlib3==5.0.1,<6 From d8118fa35507bfaec263ab1def09c079fe6f4a62 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 8 Oct 2026 17:08:17 +0200 Subject: [PATCH 7/9] chore(deps): downgrade websockets to v16.1.1 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 67fc621d1..940536cc1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ uvicorn==0.54.0 pydantic==2.13.5 fastapi==0.142.4 python-multipart==0.0.32 -websockets==17.2 +websockets==16.1.1 # google-genai 2.10.0 has requirement websockets<17.0 aiohttp>=3.14.4,<3.15 aiofiles>=25.1.0,<26.0 Jinja2>=3.1.6,<3.2 From e0260f27c75bc708daf53ca4c05eaf569c67383a Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 8 Oct 2026 18:39:20 +0200 Subject: [PATCH 8/9] fix(rbac): annotate query variables with Select type for better type checking --- gns3server/db/repositories/rbac.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gns3server/db/repositories/rbac.py b/gns3server/db/repositories/rbac.py index 4417e901d..93cdbe32a 100644 --- a/gns3server/db/repositories/rbac.py +++ b/gns3server/db/repositories/rbac.py @@ -16,11 +16,11 @@ # along with this program. If not, see . import logging -from typing import List, Optional, Union, cast +from typing import Any, List, Optional, Union, cast from urllib.parse import urlparse from uuid import UUID -from sqlalchemy import delete, select, update +from sqlalchemy import Select, delete, select, update from sqlalchemy.engine import CursorResult from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -331,7 +331,7 @@ class RbacRepository(BaseRepository): Retrieve all user ACEs matching the user_id and privilege name. """ - query = ( + query: Select[Any, Any, Any, Any] = ( select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name) .join(models.Privilege.roles) .join(models.Role.acl_entries) @@ -349,7 +349,7 @@ class RbacRepository(BaseRepository): Retrieve all group ACEs matching the user_id and privilege name. """ - query = ( + query: Select[Any, Any, Any, Any] = ( select(models.ACE.path, models.ACE.propagate, models.ACE.allowed, models.Privilege.name) .join(models.Privilege.roles) .join(models.Role.acl_entries) From d1d4e7dce5cd61e099c7ed5864a680dc85027f86 Mon Sep 17 00:00:00 2001 From: grossmj Date: Thu, 8 Oct 2026 18:45:04 +0200 Subject: [PATCH 9/9] fix(deps): install the SQLAlchemy asyncio module so the 'greenlet' library is installed --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 940536cc1..c874bfbb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,8 +11,7 @@ sentry-sdk>=2.70.0,<3 # optional dependency psutil>=7.2.2 distro>=1.9.0 py-cpuinfo>=9.0.0,<10.0 -greenlet==3.5.3; python_version >= '3.13' # necessary to run sqlalchemy on Python >= 3.13 -sqlalchemy==2.1.4 +sqlalchemy[asyncio]==2.1.4 aiosqlite==0.22.1 alembic==1.20 bcrypt==5.0.0