Skip to content
Merged
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
33 changes: 3 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,36 +76,9 @@ If you wish to introduce backwards incompatible changes, you must create a new v
To create a new version:

- create a new directory under `app/versions` with the name of the next version (eg: v2, v3, etc.)
- create a new `FastAPI` app in that directory and add any routes, models, etc.
- in `app/main.py` import the app from the new version and append it to the `VERSIONS` constant.
- the `VERSIONS` constant contains tuples where the first value is the version prefix (eg: `/v2`, `/v3`, etc.)
and the second value contains the corresponding app.

Note that all applications in the `VERSIONS` constant will implement any routes defined in previous versions
(ie. versions that are listed earlier in `VERSIONS`).

If a route should not be made available in later versions, add the `@last_version` decorator to it.

For example, if v1 defines:

```python
@app.get("/test")
def test():
...
```

Then the `/test` route will be available in versions `/v2`, `/v3`, etc.

If v2 then redefines it as:

```python
@app.get("/test")
@last_version
def test():
...
```

Then the `/test` route will not be available from `/v3` onwards.
- create a new `fastapi.APIRouter` router in that directory and add any routes, models, etc.
- the new router should have a prefix of the form `vX` where `X` is the version number (eg: `/v2`, `/v3`, etc.)
- in `app/main.py` import the app from the new version and add it to the app with `.include_router`.

## Testing

Expand Down
69 changes: 42 additions & 27 deletions marble_api/app.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
from importlib import metadata

from fastapi import FastAPI, Request

from marble_api.utils.routing import get_routes
from marble_api.versions.v1.app import app as v1_app
from marble_api.versions.versioning import add_fallback_routes
from marble_api.versions.v1.app import router as v1_router

VERSIONS = [("/v1", v1_app)]
_metadata = metadata.metadata("marble_api").json

app = FastAPI(
title=_metadata["name"],
version=_metadata["version"],
description=_metadata["summary"],
docs_url=None,
redoc_url="/docs",
)

app = FastAPI()
app.include_router(v1_router)


@app.get("/")
@app.get("/", tags=["root"])
async def root(request: Request) -> dict:
"""Return information about all routes."""
return {
"routes": [
{
"path": f"{m.path if (m := info.get('mount')) else ''}{info['route'].path}",
"methods": info["route"].methods,
}
for info in get_routes(request.app, included_in_schema_only=True)
]
}


def _mount_versions() -> None:
"""Mount all implemented versions of this API under app."""
for i, (prefix, version_app) in enumerate(VERSIONS):
app.mount(prefix, version_app)
previous_version = VERSIONS[i - 1][1] if i else app
add_fallback_routes(version_app, previous_version)


_mount_versions()
"""Return app information."""
return {"name": request.app.title, "version": request.app.version, "description": request.app.description}


_original_openapi = app.openapi


def openapi_with_tag_groups() -> dict:
"""Update openapi schema with x-tagGroup data for nested versions."""
if app.openapi_schema:
return app.openapi_schema

_original_openapi()
groups = {"/": {"root"}}
for path, path_schema in app.openapi_schema["paths"].items():
path_version = path.split("/")[1]
if path_version.startswith("v"):
version_tag = f"Version {path_version[1:]}"
if version_tag not in groups:
groups[version_tag] = set()
for route_schema in path_schema.values():
if "tags" in route_schema:
route_schema["tags"] = [f"{path_version}: {tag}" for tag in route_schema["tags"]]
groups[version_tag].update(route_schema["tags"])
app.openapi_schema["x-tagGroups"] = [{"name": group, "tags": list(tags)} for group, tags in groups.items()]
return app.openapi_schema


app.openapi = openapi_with_tag_groups
2 changes: 1 addition & 1 deletion marble_api/utils/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def get_routes(

This includes FastAPI applications recursively mounted as well.
If included_in_schema_only is True, do not include routes who are not included in the schema
(ie. their included_in_schema attribute is False)._in_
(ie. their included_in_schema attribute is False).
"""
for route in app_.routes:
if isinstance(route, Mount):
Expand Down
8 changes: 4 additions & 4 deletions marble_api/versions/v1/app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from fastapi import FastAPI
from fastapi import APIRouter

from marble_api.versions.v1.data_request.routes import admin_router as data_request_admin_router
from marble_api.versions.v1.data_request.routes import user_router as data_request_user_router

app = FastAPI(version="1")
router = APIRouter(prefix="/v1")

app.include_router(data_request_user_router)
app.include_router(data_request_admin_router)
router.include_router(data_request_user_router)
router.include_router(data_request_admin_router)
39 changes: 0 additions & 39 deletions marble_api/versions/versioning.py

This file was deleted.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ classifiers = [
"Programming Language :: Python :: 3 :: Only",
]
dependencies = [
"fastapi<0.137", # TODO: handle breaking changes https://github.com/fastapi/fastapi/pull/15745
"fastapi~=0.137",
"pymongo~=4.14",
"geojson-pydantic~=2.0",
"stac-pydantic~=3.4",
Expand Down
12 changes: 3 additions & 9 deletions test/integration/test_app.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import pytest

from marble_api.app import VERSIONS

pytestmark = [pytest.mark.anyio, pytest.mark.no_db_cleanup]


async def test_root_in_root(async_client):
resp = await async_client.get("/")
assert {"methods": ["GET"], "path": "/"} in resp.json()["routes"]


async def test_version_roots_in_root(async_client):
resp = await async_client.get("/")
for version, _ in VERSIONS:
assert {"methods": ["GET"], "path": f"{version}/"} in resp.json()["routes"]
assert "name" in resp.json()
assert "version" in resp.json()
assert "description" in resp.json()
1 change: 0 additions & 1 deletion test/unit/versions/test_versioning.py

This file was deleted.

Loading