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: 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 2abfdc1..a7951e5 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,12 +2,16 @@ # This file is autogenerated by pip-compile with Python 3.8 # 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.5.2 + # 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 @@ -49,17 +56,30 @@ docutils==0.20.1 # sphinx-rtd-theme # sphinx-tabs exceptiongroup==1.2.2 - # via pytest + # via + # anyio + # 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 @@ -83,7 +103,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 +124,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 +136,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 +149,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 +157,24 @@ 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) + # s2-python (pyproject.toml) pyyaml==6.0.2 # via # datamodel-code-generator @@ -162,11 +183,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 +197,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 +211,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,6 +220,8 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx +starlette==0.44.0 + # via fastapi tomli==2.2.1 # via # black @@ -212,19 +237,22 @@ tomli==2.2.1 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 + # anyio # astroid # black + # fastapi # mypy # pydantic # pydantic-core # pylint # pyright + # starlette # tox urllib3==2.2.3 # via requests @@ -233,7 +261,7 @@ 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 diff --git a/pyproject.toml b/pyproject.toml index 9e2c664..c41d270 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ flask = [ "Flask", ] testing = [ + "httpx", "pytest", "pytest-coverage", "pytest-timer", 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, diff --git a/src/s2python/authorization/fastapi_service.py b/src/s2python/authorization/fastapi_service.py index e69de29..77dcd7e 100644 --- a/src/s2python/authorization/fastapi_service.py +++ b/src/s2python/authorization/fastapi_service.py @@ -0,0 +1,31 @@ +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 + +from typing import Any + +from s2python.authorization.default_server import S2DefaultServer +from s2python.generated.gen_s2_pairing import ConnectionDetails, ConnectionRequest, PairingResponse, PairingRequest + + +class S2FastAPI(FastAPI): + + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self.s2 = S2DefaultServer() + + +app = S2FastAPI() + + +@app.post('/requestConnection', response_model=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) -> PairingResponse: + return app.s2.handle_pairing_request(body) diff --git a/src/s2python/authorization/server.py b/src/s2python/authorization/server.py index 68fdb2e..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. - """ diff --git a/tests/unit/fastapi_test.py b/tests/unit/fastapi_test.py new file mode 100644 index 0000000..827b0a4 --- /dev/null +++ b/tests/unit/fastapi_test.py @@ -0,0 +1,26 @@ +from fastapi.testclient import TestClient + +from s2python.authorization.fastapi_service import app + + +client = TestClient(app) + + +def test_post_pairing_request(): + response = client.post("/requestPairing", json={"hallo": "world"}) + assert response.status_code == 200 + assert response.json() == { + "requestConnectionUri": None, + "s2ServerNodeId": None, + "serverNodeDescription": None, + } + + +def test_post_connection_request(): + response = client.post("/requestConnection", json={"hallo": "world"}) + assert response.status_code == 200 + assert response.json() == { + "challenge": None, + "connectionUri": None, + "selectedProtocol": None, + } 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}