Skip to content

Commit

Permalink
✨ Enrich OpenAPI specification (#181)
Browse files Browse the repository at this point in the history
  • Loading branch information
perdy committed Mar 4, 2025
1 parent ee9ccad commit 2f95f9f
Show file tree
Hide file tree
Showing 22 changed files with 513 additions and 186 deletions.
26 changes: 11 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ and production-ready services, offering automatic deployment for ML models.

Some remarkable characteristics:

* Generic classes for API resources with the convenience of standard CRUD methods over SQLAlchemy tables.
* A schema system (based on Marshmallow or Typesystem) which allows the declaration of inputs and outputs of endpoints
- Generic classes for API resources with the convenience of standard CRUD methods over SQLAlchemy tables.
- A schema system (based on Marshmallow or Typesystem) which allows the declaration of inputs and outputs of endpoints
very easily, with the convenience of reliable and automatic data-type validation.
* Dependency injection to make ease the process of managing parameters needed in endpoints via the use of `Component`s.
- Dependency injection to make ease the process of managing parameters needed in endpoints via the use of `Component`s.
Flama ASGI objects like `Request`, `Response`, `Session` and so on are defined as `Component`s ready to be injected in
your endpoints.
* `Component`s as the base of the plugin ecosystem, allowing you to create custom or use those already defined in your
- `Component`s as the base of the plugin ecosystem, allowing you to create custom or use those already defined in your
endpoints, injected as parameters.
* Auto generated API schema using OpenAPI standard.
* Auto generated `docs`, and provides a Swagger UI and ReDoc endpoints.
* Automatic handling of pagination, with several methods at your disposal such as `limit-offset` and `page numbering`,
- Auto generated API schema using OpenAPI standard.
- Auto generated `docs`, and provides a Swagger UI and ReDoc endpoints.
- Automatic handling of pagination, with several methods at your disposal such as `limit-offset` and `page numbering`,
to name a few.

## Installation
Expand All @@ -52,7 +52,7 @@ Flama is fully compatible with all [supported versions](https://devguide.python.
you to use the latest version available.

For a detailed explanation on how to install flama
visit: [https://flama.dev/docs/getting-started/installation](https://flama.dev/docs/getting-started/installation).
visit: [https://flama.dev/docs/getting-started/installation](https://flama.dev/docs/getting-started/installation).

## Getting Started

Expand All @@ -68,11 +68,7 @@ Visit [https://flama.dev/docs/](https://flama.dev/docs/) to view the full docume
```python
from flama import Flama

app = Flama(
title="Hello-🔥",
version="1.0",
description="My first API",
)
app = Flama()


@app.route("/")
Expand Down Expand Up @@ -101,8 +97,8 @@ flama run examples.hello_flama:app

## Authors

* José Antonio Perdiguero López ([@perdy](https://github.com/perdy/))
* Miguel Durán-Olivencia ([@migduroli](https://github.com/migduroli/))
- José Antonio Perdiguero López ([@perdy](https://github.com/perdy/))
- Miguel Durán-Olivencia ([@migduroli](https://github.com/migduroli/))

## Contributing

Expand Down
10 changes: 7 additions & 3 deletions examples/add_model_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,13 @@ def metadata(self):


app = Flama(
title="Flama ML",
version="0.1.0",
description="Machine learning API using Flama 🔥",
openapi={
"info": {
"title": "Flama ML",
"version": "0.1.0",
"description": "Machine learning API using Flama 🔥",
}
},
docs="/docs/",
components=[component],
)
Expand Down
10 changes: 7 additions & 3 deletions examples/add_model_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
from flama.resources import resource_method

app = Flama(
title="Flama ML",
version="0.1.0",
description="Machine learning API using Flama 🔥",
openapi={
"info": {
"title": "Flama ML",
"version": "0.1.0",
"description": "Machine learning API using Flama 🔥",
}
},
docs="/docs/",
)

Expand Down
10 changes: 7 additions & 3 deletions examples/add_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,13 @@ def user(username: str):


app = Flama(
title="Flama ML",
version="0.1.0",
description="Machine learning API using Flama 🔥",
openapi={
"info": {
"title": "Flama ML",
"version": "0.1.0",
"description": "Machine learning API using Flama 🔥",
}
},
routes=[
routing.Route("/", home),
routing.Route("/user/me", user_me),
Expand Down
10 changes: 7 additions & 3 deletions examples/data_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
from flama import Flama, schemas

app = Flama(
title="Puppy Register", # API title
version="0.1", # API version
description="A register of puppies", # API description
openapi={
"info": {
"title": "Puppy Register", # API title
"version": "0.1", # API version
"description": "A register of puppies", # API description
}
},
schema="/schema/", # Path to expose OpenAPI schema
docs="/docs/", # Path to expose Docs application
)
Expand Down
13 changes: 10 additions & 3 deletions examples/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
from flama import Flama, routing

app = Flama(
title="Hello-🔥",
version="1.0",
description="My first API",
openapi={
"info": {
"title": "Hello-🔥",
"version": "1.0",
"description": "My first API",
},
"tags": [
{"name": "Salute", "description": "This is the salute description"},
],
},
debug=True,
)

Expand Down
13 changes: 12 additions & 1 deletion examples/hello_flama.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import flama

app = flama.Flama(title="Hello-🔥", version="1.0", description="My first API")
app = flama.Flama(
openapi={
"info": {
"title": "Hello-🔥",
"version": "1.0",
"description": "My first API",
},
"tags": [
{"name": "Salute", "description": "This is the salute description"},
],
}
)


@app.route("/")
Expand Down
10 changes: 7 additions & 3 deletions examples/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ def minimum_age_validation(cls, v):


app = Flama(
title="Puppy Register", # API title
version="0.1", # API version
description="A register of puppies", # API description
openapi={
"info": {
"title": "Puppy Register", # API title
"version": "0.1", # API version
"description": "A register of puppies", # API description
}
},
)


Expand Down
10 changes: 7 additions & 3 deletions examples/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ class PuppyResource(CRUDResource):


app = Flama(
title="Puppy Register", # API title
version="0.1.0", # API version
description="A register of puppies", # API description
openapi={
"info": {
"title": "Puppy Register", # API title
"version": "0.1.0", # API version
"description": "A register of puppies", # API description
}
},
modules=[SQLAlchemyModule(database=DATABASE_URL)],
)

Expand Down
14 changes: 10 additions & 4 deletions flama/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,22 @@
class Flama:
def __init__(
self,
*,
routes: t.Optional[t.Sequence["routing.BaseRoute"]] = None,
components: t.Optional[t.Union[t.Sequence[injection.Component], set[injection.Component]]] = None,
modules: t.Optional[t.Union[t.Sequence["Module"], set["Module"]]] = None,
middleware: t.Optional[t.Sequence["Middleware"]] = None,
debug: bool = False,
events: t.Optional[t.Union[dict[str, list[t.Callable[..., t.Coroutine[t.Any, t.Any, None]]]], Events]] = None,
lifespan: t.Optional[t.Callable[[t.Optional["Flama"]], t.AsyncContextManager]] = None,
title: str = "Flama",
version: str = "0.1.0",
description: str = "Firing up with the flame",
openapi: types.OpenAPISpec = {
"info": {
"title": "Flama",
"version": "0.1.0",
"summary": "Flama application",
"description": "Firing up with the flame",
},
},
schema: t.Optional[str] = "/schema/",
docs: t.Optional[str] = "/docs/",
schema_library: t.Optional[str] = None,
Expand Down Expand Up @@ -92,7 +98,7 @@ def __init__(
# Initialise modules
default_modules = [
ResourcesModule(worker=worker),
SchemaModule(title, version, description, schema=schema, docs=docs),
SchemaModule(openapi, schema=schema, docs=docs),
ModelsModule(),
]
self.modules = Modules(app=self, modules={*default_modules, *(modules or [])})
Expand Down
10 changes: 7 additions & 3 deletions flama/cli/templates/app.py.j2
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ from flama import Flama

app = Flama(
debug={{ debug }},
title="{{ title }}",
version="{{ version }}",
description="{{ description }}",
openapi={
"info": {
"title": "{{ title }}",
"version": "{{ version }}",
"description": "{{ description }}",
}
},
schema="{{ schema }}",
docs="{{ docs }}"
)
Expand Down
10 changes: 9 additions & 1 deletion flama/compat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import sys

__all__ = ["Concatenate", "ParamSpec", "TypeGuard", "UnionType", "StrEnum", "tomllib"]
__all__ = ["Concatenate", "ParamSpec", "TypeGuard", "UnionType", "NotRequired", "StrEnum", "tomllib"]

# PORT: Remove when stop supporting 3.9
# Concatenate was added in Python 3.10
Expand Down Expand Up @@ -37,6 +37,14 @@
else:
from typing import Union as UnionType

# PORT: Remove when stop supporting 3.10
# NotRequired was added in Python 3.11
# https://docs.python.org/3/library/enum.html#enum.StrEnum
if sys.version_info >= (3, 11):
from typing import NotRequired
else:
from typing_extensions import NotRequired


# PORT: Remove when stop supporting 3.10
# StrEnum was added in Python 3.11
Expand Down
31 changes: 2 additions & 29 deletions flama/schemas/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,35 +242,8 @@ def get_openapi_ref(


class SchemaGenerator:
def __init__(
self,
title: str,
version: str,
description: t.Optional[str] = None,
terms_of_service: t.Optional[str] = None,
contact_name: t.Optional[str] = None,
contact_url: t.Optional[str] = None,
contact_email: t.Optional[str] = None,
license_name: t.Optional[str] = None,
license_url: t.Optional[str] = None,
schemas: t.Optional[dict] = None,
):
contact = (
openapi.Contact(name=contact_name, url=contact_url, email=contact_email)
if contact_name or contact_url or contact_email
else None
)

license = openapi.License(name=license_name, url=license_url) if license_name else None

self.spec = openapi.OpenAPISpec(
title=title,
version=version,
description=description,
terms_of_service=terms_of_service,
contact=contact,
license=license,
)
def __init__(self, spec: types.OpenAPISpec, schemas: t.Optional[dict[str, schemas.Schema]] = None):
self.spec = openapi.OpenAPISpec.from_spec(spec)

# Builtin definitions
self.schemas = SchemaRegistry(schemas=schemas)
Expand Down
24 changes: 5 additions & 19 deletions flama/schemas/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pathlib import Path
from types import ModuleType

from flama import http, pagination, schemas
from flama import http, pagination, schemas, types
from flama.modules import Module
from flama.schemas.generator import SchemaGenerator

Expand All @@ -14,22 +14,13 @@
class SchemaModule(Module):
name = "schema"

def __init__(
self,
title: str,
version: str,
description: str,
schema: t.Optional[str] = None,
docs: t.Optional[str] = None,
):
def __init__(self, openapi: types.OpenAPISpec, *, schema: t.Optional[str] = None, docs: t.Optional[str] = None):
super().__init__()
# Schema definitions
self.schemas: dict[str, t.Any] = {}

# Schema
self.title = title
self.version = version
self.description = description
self.openapi = openapi
self.schema_path = schema
self.docs_path = docs

Expand All @@ -48,9 +39,7 @@ def schema_generator(self) -> SchemaGenerator:
:return: API Schema Generator.
"""
self.schemas.update({**schemas.schemas.SCHEMAS, **pagination.paginator.schemas})
return SchemaGenerator(
title=self.title, version=self.version, description=self.description, schemas=self.schemas
)
return SchemaGenerator(spec=self.openapi, schemas=self.schemas)

@property
def schema(self) -> dict[str, t.Any]:
Expand Down Expand Up @@ -81,13 +70,10 @@ def add_routes(self) -> None:
if self.schema_path:
self.app.add_route(self.schema_path, self.schema_view, methods=["GET"], include_in_schema=False)
if self.docs_path:
assert self.schema_path, "Schema path must be defined to use docs view"
self.app.add_route(self.docs_path, self.docs_view, methods=["GET"], include_in_schema=False)

def schema_view(self) -> http.OpenAPIResponse:
return http.OpenAPIResponse(self.schema)

def docs_view(self) -> http.HTMLResponse:
return http._FlamaTemplateResponse(
"schemas/docs.html", {"title": self.title, "schema_url": self.schema_path, "docs_url": self.docs_path}
)
return http._FlamaTemplateResponse("schemas/docs.html", {"schema": self.schema})
Loading

0 comments on commit 2f95f9f

Please sign in to comment.