From 3155024d729993d552e747a73ef19e8d2e5f42f0 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 14:26:40 +0200 Subject: [PATCH 01/29] feat: hello world FastAPI service Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index e69de29..50117c6 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -0,0 +1,14 @@ +try: + from fastapi import FastAPI +except ImportError as exc: + raise ImportError( + "The 'fastapi' package is required. Run 'pip install s2-python[fastapi]' to use this feature." + ) from exc + + +app = FastAPI() + + +@app.get("/") +async def root(): + return {"message": "Hello World"} From e28c437f4116c503bd30aa1b74bdad8251622943 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 14:31:39 +0200 Subject: [PATCH 02/29] feat: mixing AbstractAuthServer Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 8 +++++++- src/s2python/authorization/server.py | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index 50117c6..6b12ac0 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -5,8 +5,14 @@ "The 'fastapi' package is required. Run 'pip install s2-python[fastapi]' to use this feature." ) from exc +from s2python.authorization.server import AbstractAuthServer -app = FastAPI() + +class FastAPIAuthServer(AbstractAuthServer, FastAPI): + ... + + +app = FastAPIAuthServer() @app.get("/") diff --git a/src/s2python/authorization/server.py b/src/s2python/authorization/server.py index e69de29..d9e17c1 100644 --- a/src/s2python/authorization/server.py +++ b/src/s2python/authorization/server.py @@ -0,0 +1,2 @@ +class AbstractAuthServer: + pass From e0f817539e95f572cbfbcd040c11fb380db30234 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 14:43:25 +0200 Subject: [PATCH 03/29] dev: get a feel for using the mixin Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 9 ++++++--- src/s2python/authorization/server.py | 11 +++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index 6b12ac0..7868c8c 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -1,3 +1,5 @@ +import requests + try: from fastapi import FastAPI except ImportError as exc: @@ -15,6 +17,7 @@ class FastAPIAuthServer(AbstractAuthServer, FastAPI): app = FastAPIAuthServer() -@app.get("/") -async def root(): - return {"message": "Hello World"} +@app.get("requestPairing/") +async def root(request_data): + pairing_response = app.handle_pairing_request(request_data) + return pairing_response diff --git a/src/s2python/authorization/server.py b/src/s2python/authorization/server.py index d9e17c1..c756f40 100644 --- a/src/s2python/authorization/server.py +++ b/src/s2python/authorization/server.py @@ -1,2 +1,9 @@ -class AbstractAuthServer: - pass +from abc import ABC, abstractmethod +from typing import Any, Dict + + +class AbstractAuthServer(ABC): + + @abstractmethod + def handle_pairing_request(self, request_data: Dict) -> Any: + pass From e7500b508800adecd0dcd195ee3b41d3a0ae1325 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 15:00:22 +0200 Subject: [PATCH 04/29] dev: get a better feel for using the mixin Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 13 +++++++++---- src/s2python/authorization/server.py | 9 +++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index 7868c8c..2f2ab64 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -8,6 +8,7 @@ ) from exc from s2python.authorization.server import AbstractAuthServer +from s2python.generated.gen_s2_pairing import ConnectionDetails, ConnectionRequest, PairingResponse, PairingRequest class FastAPIAuthServer(AbstractAuthServer, FastAPI): @@ -17,7 +18,11 @@ class FastAPIAuthServer(AbstractAuthServer, FastAPI): app = FastAPIAuthServer() -@app.get("requestPairing/") -async def root(request_data): - pairing_response = app.handle_pairing_request(request_data) - return pairing_response +@app.post('/requestConnection', response_model=ConnectionDetails) +async def post_request_connection(body: ConnectionRequest = None) -> ConnectionDetails: + return app.handle_connection_request(body) + + +@app.post('/requestPairing', response_model=PairingResponse) +async def post_request_pairing(body: PairingRequest = None) -> PairingResponse: + return app.handle_pairing_request(body) diff --git a/src/s2python/authorization/server.py b/src/s2python/authorization/server.py index c756f40..b81a5b6 100644 --- a/src/s2python/authorization/server.py +++ b/src/s2python/authorization/server.py @@ -1,9 +1,14 @@ from abc import ABC, abstractmethod -from typing import Any, Dict + +from s2python.generated.gen_s2_pairing import ConnectionDetails, ConnectionRequest, PairingResponse, PairingRequest class AbstractAuthServer(ABC): @abstractmethod - def handle_pairing_request(self, request_data: Dict) -> Any: + def handle_pairing_request(self, request_data: PairingRequest) -> PairingResponse: + pass + + @abstractmethod + def handle_connection_request(self, request_data: ConnectionRequest) -> ConnectionDetails: pass From 591ffd50b324f52830f8502720d47c35dcaf71a6 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 15:02:25 +0200 Subject: [PATCH 05/29] dev: namespace the methods from the abstract class (no longer a pure mixin) Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index 2f2ab64..ae102e1 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -11,8 +11,11 @@ from s2python.generated.gen_s2_pairing import ConnectionDetails, ConnectionRequest, PairingResponse, PairingRequest -class FastAPIAuthServer(AbstractAuthServer, FastAPI): - ... +class FastAPIAuthServer(FastAPI): + + def __init__(self, *args, **kwargs): + self.s2 = AbstractAuthServer() + super().__init__(*args, **kwargs) app = FastAPIAuthServer() @@ -20,9 +23,9 @@ class FastAPIAuthServer(AbstractAuthServer, FastAPI): @app.post('/requestConnection', response_model=ConnectionDetails) async def post_request_connection(body: ConnectionRequest = None) -> ConnectionDetails: - return app.handle_connection_request(body) + return app.s2.handle_connection_request(body) @app.post('/requestPairing', response_model=PairingResponse) async def post_request_pairing(body: PairingRequest = None) -> PairingResponse: - return app.handle_pairing_request(body) + return app.s2.handle_pairing_request(body) From ab598a14b057adfe77542d020add1b70a6333ca7 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 15:51:17 +0200 Subject: [PATCH 06/29] dev: start test, must subclass abstract class Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 17 ++++++++++++----- src/s2python/generated/gen_s2_pairing.py | 13 +++++-------- tests/unit/fast_api_test.py | 12 ++++++++++++ 3 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 tests/unit/fast_api_test.py diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index ae102e1..235b08b 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -1,5 +1,3 @@ -import requests - try: from fastapi import FastAPI except ImportError as exc: @@ -11,14 +9,23 @@ from s2python.generated.gen_s2_pairing import ConnectionDetails, ConnectionRequest, PairingResponse, PairingRequest -class FastAPIAuthServer(FastAPI): +class FastAPIAuthServer(AbstractAuthServer): + + def handle_pairing_request(self, request_data: PairingRequest) -> PairingResponse: + return PairingResponse() + + def handle_connection_request(self, request_data: ConnectionRequest) -> ConnectionDetails: + return ConnectionDetails() + + +class MyFastAPI(FastAPI): def __init__(self, *args, **kwargs): - self.s2 = AbstractAuthServer() + self.s2 = FastAPIAuthServer() super().__init__(*args, **kwargs) -app = FastAPIAuthServer() +app = MyFastAPI() @app.post('/requestConnection', response_model=ConnectionDetails) diff --git a/src/s2python/generated/gen_s2_pairing.py b/src/s2python/generated/gen_s2_pairing.py index 32979b9..7f570eb 100644 --- a/src/s2python/generated/gen_s2_pairing.py +++ b/src/s2python/generated/gen_s2_pairing.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from datetime import datetime from enum import Enum, auto +from pydantic import BaseModel from typing import List, Optional @@ -66,8 +67,7 @@ class PairingInfo: validUntil: Optional[datetime] = None -@dataclass -class PairingRequest: +class PairingRequest(BaseModel): """Request to initiate pairing.""" token: Optional[PairingToken] = None @@ -77,8 +77,7 @@ class PairingRequest: supportedProtocols: Optional[List[Protocols]] = None -@dataclass -class PairingResponse: +class PairingResponse(BaseModel): """Response to a pairing request.""" s2ServerNodeId: Optional[uuid.UUID] = None @@ -86,16 +85,14 @@ class PairingResponse: requestConnectionUri: Optional[str] = None -@dataclass -class ConnectionRequest: +class ConnectionRequest(BaseModel): """Request to establish a connection.""" s2ClientNodeId: Optional[uuid.UUID] = None supportedProtocols: Optional[List[Protocols]] = None -@dataclass -class ConnectionDetails: +class ConnectionDetails(BaseModel): """Details for establishing a connection.""" selectedProtocol: Optional[Protocols] = None diff --git a/tests/unit/fast_api_test.py b/tests/unit/fast_api_test.py new file mode 100644 index 0000000..78392f2 --- /dev/null +++ b/tests/unit/fast_api_test.py @@ -0,0 +1,12 @@ +from fastapi.testclient import TestClient + +from s2python.authorization.fastapi_service import MyFastAPI + + +client = TestClient(MyFastAPI) + + +def test_post_pairing_request(): + response = client.post("/pairingRequest") + assert response.status_code == 200 + assert response.json() == {"msg": "Hello World"} From 6ec509d9918f59a9dd01b9244a667d9777b4f96f Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 16:19:09 +0200 Subject: [PATCH 07/29] fix: switch to using datamodel-codegen Signed-off-by: F.N. Claessen --- src/s2python/generated/gen_s2_pairing.py | 182 +++++------------------ 1 file changed, 41 insertions(+), 141 deletions(-) diff --git a/src/s2python/generated/gen_s2_pairing.py b/src/s2python/generated/gen_s2_pairing.py index 7f570eb..01130c8 100644 --- a/src/s2python/generated/gen_s2_pairing.py +++ b/src/s2python/generated/gen_s2_pairing.py @@ -1,170 +1,70 @@ -""" -Generated classes based on s2-over-ip-pairing.yaml OpenAPI schema. -This file is auto-generated and should not be modified directly. -""" - -import uuid -from dataclasses import dataclass -from datetime import datetime -from enum import Enum, auto -from pydantic import BaseModel -from typing import List, Optional - +# generated by datamodel-codegen: +# filename: s2-over-ip-pairing.yaml +# timestamp: 2025-04-15T14:19:11+00:00 -class Protocols(str, Enum): - """Supported protocol types.""" +from __future__ import annotations - WebSocketSecure = "WebSocketSecure" +from enum import Enum +from typing import List, Optional +from uuid import UUID +from pydantic import AnyUrl, AwareDatetime, BaseModel, RootModel, constr -class S2Role(str, Enum): - """Roles in the S2 protocol.""" - CEM = "CEM" - RM = "RM" +class Protocols(Enum): + WebSocketSecure = 'WebSocketSecure' -class Deployment(str, Enum): - """Deployment types.""" +class S2Role(Enum): + CEM = 'CEM' + RM = 'RM' - WAN = "WAN" - LAN = "LAN" +class Deployment(Enum): + WAN = 'WAN' + LAN = 'LAN' -@dataclass -class S2NodeDescription: - """Description of an S2 node.""" - brand: Optional[str] = None - logoUri: Optional[str] = None - type: Optional[str] = None - modelName: Optional[str] = None - userDefinedName: Optional[str] = None - role: Optional[S2Role] = None - deployment: Optional[Deployment] = None +class PairingToken(RootModel[constr(pattern=r'^[0-9a-zA-Z]{32}$')]): + root: constr(pattern=r'^[0-9a-zA-Z]{32}$') -class PairingToken(str): - """A token used for pairing. +class PairingInfo(BaseModel): + pairingUri: Optional[AnyUrl] = None + token: Optional[PairingToken] = None + validUntil: Optional[AwareDatetime] = None - Must match pattern: ^[0-9a-zA-Z]{32}$ - """ - def __new__(cls, content: str): - import re +class ConnectionRequest(BaseModel): + s2ClientNodeId: Optional[UUID] = None + supportedProtocols: Optional[List[Protocols]] = None - if not re.match(r"^[0-9a-zA-Z]{32}$", content): - raise ValueError("PairingToken must be 32 alphanumeric characters") - return super().__new__(cls, content) +class ConnectionDetails(BaseModel): + selectedProtocol: Optional[Protocols] = None + challenge: Optional[str] = None + connectionUri: Optional[AnyUrl] = None -@dataclass -class PairingInfo: - """Information about a pairing.""" - pairingUri: Optional[str] = None - token: Optional[PairingToken] = None - validUntil: Optional[datetime] = None +class S2NodeDescription(BaseModel): + brand: Optional[str] = None + logoUri: Optional[AnyUrl] = None + type: Optional[str] = None + modelName: Optional[str] = None + userDefinedName: Optional[str] = None + role: Optional[S2Role] = None + deployment: Optional[Deployment] = None class PairingRequest(BaseModel): - """Request to initiate pairing.""" - token: Optional[PairingToken] = None - publicKey: Optional[bytes] = None - s2ClientNodeId: Optional[uuid.UUID] = None + publicKey: Optional[str] = None + s2ClientNodeId: Optional[UUID] = None s2ClientNodeDescription: Optional[S2NodeDescription] = None supportedProtocols: Optional[List[Protocols]] = None class PairingResponse(BaseModel): - """Response to a pairing request.""" - - s2ServerNodeId: Optional[uuid.UUID] = None + s2ServerNodeId: Optional[UUID] = None serverNodeDescription: Optional[S2NodeDescription] = None - requestConnectionUri: Optional[str] = None - - -class ConnectionRequest(BaseModel): - """Request to establish a connection.""" - - s2ClientNodeId: Optional[uuid.UUID] = None - supportedProtocols: Optional[List[Protocols]] = None - - -class ConnectionDetails(BaseModel): - """Details for establishing a connection.""" - - selectedProtocol: Optional[Protocols] = None - challenge: Optional[bytes] = None - connectionUri: Optional[str] = None - - -# Serialization/Deserialization functions - - -def _is_dataclass_instance(obj): - """Check if an object is a dataclass instance.""" - from dataclasses import is_dataclass - - return is_dataclass(obj) and not isinstance(obj, type) - - -def to_dict(obj): - """Convert a dataclass instance to a dictionary.""" - if isinstance(obj, datetime): - return obj.isoformat() - elif isinstance(obj, uuid.UUID): - return str(obj) - elif isinstance(obj, bytes): - import base64 - - return base64.b64encode(obj).decode("ascii") - elif isinstance(obj, Enum): - return obj.value - elif isinstance(obj, list): - return [to_dict(item) for item in obj] - elif _is_dataclass_instance(obj): - result = {} - for field in obj.__dataclass_fields__: - value = getattr(obj, field) - if value is not None: - result[field] = to_dict(value) - return result - else: - return obj - - -def from_dict(cls, data): - """Create a dataclass instance from a dictionary.""" - if data is None: - return None - - if cls is datetime: - return datetime.fromisoformat(data) - elif cls is uuid.UUID: - return uuid.UUID(data) - elif cls is bytes: - import base64 - - return base64.b64decode(data.encode("ascii")) - elif issubclass(cls, Enum): - return cls(data) - elif issubclass(cls, PairingToken): - return PairingToken(data) - elif hasattr(cls, "__dataclass_fields__"): - fieldtypes = cls.__annotations__ - instance_data = {} - - for field, field_type in fieldtypes.items(): - if field in data and data[field] is not None: - # Handle List[Type] annotations - if hasattr(field_type, "__origin__") and field_type.__origin__ is list: - item_type = field_type.__args__[0] - instance_data[field] = [from_dict(item_type, item) for item in data[field]] - else: - instance_data[field] = from_dict(field_type, data[field]) - - return cls(**instance_data) - else: - return data + requestConnectionUri: Optional[AnyUrl] = None From b95cd251a011e458085574c8f03aa35b9743389e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 16:22:05 +0200 Subject: [PATCH 08/29] refactor: rename test module Signed-off-by: F.N. Claessen --- tests/unit/{fast_api_test.py => fastapi_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/{fast_api_test.py => fastapi_test.py} (100%) diff --git a/tests/unit/fast_api_test.py b/tests/unit/fastapi_test.py similarity index 100% rename from tests/unit/fast_api_test.py rename to tests/unit/fastapi_test.py From 05b0737df3169dd6e8159f04b150bf4134cb1ed6 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 16:24:37 +0200 Subject: [PATCH 09/29] fix: setup TestClient Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 2 +- tests/unit/fastapi_test.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index 235b08b..1b583af 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -21,8 +21,8 @@ def handle_connection_request(self, request_data: ConnectionRequest) -> Connecti class MyFastAPI(FastAPI): def __init__(self, *args, **kwargs): - self.s2 = FastAPIAuthServer() super().__init__(*args, **kwargs) + self.s2 = FastAPIAuthServer() app = MyFastAPI() diff --git a/tests/unit/fastapi_test.py b/tests/unit/fastapi_test.py index 78392f2..493ca79 100644 --- a/tests/unit/fastapi_test.py +++ b/tests/unit/fastapi_test.py @@ -1,9 +1,9 @@ from fastapi.testclient import TestClient -from s2python.authorization.fastapi_service import MyFastAPI +from s2python.authorization.fastapi_service import app -client = TestClient(MyFastAPI) +client = TestClient(app) def test_post_pairing_request(): From 2640f14130d027fa625920f35f883eac9947915c Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 16:25:13 +0200 Subject: [PATCH 10/29] fix: wrong URL Signed-off-by: F.N. Claessen --- tests/unit/fastapi_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/fastapi_test.py b/tests/unit/fastapi_test.py index 493ca79..3d3f327 100644 --- a/tests/unit/fastapi_test.py +++ b/tests/unit/fastapi_test.py @@ -7,6 +7,6 @@ def test_post_pairing_request(): - response = client.post("/pairingRequest") + response = client.post("/requestPairing") assert response.status_code == 200 assert response.json() == {"msg": "Hello World"} From 82214df9640a17fe1c7ab6e1c324e5161536be26 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 16:26:19 +0200 Subject: [PATCH 11/29] fix: check response for expected keys Signed-off-by: F.N. Claessen --- tests/unit/fastapi_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit/fastapi_test.py b/tests/unit/fastapi_test.py index 3d3f327..3193b94 100644 --- a/tests/unit/fastapi_test.py +++ b/tests/unit/fastapi_test.py @@ -9,4 +9,8 @@ def test_post_pairing_request(): response = client.post("/requestPairing") assert response.status_code == 200 - assert response.json() == {"msg": "Hello World"} + assert response.json() == { + "requestConnectionUri": None, + "s2ServerNodeId": None, + "serverNodeDescription": None, + } From d6a51e9c0ab6afecdeca0dfe877450f10c33e988 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 16:27:24 +0200 Subject: [PATCH 12/29] feat: add test for posting a connection request Signed-off-by: F.N. Claessen --- tests/unit/fastapi_test.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/fastapi_test.py b/tests/unit/fastapi_test.py index 3193b94..19847c6 100644 --- a/tests/unit/fastapi_test.py +++ b/tests/unit/fastapi_test.py @@ -14,3 +14,13 @@ def test_post_pairing_request(): "s2ServerNodeId": None, "serverNodeDescription": None, } + + +def test_post_connection_request(): + response = client.post("/requestConnection") + assert response.status_code == 200 + assert response.json() == { + "challenge": None, + "connectionUri": None, + "selectedProtocol": None, + } From 923e4b2bf401ef5b3fdb117684557bbd5c672972 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 16:40:44 +0200 Subject: [PATCH 13/29] docs: add instruction for generating gen_s2_pairing.py Signed-off-by: F.N. Claessen --- ci/generate_s2.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/generate_s2.sh b/ci/generate_s2.sh index f1ee694..0df831c 100755 --- a/ci/generate_s2.sh +++ b/ci/generate_s2.sh @@ -3,3 +3,4 @@ . .venv/bin/activate datamodel-codegen --input specification/openapi.yml --input-file-type openapi --output-model-type pydantic_v2.BaseModel --output src/s2python/generated/gen_s2.py --use-one-literal-as-default +# datamodel-codegen --input specification/s2-over-ip-pairing.yaml --input-file-type openapi --output-model-type pydantic_v2.BaseModel --output src/s2python/generated/gen_s2_pairing.py --use-one-literal-as-default From c817a4ca23e719263662d0a66a6ef52f68df8afd Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 16:41:57 +0200 Subject: [PATCH 14/29] feat: add generated classes for S2 pairing & authentication Signed-off-by: F.N. Claessen --- src/s2python/generated/gen_s2_pairing.py | 189 ++++++----------------- 1 file changed, 43 insertions(+), 146 deletions(-) diff --git a/src/s2python/generated/gen_s2_pairing.py b/src/s2python/generated/gen_s2_pairing.py index 32979b9..15a6b05 100644 --- a/src/s2python/generated/gen_s2_pairing.py +++ b/src/s2python/generated/gen_s2_pairing.py @@ -1,173 +1,70 @@ -""" -Generated classes based on s2-over-ip-pairing.yaml OpenAPI schema. -This file is auto-generated and should not be modified directly. -""" - -import uuid -from dataclasses import dataclass -from datetime import datetime -from enum import Enum, auto -from typing import List, Optional +# generated by datamodel-codegen: +# filename: s2-over-ip-pairing.yaml +# timestamp: 2025-04-15T14:41:29+00:00 +from __future__ import annotations -class Protocols(str, Enum): - """Supported protocol types.""" +from enum import Enum +from typing import List, Optional +from uuid import UUID - WebSocketSecure = "WebSocketSecure" +from pydantic import AnyUrl, AwareDatetime, BaseModel, RootModel, constr -class S2Role(str, Enum): - """Roles in the S2 protocol.""" +class Protocols(Enum): + WebSocketSecure = 'WebSocketSecure' - CEM = "CEM" - RM = "RM" +class S2Role(Enum): + CEM = 'CEM' + RM = 'RM' -class Deployment(str, Enum): - """Deployment types.""" - WAN = "WAN" - LAN = "LAN" +class Deployment(Enum): + WAN = 'WAN' + LAN = 'LAN' -@dataclass -class S2NodeDescription: - """Description of an S2 node.""" +class PairingToken(RootModel[constr(pattern=r'^[0-9a-zA-Z]{32}$')]): + root: constr(pattern=r'^[0-9a-zA-Z]{32}$') - brand: Optional[str] = None - logoUri: Optional[str] = None - type: Optional[str] = None - modelName: Optional[str] = None - userDefinedName: Optional[str] = None - role: Optional[S2Role] = None - deployment: Optional[Deployment] = None +class PairingInfo(BaseModel): + pairingUri: Optional[AnyUrl] = None + token: Optional[PairingToken] = None + validUntil: Optional[AwareDatetime] = None -class PairingToken(str): - """A token used for pairing. - - Must match pattern: ^[0-9a-zA-Z]{32}$ - """ - - def __new__(cls, content: str): - import re - if not re.match(r"^[0-9a-zA-Z]{32}$", content): - raise ValueError("PairingToken must be 32 alphanumeric characters") - return super().__new__(cls, content) +class ConnectionRequest(BaseModel): + s2ClientNodeId: Optional[UUID] = None + supportedProtocols: Optional[List[Protocols]] = None -@dataclass -class PairingInfo: - """Information about a pairing.""" +class ConnectionDetails(BaseModel): + selectedProtocol: Optional[Protocols] = None + challenge: Optional[str] = None + connectionUri: Optional[AnyUrl] = None - pairingUri: Optional[str] = None - token: Optional[PairingToken] = None - validUntil: Optional[datetime] = None +class S2NodeDescription(BaseModel): + brand: Optional[str] = None + logoUri: Optional[AnyUrl] = None + type: Optional[str] = None + modelName: Optional[str] = None + userDefinedName: Optional[str] = None + role: Optional[S2Role] = None + deployment: Optional[Deployment] = None -@dataclass -class PairingRequest: - """Request to initiate pairing.""" +class PairingRequest(BaseModel): token: Optional[PairingToken] = None - publicKey: Optional[bytes] = None - s2ClientNodeId: Optional[uuid.UUID] = None + publicKey: Optional[str] = None + s2ClientNodeId: Optional[UUID] = None s2ClientNodeDescription: Optional[S2NodeDescription] = None supportedProtocols: Optional[List[Protocols]] = None -@dataclass -class PairingResponse: - """Response to a pairing request.""" - - s2ServerNodeId: Optional[uuid.UUID] = None +class PairingResponse(BaseModel): + s2ServerNodeId: Optional[UUID] = None serverNodeDescription: Optional[S2NodeDescription] = None - requestConnectionUri: Optional[str] = None - - -@dataclass -class ConnectionRequest: - """Request to establish a connection.""" - - s2ClientNodeId: Optional[uuid.UUID] = None - supportedProtocols: Optional[List[Protocols]] = None - - -@dataclass -class ConnectionDetails: - """Details for establishing a connection.""" - - selectedProtocol: Optional[Protocols] = None - challenge: Optional[bytes] = None - connectionUri: Optional[str] = None - - -# Serialization/Deserialization functions - - -def _is_dataclass_instance(obj): - """Check if an object is a dataclass instance.""" - from dataclasses import is_dataclass - - return is_dataclass(obj) and not isinstance(obj, type) - - -def to_dict(obj): - """Convert a dataclass instance to a dictionary.""" - if isinstance(obj, datetime): - return obj.isoformat() - elif isinstance(obj, uuid.UUID): - return str(obj) - elif isinstance(obj, bytes): - import base64 - - return base64.b64encode(obj).decode("ascii") - elif isinstance(obj, Enum): - return obj.value - elif isinstance(obj, list): - return [to_dict(item) for item in obj] - elif _is_dataclass_instance(obj): - result = {} - for field in obj.__dataclass_fields__: - value = getattr(obj, field) - if value is not None: - result[field] = to_dict(value) - return result - else: - return obj - - -def from_dict(cls, data): - """Create a dataclass instance from a dictionary.""" - if data is None: - return None - - if cls is datetime: - return datetime.fromisoformat(data) - elif cls is uuid.UUID: - return uuid.UUID(data) - elif cls is bytes: - import base64 - - return base64.b64decode(data.encode("ascii")) - elif issubclass(cls, Enum): - return cls(data) - elif issubclass(cls, PairingToken): - return PairingToken(data) - elif hasattr(cls, "__dataclass_fields__"): - fieldtypes = cls.__annotations__ - instance_data = {} - - for field, field_type in fieldtypes.items(): - if field in data and data[field] is not None: - # Handle List[Type] annotations - if hasattr(field_type, "__origin__") and field_type.__origin__ is list: - item_type = field_type.__args__[0] - instance_data[field] = [from_dict(item_type, item) for item in data[field]] - else: - instance_data[field] = from_dict(field_type, data[field]) - - return cls(**instance_data) - else: - return data + requestConnectionUri: Optional[AnyUrl] = None From 31f9de047dcbf1e41e721e6bd83cadc334fa3876 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 18:08:38 +0200 Subject: [PATCH 15/29] fix: add extra testenv requirement Signed-off-by: F.N. Claessen --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index fbef9e5..f59d4cf 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ passenv = extras = testing ws + fastapi commands = pytest {posargs} From 88ae53ab04f11b4646f0d6c4898a25b52300aac0 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 18:17:17 +0200 Subject: [PATCH 16/29] fix: fastapi's testclient requires httpx Signed-off-by: F.N. Claessen --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 239a3de..dbc2342 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ flask = [ "Flask", ] testing = [ + "httpx", "pytest", "pytest-coverage", "pytest-timer", From 6a8d424b8251faab55f2e5b234912682d9cf6be4 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 18:23:57 +0200 Subject: [PATCH 17/29] style: pylint Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index 1b583af..09183a4 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -20,7 +20,7 @@ def handle_connection_request(self, request_data: ConnectionRequest) -> Connecti class MyFastAPI(FastAPI): - def __init__(self, *args, **kwargs): + def __init__(self, *args: list, **kwargs: dict): super().__init__(*args, **kwargs) self.s2 = FastAPIAuthServer() @@ -29,10 +29,10 @@ def __init__(self, *args, **kwargs): @app.post('/requestConnection', response_model=ConnectionDetails) -async def post_request_connection(body: ConnectionRequest = None) -> ConnectionDetails: +async def post_request_connection(body: ConnectionRequest) -> ConnectionDetails: return app.s2.handle_connection_request(body) @app.post('/requestPairing', response_model=PairingResponse) -async def post_request_pairing(body: PairingRequest = None) -> PairingResponse: +async def post_request_pairing(body: PairingRequest) -> PairingResponse: return app.s2.handle_pairing_request(body) From 1018743b269ef543fbcde07791bbe9d709ef96d1 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 18:26:09 +0200 Subject: [PATCH 18/29] chore: update update_dependencies.sh and run it Signed-off-by: F.N. Claessen --- ci/update_dependencies.sh | 2 +- dev-requirements.txt | 102 +++++++++++++++++++------------------- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/ci/update_dependencies.sh b/ci/update_dependencies.sh index ca11b03..bd39787 100755 --- a/ci/update_dependencies.sh +++ b/ci/update_dependencies.sh @@ -1,4 +1,4 @@ #!/usr/bin/env sh . .venv/bin/activate -pip-compile -U --extra=testing --extra=development --extra=docs -o ./dev-requirements.txt setup.cfg +pip-compile --extra=ws --extra=fastapi --extra=development --extra=docs --extra=testing --output-file=./dev-requirements.txt pyproject.toml diff --git a/dev-requirements.txt b/dev-requirements.txt index 5b3daa2..192ea31 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,13 +1,17 @@ # -# This file is autogenerated by pip-compile with Python 3.8 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --extra=development --extra=docs --extra=testing --output-file=./dev-requirements.txt setup.cfg +# pip-compile --extra=development --extra=docs --extra=fastapi --extra=testing --extra=ws --output-file=./dev-requirements.txt pyproject.toml # alabaster==0.7.13 # via sphinx annotated-types==0.7.0 # via pydantic +anyio==4.9.0 + # via + # httpx + # starlette argcomplete==3.5.3 # via datamodel-code-generator astroid==3.2.4 @@ -21,7 +25,10 @@ build==1.2.2.post1 cachetools==5.5.2 # via tox certifi==2025.1.31 - # via requests + # via + # httpcore + # httpx + # requests cfgv==3.4.0 # via pre-commit chardet==5.2.0 @@ -32,13 +39,13 @@ click==8.1.8 # via # black # pip-tools - # s2-python (setup.cfg) + # s2-python (pyproject.toml) colorama==0.4.6 # via tox coverage[toml]==7.6.1 # via pytest-cov datamodel-code-generator==0.27.3 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) dill==0.3.9 # via pylint distlib==0.3.9 @@ -48,24 +55,29 @@ docutils==0.20.1 # sphinx # sphinx-rtd-theme # sphinx-tabs -exceptiongroup==1.2.2 - # via pytest +fastapi==0.115.12 + # via s2-python (pyproject.toml) filelock==3.16.1 # via # tox # virtualenv genson==1.3.0 # via datamodel-code-generator +h11==0.14.0 + # via httpcore +httpcore==1.0.8 + # via httpx +httpx==0.28.1 + # via s2-python (pyproject.toml) identify==2.6.1 # via pre-commit idna==3.10 - # via requests + # via + # anyio + # httpx + # requests imagesize==1.4.1 # via sphinx -importlib-metadata==8.5.0 - # via - # build - # sphinx inflect==5.6.2 # via datamodel-code-generator iniconfig==2.0.0 @@ -83,7 +95,7 @@ markupsafe==2.1.5 mccabe==0.7.0 # via pylint mypy==1.14.1 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) mypy-extensions==1.0.0 # via # black @@ -104,7 +116,7 @@ packaging==24.2 pathspec==0.12.1 # via black pip-tools==7.4.1 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) platformdirs==4.3.6 # via # black @@ -116,11 +128,12 @@ pluggy==1.5.0 # pytest # tox pre-commit==3.5.0 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) pydantic==2.10.6 # via # datamodel-code-generator - # s2-python (setup.cfg) + # fastapi + # s2-python (pyproject.toml) pydantic-core==2.27.2 # via pydantic pygments==2.19.1 @@ -128,7 +141,7 @@ pygments==2.19.1 # sphinx # sphinx-tabs pylint==3.2.7 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) pyproject-api==1.8.0 # via tox pyproject-hooks==1.2.0 @@ -136,24 +149,22 @@ pyproject-hooks==1.2.0 # build # pip-tools pyright==1.1.396 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) pytest==8.3.5 # via # pytest-cov # pytest-timer - # s2-python (setup.cfg) + # s2-python (pyproject.toml) pytest-cov==5.0.0 # via pytest-cover pytest-cover==3.0.0 # via pytest-coverage pytest-coverage==0.0 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) pytest-timer==1.0.0 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) pytz==2025.1 - # via - # babel - # s2-python (setup.cfg) + # via s2-python (pyproject.toml) pyyaml==6.0.2 # via # datamodel-code-generator @@ -162,11 +173,13 @@ requests==2.32.3 # via sphinx six==1.17.0 # via sphinxcontrib-httpdomain +sniffio==1.3.1 + # via anyio snowballstemmer==2.2.0 # via sphinx sphinx==7.1.2 # via - # s2-python (setup.cfg) + # s2-python (pyproject.toml) # sphinx-copybutton # sphinx-fontawesome # sphinx-rtd-theme @@ -174,13 +187,13 @@ sphinx==7.1.2 # sphinxcontrib-httpdomain # sphinxcontrib-jquery sphinx-copybutton==0.5.2 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) sphinx-fontawesome==0.0.6 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) sphinx-rtd-theme==3.0.2 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) sphinx-tabs==3.4.7 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) sphinxcontrib-applehelp==1.0.4 # via sphinx sphinxcontrib-devhelp==1.0.2 @@ -188,7 +201,7 @@ sphinxcontrib-devhelp==1.0.2 sphinxcontrib-htmlhelp==2.0.1 # via sphinx sphinxcontrib-httpdomain==1.8.1 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) sphinxcontrib-jquery==4.1 # via sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 @@ -197,35 +210,24 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx +starlette==0.46.2 + # via fastapi tomli==2.2.1 - # via - # black - # build - # coverage - # datamodel-code-generator - # mypy - # pip-tools - # pylint - # pyproject-api - # pytest - # tox + # via datamodel-code-generator tomlkit==0.13.2 # via pylint tox==4.24.1 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) types-pytz==2024.2.0.20241221 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) typing-extensions==4.12.2 # via - # annotated-types - # astroid - # black + # anyio + # fastapi # mypy # pydantic # pydantic-core - # pylint # pyright - # tox urllib3==2.2.3 # via requests virtualenv==20.29.2 @@ -233,11 +235,9 @@ virtualenv==20.29.2 # pre-commit # tox websockets==13.1 - # via s2-python (setup.cfg) + # via s2-python (pyproject.toml) wheel==0.45.1 # via pip-tools -zipp==3.20.2 - # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip From 8cdc78b1608f54dfe1f4bdede05751ffc42d8f8f Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 21:53:24 +0200 Subject: [PATCH 19/29] fix: make dev-requirements for Python 3.8 Signed-off-by: F.N. Claessen --- dev-requirements.txt | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 192ea31..89cd02e 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.11 +# This file is autogenerated by pip-compile with Python 3.8 # by the following command: # # pip-compile --extra=development --extra=docs --extra=fastapi --extra=testing --extra=ws --output-file=./dev-requirements.txt pyproject.toml @@ -8,7 +8,7 @@ alabaster==0.7.13 # via sphinx annotated-types==0.7.0 # via pydantic -anyio==4.9.0 +anyio==4.5.2 # via # httpx # starlette @@ -55,6 +55,10 @@ docutils==0.20.1 # sphinx # sphinx-rtd-theme # sphinx-tabs +exceptiongroup==1.2.2 + # via + # anyio + # pytest fastapi==0.115.12 # via s2-python (pyproject.toml) filelock==3.16.1 @@ -78,6 +82,10 @@ idna==3.10 # requests imagesize==1.4.1 # via sphinx +importlib-metadata==8.5.0 + # via + # build + # sphinx inflect==5.6.2 # via datamodel-code-generator iniconfig==2.0.0 @@ -164,7 +172,9 @@ pytest-coverage==0.0 pytest-timer==1.0.0 # via s2-python (pyproject.toml) pytz==2025.1 - # via s2-python (pyproject.toml) + # via + # babel + # s2-python (pyproject.toml) pyyaml==6.0.2 # via # datamodel-code-generator @@ -210,10 +220,20 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -starlette==0.46.2 +starlette==0.44.0 # via fastapi tomli==2.2.1 - # via datamodel-code-generator + # via + # black + # build + # coverage + # datamodel-code-generator + # mypy + # pip-tools + # pylint + # pyproject-api + # pytest + # tox tomlkit==0.13.2 # via pylint tox==4.24.1 @@ -222,12 +242,18 @@ types-pytz==2024.2.0.20241221 # via s2-python (pyproject.toml) typing-extensions==4.12.2 # via + # annotated-types # anyio + # astroid + # black # fastapi # mypy # pydantic # pydantic-core + # pylint # pyright + # starlette + # tox urllib3==2.2.3 # via requests virtualenv==20.29.2 @@ -238,6 +264,8 @@ websockets==13.1 # via s2-python (pyproject.toml) wheel==0.45.1 # via pip-tools +zipp==3.20.2 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # pip From cc1dc10bffda1d8524c0e882a4fb37e36f062eda Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 22:03:28 +0200 Subject: [PATCH 20/29] fix: update dev installation instructions Signed-off-by: F.N. Claessen --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2e7674a..04b5b49 100644 --- a/README.rst +++ b/README.rst @@ -59,7 +59,7 @@ Development For development, you can install the required dependencies using the following command: - pip install -e .[testing,development] + pip install -e .[testing,development,ws,fastapi] The tests can be run using tox: From 24ff521b262eae834232557f1b45f3a70fd0d638 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 22:09:31 +0200 Subject: [PATCH 21/29] fix: pass json body to the endpoints Signed-off-by: F.N. Claessen --- tests/unit/fastapi_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/fastapi_test.py b/tests/unit/fastapi_test.py index 19847c6..827b0a4 100644 --- a/tests/unit/fastapi_test.py +++ b/tests/unit/fastapi_test.py @@ -7,7 +7,7 @@ def test_post_pairing_request(): - response = client.post("/requestPairing") + response = client.post("/requestPairing", json={"hallo": "world"}) assert response.status_code == 200 assert response.json() == { "requestConnectionUri": None, @@ -17,7 +17,7 @@ def test_post_pairing_request(): def test_post_connection_request(): - response = client.post("/requestConnection") + response = client.post("/requestConnection", json={"hallo": "world"}) assert response.status_code == 200 assert response.json() == { "challenge": None, From 14b892a7210d92f502d5bf14fb44afb60432cfef Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 15 Apr 2025 22:13:26 +0200 Subject: [PATCH 22/29] style: mypy Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index 09183a4..2b0dfa9 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -5,6 +5,8 @@ "The 'fastapi' package is required. Run 'pip install s2-python[fastapi]' to use this feature." ) from exc +from typing import Any + from s2python.authorization.server import AbstractAuthServer from s2python.generated.gen_s2_pairing import ConnectionDetails, ConnectionRequest, PairingResponse, PairingRequest @@ -20,7 +22,7 @@ def handle_connection_request(self, request_data: ConnectionRequest) -> Connecti class MyFastAPI(FastAPI): - def __init__(self, *args: list, **kwargs: dict): + def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.s2 = FastAPIAuthServer() From 42d25e40ed07950b85ce5b9c607776f7ae4d2b6f Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 20 May 2025 10:22:25 +0200 Subject: [PATCH 23/29] refactor: change to S2AbstractServer Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 4 ++-- src/s2python/authorization/server.py | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index 2b0dfa9..270df3d 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -7,11 +7,11 @@ from typing import Any -from s2python.authorization.server import AbstractAuthServer +from s2python.authorization.server import S2AbstractServer from s2python.generated.gen_s2_pairing import ConnectionDetails, ConnectionRequest, PairingResponse, PairingRequest -class FastAPIAuthServer(AbstractAuthServer): +class FastAPIAuthServer(S2AbstractServer): def handle_pairing_request(self, request_data: PairingRequest) -> PairingResponse: return PairingResponse() diff --git a/src/s2python/authorization/server.py b/src/s2python/authorization/server.py index b81a5b6..add6dae 100644 --- a/src/s2python/authorization/server.py +++ b/src/s2python/authorization/server.py @@ -1,14 +1,21 @@ -from abc import ABC, abstractmethod +import abc from s2python.generated.gen_s2_pairing import ConnectionDetails, ConnectionRequest, PairingResponse, PairingRequest -class AbstractAuthServer(ABC): +class S2AbstractServer(abc.ABC): + """Abstract server for handling S2 protocol pairing and connections. - @abstractmethod + This class serves as an interface that developers can extend to implement + S2 protocol functionality with their preferred technology stack. + Concrete implementations should override the abstract methods marked + with @abc.abstractmethod. + """ + + @abc.abstractmethod def handle_pairing_request(self, request_data: PairingRequest) -> PairingResponse: pass - @abstractmethod + @abc.abstractmethod def handle_connection_request(self, request_data: ConnectionRequest) -> ConnectionDetails: pass From 1d1c8da3f7792cbc5bfe1e8edf9014d9466c4aef Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 23 May 2025 15:05:57 +0200 Subject: [PATCH 24/29] refactor: split up default server class into parts handling tokens and challenges and part setting up a default HTTP server Signed-off-by: F.N. Claessen --- src/s2python/authorization/default_server.py | 3 +++ src/s2python/authorization/examples/example_s2_server.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/s2python/authorization/default_server.py b/src/s2python/authorization/default_server.py index 9f6b339..6a6e665 100644 --- a/src/s2python/authorization/default_server.py +++ b/src/s2python/authorization/default_server.py @@ -222,6 +222,9 @@ def _create_encrypted_challenge( return str(challenge) + +class S2DefaultHTTPServer(S2DefaultServer): + def start_server(self) -> None: """Start the HTTP server.""" diff --git a/src/s2python/authorization/examples/example_s2_server.py b/src/s2python/authorization/examples/example_s2_server.py index c655743..7bd5949 100644 --- a/src/s2python/authorization/examples/example_s2_server.py +++ b/src/s2python/authorization/examples/example_s2_server.py @@ -9,7 +9,7 @@ from datetime import datetime, timedelta from typing import Any -from s2python.authorization.default_server import S2DefaultServer +from s2python.authorization.default_server import S2DefaultHTTPServer from s2python.generated.gen_s2_pairing import ( S2NodeDescription, Deployment, @@ -71,7 +71,7 @@ def signal_handler(sig: int, frame: Any) -> None: ) # Create and configure the server - server = S2DefaultServer( + server = S2DefaultHTTPServer( host=args.host, http_port=args.http_port, ws_port=args.ws_port, From d15051a26a844d0d86031ae301159b4e9972c7bc Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 23 May 2025 15:23:38 +0200 Subject: [PATCH 25/29] fix: call actual implementations to handle_pairing_request and handle_connection_request Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index 270df3d..7f696fc 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -7,17 +7,17 @@ from typing import Any -from s2python.authorization.server import S2AbstractServer +from s2python.authorization.default_server import S2DefaultServer from s2python.generated.gen_s2_pairing import ConnectionDetails, ConnectionRequest, PairingResponse, PairingRequest -class FastAPIAuthServer(S2AbstractServer): +class FastAPIAuthServer(S2DefaultServer): def handle_pairing_request(self, request_data: PairingRequest) -> PairingResponse: - return PairingResponse() + return super().handle_pairing_request(request_data) def handle_connection_request(self, request_data: ConnectionRequest) -> ConnectionDetails: - return ConnectionDetails() + return super().handle_connection_request(request_data) class MyFastAPI(FastAPI): From 4965f47bc316ea1f550057c111410a9b17f169c8 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 23 May 2025 15:24:28 +0200 Subject: [PATCH 26/29] refactor: remove redundant class Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index 7f696fc..1efcc94 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -11,20 +11,11 @@ from s2python.generated.gen_s2_pairing import ConnectionDetails, ConnectionRequest, PairingResponse, PairingRequest -class FastAPIAuthServer(S2DefaultServer): - - def handle_pairing_request(self, request_data: PairingRequest) -> PairingResponse: - return super().handle_pairing_request(request_data) - - def handle_connection_request(self, request_data: ConnectionRequest) -> ConnectionDetails: - return super().handle_connection_request(request_data) - - class MyFastAPI(FastAPI): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - self.s2 = FastAPIAuthServer() + self.s2 = S2DefaultServer() app = MyFastAPI() From 557bd40e9c6f09c61b767ffe170efe4798b6c8db Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 23 May 2025 15:24:51 +0200 Subject: [PATCH 27/29] refactor: rename custom FastAPI class Signed-off-by: F.N. Claessen --- src/s2python/authorization/fastapi_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index 1efcc94..77dcd7e 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -11,14 +11,14 @@ from s2python.generated.gen_s2_pairing import ConnectionDetails, ConnectionRequest, PairingResponse, PairingRequest -class MyFastAPI(FastAPI): +class S2FastAPI(FastAPI): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.s2 = S2DefaultServer() -app = MyFastAPI() +app = S2FastAPI() @app.post('/requestConnection', response_model=ConnectionDetails) From 32359f6ade10ca953417010715a9f7261aced56d Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 23 May 2025 15:31:13 +0200 Subject: [PATCH 28/29] dev: remove abstract methods to start and stop the server Signed-off-by: F.N. Claessen --- src/s2python/authorization/server.py | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/s2python/authorization/server.py b/src/s2python/authorization/server.py index 68fdb2e..1e30797 100644 --- a/src/s2python/authorization/server.py +++ b/src/s2python/authorization/server.py @@ -242,18 +242,18 @@ def _create_encrypted_challenge( str: The encrypted challenge """ - @abc.abstractmethod - def start_server(self) -> None: - """Start the server. - - This method should be implemented by concrete subclasses to start - the server using their preferred web framework. - """ - - @abc.abstractmethod - def stop_server(self) -> None: - """Stop the server. - - This method should be implemented by concrete subclasses to stop - the server gracefully. - """ + # @abc.abstractmethod + # def start_server(self) -> None: + # """Start the server. + # + # This method should be implemented by concrete subclasses to start + # the server using their preferred web framework. + # """ + # + # @abc.abstractmethod + # def stop_server(self) -> None: + # """Stop the server. + # + # This method should be implemented by concrete subclasses to stop + # the server gracefully. + # """ From 1f64d6c69698db6d49f40a65ce5f1395e5f19016 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Fri, 23 May 2025 15:37:20 +0200 Subject: [PATCH 29/29] fix: actually remove abstract methods to start and stop the server Signed-off-by: F.N. Claessen --- src/s2python/authorization/server.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/s2python/authorization/server.py b/src/s2python/authorization/server.py index 1e30797..84dfb1d 100644 --- a/src/s2python/authorization/server.py +++ b/src/s2python/authorization/server.py @@ -241,19 +241,3 @@ def _create_encrypted_challenge( Returns: str: The encrypted challenge """ - - # @abc.abstractmethod - # def start_server(self) -> None: - # """Start the server. - # - # This method should be implemented by concrete subclasses to start - # the server using their preferred web framework. - # """ - # - # @abc.abstractmethod - # def stop_server(self) -> None: - # """Stop the server. - # - # This method should be implemented by concrete subclasses to stop - # the server gracefully. - # """