Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ venv
dist/
build/
%LOCALAPPDATA%
s2-python/src/s2python/specification/s2-pairing/s2-over-ip-pairing.yaml
1 change: 1 addition & 0 deletions ci/generate_s2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ classifiers = [
ws = [
"websockets~=13.1",
]
fastapi = [
"fastapi",
]
flask = [
"Flask",
]
testing = [
"pytest",
"pytest-coverage",
Expand Down
67 changes: 67 additions & 0 deletions src/s2python/authorization/client.py
Original file line number Diff line number Diff line change
@@ -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.
"""
Empty file.
Empty file.
Empty file.
Empty file.
70 changes: 70 additions & 0 deletions src/s2python/generated/gen_s2_pairing.py
Original file line number Diff line number Diff line change
@@ -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