diff --git a/.gitignore b/.gitignore index 5baf340..3f96451 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ venv dist/ build/ %LOCALAPPDATA% +s2-python/src/s2python/specification/s2-pairing/s2-over-ip-pairing.yaml 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 diff --git a/pyproject.toml b/pyproject.toml index 85788e6..239a3de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,12 @@ classifiers = [ ws = [ "websockets~=13.1", ] +fastapi = [ + "fastapi", +] +flask = [ + "Flask", +] testing = [ "pytest", "pytest-coverage", diff --git a/src/s2python/authorization/client.py b/src/s2python/authorization/client.py new file mode 100644 index 0000000..90e3a17 --- /dev/null +++ b/src/s2python/authorization/client.py @@ -0,0 +1,67 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict + + +class AbstractConnectionClient(ABC): + """Abstract class for handling the /requestConnection endpoint.""" + + def request_connection(self) -> Any: + """Orchestrate the connection request flow: build → execute → handle.""" + request_data = self.build_connection_request() + response_data = self.execute_connection_request(request_data) + return self.handle_connection_response(response_data) + + @abstractmethod + def build_connection_request(self) -> Dict: + """ + Build the payload for the ConnectionRequest schema. + Returns a dictionary with keys: s2ClientNodeId, supportedProtocols. + """ + + @abstractmethod + def execute_connection_request(self, request_data: Dict) -> Dict: + """ + Execute the POST request to /requestConnection. + Implementations should send the request_data to the endpoint + and return the JSON response as a dictionary. + """ + + @abstractmethod + def handle_connection_response(self, response_data: Dict) -> Any: + """ + Process the ConnectionDetails response (e.g., extract challenge and connection URI). + The response_data contains keys: selectedProtocol, challenge, connectionUri. + """ + + +class AbstractPairingClient(ABC): + """Abstract class for handling the /requestPairing endpoint.""" + + def request_pairing(self) -> Any: + """Orchestrate the pairing request flow: build → execute → handle.""" + request_data = self.build_pairing_request() + response_data = self.execute_pairing_request(request_data) + return self.handle_pairing_response(response_data) + + @abstractmethod + def build_pairing_request(self) -> Dict: + """ + Build the payload for the PairingRequest schema. + Returns a dictionary with keys: token, publicKey, s2ClientNodeId, + s2ClientNodeDescription, supportedProtocols. + """ + + @abstractmethod + def execute_pairing_request(self, request_data: Dict) -> Dict: + """ + Execute the POST request to /requestPairing. + Implementations should send the request_data to the endpoint + and return the JSON response as a dictionary. + """ + + @abstractmethod + def handle_pairing_response(self, response_data: Dict) -> Any: + """ + Process the PairingResponse (e.g., extract server details). + The response_data contains keys: s2ServerNodeId, serverNodeDescription, requestConnectionUri. + """ diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py new file mode 100644 index 0000000..e69de29 diff --git a/src/s2python/authorization/flask_service.py b/src/s2python/authorization/flask_service.py new file mode 100644 index 0000000..e69de29 diff --git a/src/s2python/authorization/server.py b/src/s2python/authorization/server.py new file mode 100644 index 0000000..e69de29 diff --git a/src/s2python/authorization/service.py b/src/s2python/authorization/service.py new file mode 100644 index 0000000..e69de29 diff --git a/src/s2python/generated/gen_s2_pairing.py b/src/s2python/generated/gen_s2_pairing.py new file mode 100644 index 0000000..15a6b05 --- /dev/null +++ b/src/s2python/generated/gen_s2_pairing.py @@ -0,0 +1,70 @@ +# generated by datamodel-codegen: +# filename: s2-over-ip-pairing.yaml +# timestamp: 2025-04-15T14:41:29+00:00 + +from __future__ import annotations + +from enum import Enum +from typing import List, Optional +from uuid import UUID + +from pydantic import AnyUrl, AwareDatetime, BaseModel, RootModel, constr + + +class Protocols(Enum): + WebSocketSecure = 'WebSocketSecure' + + +class S2Role(Enum): + CEM = 'CEM' + RM = 'RM' + + +class Deployment(Enum): + WAN = 'WAN' + LAN = 'LAN' + + +class PairingToken(RootModel[constr(pattern=r'^[0-9a-zA-Z]{32}$')]): + root: constr(pattern=r'^[0-9a-zA-Z]{32}$') + + +class PairingInfo(BaseModel): + pairingUri: Optional[AnyUrl] = None + token: Optional[PairingToken] = None + validUntil: Optional[AwareDatetime] = None + + +class ConnectionRequest(BaseModel): + s2ClientNodeId: Optional[UUID] = None + supportedProtocols: Optional[List[Protocols]] = None + + +class ConnectionDetails(BaseModel): + selectedProtocol: Optional[Protocols] = None + challenge: Optional[str] = None + connectionUri: Optional[AnyUrl] = 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): + token: Optional[PairingToken] = None + publicKey: Optional[str] = None + s2ClientNodeId: Optional[UUID] = None + s2ClientNodeDescription: Optional[S2NodeDescription] = None + supportedProtocols: Optional[List[Protocols]] = None + + +class PairingResponse(BaseModel): + s2ServerNodeId: Optional[UUID] = None + serverNodeDescription: Optional[S2NodeDescription] = None + requestConnectionUri: Optional[AnyUrl] = None