sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #08894
[Merge] ~r00ta/maas:MAASENG-1782 into maas:sqlalchemy-spike
Jacopo Rota has proposed merging ~r00ta/maas:MAASENG-1782 into maas:sqlalchemy-spike.
Commit message:
Scaffold maasfastserver application
Requested reviews:
MAAS Maintainers (maas-maintainers)
For more details, see:
https://code.launchpad.net/~r00ta/maas/+git/maas/+merge/443955
This MP aims to
1) install the required packages for local development in the virtual environment .ve created by the makefile
2) scaffold the maasfastserver application and expose the root at `/api/v1/`
3) include the application in the setup scripts for tests, easyinstall, linting and formatting
--
Your team MAAS Maintainers is requested to review the proposed merge of ~r00ta/maas:MAASENG-1782 into maas:sqlalchemy-spike.
diff --git a/Makefile b/Makefile
index e4d1a4e..d4b08b5 100644
--- a/Makefile
+++ b/Makefile
@@ -38,6 +38,7 @@ export PGDATABASE := maas
define BIN_SCRIPTS
bin/maas \
+bin/maas-fast-region \
bin/maas-common \
bin/maas-power \
bin/maas-rack \
@@ -95,7 +96,7 @@ endif
$(VENV):
python3 -m venv --system-site-packages --clear $@
- $(VENV)/bin/pip install -e .[testing]
+ $(VENV)/bin/pip install -e .[testing,maasfastserver]
bin:
mkdir $@
diff --git a/pyproject.toml b/pyproject.toml
index 06f66e6..7ebd2da 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,6 +28,7 @@ known_first_party = """
apiclient
maascli
maasserver
+maasfastserver
maastesting
metadataserver
provisioningserver
@@ -42,7 +43,9 @@ exclude = [
]
[tool.pytest.ini_options]
+asyncio_mode = "auto"
filterwarnings = "error::BytesWarning"
testpaths = [
"src/tests",
+ "src/maasfastserver/tests",
]
diff --git a/setup.cfg b/setup.cfg
index d81e132..0ed09b2 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -34,6 +34,7 @@ include =
apiclient*
maascli*
maasserver*
+ maasfastserver*
metadataserver*
provisioningserver*
@@ -44,6 +45,7 @@ console_scripts =
maas-power = provisioningserver.power_driver_command:run
maas-rack = provisioningserver.rack_script:run
maas-region = maasserver.region_script:run
+ maas-fast-region = maasfastserver.main:run
maas-sampledata = maasserver.testing.sampledata.main:main
rackd = provisioningserver.server:run
regiond = maasserver.server:run
@@ -57,16 +59,27 @@ pytest11 =
maas-perftest = maastesting.pytest.perftest
[options.extras_require]
+maasfastserver =
+ SQLAlchemy==2.0.15
+ asyncpg==0.27.0
+ fastapi==0.91.0
+ pydantic==1.10.8
+ starlette==0.24.0
+ uvicorn==0.22.0
testing =
fixtures==4.0.0
+ httpx==0.24.1
hypothesis==6.46.9
ipdb==0.13.9
junitxml==0.7
postgresfixture==0.4.2
pytest==7.1.2
+ pytest-asyncio==0.21.0
pytest-mock==3.7.0
+ pytest-postgresql==5.0.0
pytest-xdist==2.5.0
python-subunit==1.4.0
testresources==2.0.1
testscenarios==0.5.0
testtools==2.5.0
+ uvicorn==0.22.0
diff --git a/src/maasfastserver/__init__.py b/src/maasfastserver/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maasfastserver/__init__.py
diff --git a/src/maasfastserver/api/__init__.py b/src/maasfastserver/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maasfastserver/api/__init__.py
diff --git a/src/maasfastserver/api/v1/__init__.py b/src/maasfastserver/api/v1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maasfastserver/api/v1/__init__.py
diff --git a/src/maasfastserver/api/v1/api.py b/src/maasfastserver/api/v1/api.py
new file mode 100644
index 0000000..cfcbb94
--- /dev/null
+++ b/src/maasfastserver/api/v1/api.py
@@ -0,0 +1,21 @@
+from fastapi import APIRouter
+
+from maasfastserver.api.v1.constants import V1_API_PREFIX
+from maasfastserver.api.v1.root import RootApi
+
+
+class V1Router:
+ def __init__(self):
+ self.api_router = self._setup_router()
+
+ def get_router(self) -> APIRouter:
+ return self.api_router
+
+ def _setup_router(self) -> APIRouter:
+ api_router = APIRouter()
+
+ # Add all the handlers for V1
+ api_router.include_router(
+ router=RootApi().get_router(), prefix=V1_API_PREFIX
+ )
+ return api_router
diff --git a/src/maasfastserver/api/v1/constants.py b/src/maasfastserver/api/v1/constants.py
new file mode 100644
index 0000000..8f42e33
--- /dev/null
+++ b/src/maasfastserver/api/v1/constants.py
@@ -0,0 +1 @@
+V1_API_PREFIX = "/v1"
diff --git a/src/maasfastserver/api/v1/root.py b/src/maasfastserver/api/v1/root.py
new file mode 100644
index 0000000..1fb60a1
--- /dev/null
+++ b/src/maasfastserver/api/v1/root.py
@@ -0,0 +1,25 @@
+from fastapi import APIRouter
+
+from maasfastserver.models.v1.responses.root import RootGetResponse
+
+
+class RootApi:
+ def __init__(self):
+ self.router = self._setup_router()
+
+ async def get(self) -> RootGetResponse:
+ return RootGetResponse()
+
+ def get_router(self) -> APIRouter:
+ return self.router
+
+ def _setup_router(self) -> APIRouter:
+ router = APIRouter()
+ router.add_api_route(
+ path="/",
+ endpoint=self.get,
+ methods=["GET"],
+ response_model=RootGetResponse,
+ status_code=200,
+ )
+ return router
diff --git a/src/maasfastserver/constants.py b/src/maasfastserver/constants.py
new file mode 100644
index 0000000..ad8bfb3
--- /dev/null
+++ b/src/maasfastserver/constants.py
@@ -0,0 +1 @@
+API_PREFIX = "/api"
diff --git a/src/maasfastserver/db/__init__.py b/src/maasfastserver/db/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maasfastserver/db/__init__.py
diff --git a/src/maasfastserver/db/database.py b/src/maasfastserver/db/database.py
new file mode 100644
index 0000000..a77f304
--- /dev/null
+++ b/src/maasfastserver/db/database.py
@@ -0,0 +1,32 @@
+from sqlalchemy import MetaData
+from sqlalchemy.ext.asyncio import (
+ AsyncConnection,
+ AsyncSession,
+ create_async_engine,
+)
+
+METADATA = MetaData()
+
+
+class Database:
+ dsn: str
+ conn: AsyncConnection | None
+
+ def __init__(self, dsn: str):
+ self.dsn = dsn
+ self._engine = create_async_engine(dsn)
+ self.conn = None
+
+ async def connect(self) -> None:
+ if self.conn is not None:
+ await self.conn.close()
+ self.conn = await self._engine.connect()
+
+ async def disconnect(self) -> None:
+ if self.conn is None:
+ return
+ await self.conn.close()
+ await self._engine.dispose()
+
+ def session(self) -> AsyncSession:
+ return AsyncSession(self._engine)
diff --git a/src/maasfastserver/main.py b/src/maasfastserver/main.py
new file mode 100644
index 0000000..dafb8e9
--- /dev/null
+++ b/src/maasfastserver/main.py
@@ -0,0 +1,61 @@
+from contextlib import asynccontextmanager
+from typing import AsyncGenerator
+
+from fastapi import FastAPI
+from starlette.middleware.cors import CORSMiddleware
+import uvicorn
+
+from maasfastserver.api.v1.api import V1Router
+from maasfastserver.constants import API_PREFIX
+from maasfastserver.db.database import Database
+from maasfastserver.settings import Settings
+
+
+def create_app(db_dsn: str | None = None) -> FastAPI:
+ """Create the FastAPI application."""
+ settings = Settings()
+ if db_dsn is None:
+ db_dsn = str(settings.db_dsn)
+
+ @asynccontextmanager
+ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
+ await db.connect()
+ yield
+ await db.disconnect()
+
+ db = Database(db_dsn)
+
+ app = FastAPI(
+ title="MAASFastServer",
+ name="maasfastserver",
+ lifespan=lifespan,
+ )
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=settings.allowed_origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+ app.state.db = db
+
+ # Add all the version handlers
+ app.include_router(router=V1Router().get_router(), prefix=API_PREFIX)
+
+ return app
+
+
+def run():
+ config = uvicorn.Config(
+ create_app(),
+ loop="asyncio",
+ proxy_headers=True,
+ host="0.0.0.0",
+ port=5241,
+ )
+ server = uvicorn.Server(config)
+ server.run()
+
+
+if __name__ == "__main__":
+ run()
diff --git a/src/maasfastserver/models/__init__.py b/src/maasfastserver/models/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maasfastserver/models/__init__.py
diff --git a/src/maasfastserver/models/v1/__init__.py b/src/maasfastserver/models/v1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maasfastserver/models/v1/__init__.py
diff --git a/src/maasfastserver/models/v1/responses/__init__.py b/src/maasfastserver/models/v1/responses/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maasfastserver/models/v1/responses/__init__.py
diff --git a/src/maasfastserver/models/v1/responses/root.py b/src/maasfastserver/models/v1/responses/root.py
new file mode 100644
index 0000000..16bb01c
--- /dev/null
+++ b/src/maasfastserver/models/v1/responses/root.py
@@ -0,0 +1,5 @@
+from pydantic import BaseModel
+
+
+class RootGetResponse(BaseModel):
+ """Root handler response."""
diff --git a/src/maasfastserver/settings.py b/src/maasfastserver/settings.py
new file mode 100644
index 0000000..0612eb5
--- /dev/null
+++ b/src/maasfastserver/settings.py
@@ -0,0 +1,22 @@
+from pydantic import BaseSettings, Field, PostgresDsn
+
+
+class DatabaseDsn(PostgresDsn):
+ allowed_schemes = {"postgresql+asyncpg"}
+
+
+class Settings(BaseSettings):
+ """API settings."""
+
+ db_dsn: DatabaseDsn = Field(
+ default="postgresql+asyncpg://maas:maas@localhost/maasdb",
+ env="MAASFAST_DB_DSN",
+ ) # type: ignore
+
+ # Remove allowed origins when we switch to the unix socket
+ allowed_origins: list[str] = Field(
+ default=["*"],
+ )
+
+
+SETTINGS = Settings()
diff --git a/src/maasfastserver/tests/__init__.py b/src/maasfastserver/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maasfastserver/tests/__init__.py
diff --git a/src/maasfastserver/tests/api/__init__.py b/src/maasfastserver/tests/api/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maasfastserver/tests/api/__init__.py
diff --git a/src/maasfastserver/tests/api/v1/__init__.py b/src/maasfastserver/tests/api/v1/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maasfastserver/tests/api/v1/__init__.py
diff --git a/src/maasfastserver/tests/api/v1/root_test.py b/src/maasfastserver/tests/api/v1/root_test.py
new file mode 100644
index 0000000..263381a
--- /dev/null
+++ b/src/maasfastserver/tests/api/v1/root_test.py
@@ -0,0 +1,9 @@
+from httpx import AsyncClient
+import pytest
+
+
+@pytest.mark.asyncio
+class TestRootApi:
+ async def test_get(self, user_app_client: AsyncClient) -> None:
+ response = await user_app_client.get("/api/v1/")
+ assert response.status_code == 200
diff --git a/src/maasfastserver/tests/conftest.py b/src/maasfastserver/tests/conftest.py
new file mode 100644
index 0000000..91d09f2
--- /dev/null
+++ b/src/maasfastserver/tests/conftest.py
@@ -0,0 +1,21 @@
+import pytest
+
+from .fixtures.app import user_app, user_app_client
+from .fixtures.db import db, db_setup, fixture, session
+
+__all__ = [
+ "db",
+ "db_setup",
+ "fixture",
+ "session",
+ "user_app",
+ "user_app_client",
+]
+
+
+def pytest_addoption(parser: pytest.Parser) -> None:
+ parser.addoption(
+ "--sqlalchemy-debug",
+ help="print out SQLALchemy queries",
+ action="store_true",
+ )
diff --git a/src/maasfastserver/tests/fixtures/__init__.py b/src/maasfastserver/tests/fixtures/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maasfastserver/tests/fixtures/__init__.py
diff --git a/src/maasfastserver/tests/fixtures/app.py b/src/maasfastserver/tests/fixtures/app.py
new file mode 100644
index 0000000..2d7f6e6
--- /dev/null
+++ b/src/maasfastserver/tests/fixtures/app.py
@@ -0,0 +1,24 @@
+from __future__ import annotations
+
+from typing import AsyncIterable, Iterable
+
+from fastapi import FastAPI
+from httpx import AsyncClient
+import pytest
+
+from maasfastserver.db.database import Database
+from maasfastserver.main import create_app
+
+
+@pytest.fixture
+def user_app(db: Database) -> Iterable[FastAPI]:
+ """The API for users."""
+ app = create_app(db.dsn)
+ yield app
+
+
+@pytest.fixture
+async def user_app_client(user_app: FastAPI) -> AsyncIterable[AsyncClient]:
+ """Client for the user API."""
+ async with AsyncClient(app=user_app, base_url="http://test") as client:
+ yield client
diff --git a/tox.ini b/tox.ini
index 21ee1f7..097d992 100644
--- a/tox.ini
+++ b/tox.ini
@@ -38,6 +38,7 @@ lint =
package-files/usr/sbin/maas-dhcp-helper \
setup.py \
src/maasserver \
+ src/maasfastserver \
src/maastesting/ \
src/metadataserver \
src/provisioningserver \
Follow ups