Skip to content

Commit b771b9f

Browse files
committed
✨ Enrich OpenAPI specification (#181)
1 parent 3193d63 commit b771b9f

22 files changed

+513
-186
lines changed

README.md

+11-15
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,17 @@ and production-ready services, offering automatic deployment for ML models.
3333

3434
Some remarkable characteristics:
3535

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

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

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

5757
## Getting Started
5858

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

71-
app = Flama(
72-
title="Hello-🔥",
73-
version="1.0",
74-
description="My first API",
75-
)
71+
app = Flama()
7672

7773

7874
@app.route("/")
@@ -101,8 +97,8 @@ flama run examples.hello_flama:app
10197

10298
## Authors
10399

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

107103
## Contributing
108104

examples/add_model_component.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,13 @@ def metadata(self):
9090

9191

9292
app = Flama(
93-
title="Flama ML",
94-
version="0.1.0",
95-
description="Machine learning API using Flama 🔥",
93+
openapi={
94+
"info": {
95+
"title": "Flama ML",
96+
"version": "0.1.0",
97+
"description": "Machine learning API using Flama 🔥",
98+
}
99+
},
96100
docs="/docs/",
97101
components=[component],
98102
)

examples/add_model_resource.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
from flama.resources import resource_method
1010

1111
app = Flama(
12-
title="Flama ML",
13-
version="0.1.0",
14-
description="Machine learning API using Flama 🔥",
12+
openapi={
13+
"info": {
14+
"title": "Flama ML",
15+
"version": "0.1.0",
16+
"description": "Machine learning API using Flama 🔥",
17+
}
18+
},
1519
docs="/docs/",
1620
)
1721

examples/add_models.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ def user(username: str):
6060

6161

6262
app = Flama(
63-
title="Flama ML",
64-
version="0.1.0",
65-
description="Machine learning API using Flama 🔥",
63+
openapi={
64+
"info": {
65+
"title": "Flama ML",
66+
"version": "0.1.0",
67+
"description": "Machine learning API using Flama 🔥",
68+
}
69+
},
6670
routes=[
6771
routing.Route("/", home),
6872
routing.Route("/user/me", user_me),

examples/data_schema.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66
from flama import Flama, schemas
77

88
app = Flama(
9-
title="Puppy Register", # API title
10-
version="0.1", # API version
11-
description="A register of puppies", # API description
9+
openapi={
10+
"info": {
11+
"title": "Puppy Register", # API title
12+
"version": "0.1", # API version
13+
"description": "A register of puppies", # API description
14+
}
15+
},
1216
schema="/schema/", # Path to expose OpenAPI schema
1317
docs="/docs/", # Path to expose Docs application
1418
)

examples/error.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22
from flama import Flama, routing
33

44
app = Flama(
5-
title="Hello-🔥",
6-
version="1.0",
7-
description="My first API",
5+
openapi={
6+
"info": {
7+
"title": "Hello-🔥",
8+
"version": "1.0",
9+
"description": "My first API",
10+
},
11+
"tags": [
12+
{"name": "Salute", "description": "This is the salute description"},
13+
],
14+
},
815
debug=True,
916
)
1017

examples/hello_flama.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import flama
22

3-
app = flama.Flama(title="Hello-🔥", version="1.0", description="My first API")
3+
app = flama.Flama(
4+
openapi={
5+
"info": {
6+
"title": "Hello-🔥",
7+
"version": "1.0",
8+
"description": "My first API",
9+
},
10+
"tags": [
11+
{"name": "Salute", "description": "This is the salute description"},
12+
],
13+
}
14+
)
415

516

617
@app.route("/")

examples/pagination.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@ def minimum_age_validation(cls, v):
2424

2525

2626
app = Flama(
27-
title="Puppy Register", # API title
28-
version="0.1", # API version
29-
description="A register of puppies", # API description
27+
openapi={
28+
"info": {
29+
"title": "Puppy Register", # API title
30+
"version": "0.1", # API version
31+
"description": "A register of puppies", # API description
32+
}
33+
},
3034
)
3135

3236

examples/resource.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,13 @@ class PuppyResource(CRUDResource):
3333

3434

3535
app = Flama(
36-
title="Puppy Register", # API title
37-
version="0.1.0", # API version
38-
description="A register of puppies", # API description
36+
openapi={
37+
"info": {
38+
"title": "Puppy Register", # API title
39+
"version": "0.1.0", # API version
40+
"description": "A register of puppies", # API description
41+
}
42+
},
3943
modules=[SQLAlchemyModule(database=DATABASE_URL)],
4044
)
4145

flama/applications.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,22 @@
3030
class Flama:
3131
def __init__(
3232
self,
33+
*,
3334
routes: t.Optional[t.Sequence["routing.BaseRoute"]] = None,
3435
components: t.Optional[t.Union[t.Sequence[injection.Component], set[injection.Component]]] = None,
3536
modules: t.Optional[t.Union[t.Sequence["Module"], set["Module"]]] = None,
3637
middleware: t.Optional[t.Sequence["Middleware"]] = None,
3738
debug: bool = False,
3839
events: t.Optional[t.Union[dict[str, list[t.Callable[..., t.Coroutine[t.Any, t.Any, None]]]], Events]] = None,
3940
lifespan: t.Optional[t.Callable[[t.Optional["Flama"]], t.AsyncContextManager]] = None,
40-
title: str = "Flama",
41-
version: str = "0.1.0",
42-
description: str = "Firing up with the flame",
41+
openapi: types.OpenAPISpec = {
42+
"info": {
43+
"title": "Flama",
44+
"version": "0.1.0",
45+
"summary": "Flama application",
46+
"description": "Firing up with the flame",
47+
},
48+
},
4349
schema: t.Optional[str] = "/schema/",
4450
docs: t.Optional[str] = "/docs/",
4551
schema_library: t.Optional[str] = None,
@@ -92,7 +98,7 @@ def __init__(
9298
# Initialise modules
9399
default_modules = [
94100
ResourcesModule(worker=worker),
95-
SchemaModule(title, version, description, schema=schema, docs=docs),
101+
SchemaModule(openapi, schema=schema, docs=docs),
96102
ModelsModule(),
97103
]
98104
self.modules = Modules(app=self, modules={*default_modules, *(modules or [])})

flama/cli/templates/app.py.j2

+7-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ from flama import Flama
22

33
app = Flama(
44
debug={{ debug }},
5-
title="{{ title }}",
6-
version="{{ version }}",
7-
description="{{ description }}",
5+
openapi={
6+
"info": {
7+
"title": "{{ title }}",
8+
"version": "{{ version }}",
9+
"description": "{{ description }}",
10+
}
11+
},
812
schema="{{ schema }}",
913
docs="{{ docs }}"
1014
)

flama/compat.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import sys
22

3-
__all__ = ["Concatenate", "ParamSpec", "TypeGuard", "UnionType", "StrEnum", "tomllib"]
3+
__all__ = ["Concatenate", "ParamSpec", "TypeGuard", "UnionType", "NotRequired", "StrEnum", "tomllib"]
44

55
# PORT: Remove when stop supporting 3.9
66
# Concatenate was added in Python 3.10
@@ -37,6 +37,14 @@
3737
else:
3838
from typing import Union as UnionType
3939

40+
# PORT: Remove when stop supporting 3.10
41+
# NotRequired was added in Python 3.11
42+
# https://docs.python.org/3/library/enum.html#enum.StrEnum
43+
if sys.version_info >= (3, 11):
44+
from typing import NotRequired
45+
else:
46+
from typing_extensions import NotRequired
47+
4048

4149
# PORT: Remove when stop supporting 3.10
4250
# StrEnum was added in Python 3.11

flama/schemas/generator.py

+2-29
Original file line numberDiff line numberDiff line change
@@ -242,35 +242,8 @@ def get_openapi_ref(
242242

243243

244244
class SchemaGenerator:
245-
def __init__(
246-
self,
247-
title: str,
248-
version: str,
249-
description: t.Optional[str] = None,
250-
terms_of_service: t.Optional[str] = None,
251-
contact_name: t.Optional[str] = None,
252-
contact_url: t.Optional[str] = None,
253-
contact_email: t.Optional[str] = None,
254-
license_name: t.Optional[str] = None,
255-
license_url: t.Optional[str] = None,
256-
schemas: t.Optional[dict] = None,
257-
):
258-
contact = (
259-
openapi.Contact(name=contact_name, url=contact_url, email=contact_email)
260-
if contact_name or contact_url or contact_email
261-
else None
262-
)
263-
264-
license = openapi.License(name=license_name, url=license_url) if license_name else None
265-
266-
self.spec = openapi.OpenAPISpec(
267-
title=title,
268-
version=version,
269-
description=description,
270-
terms_of_service=terms_of_service,
271-
contact=contact,
272-
license=license,
273-
)
245+
def __init__(self, spec: types.OpenAPISpec, schemas: t.Optional[dict[str, schemas.Schema]] = None):
246+
self.spec = openapi.OpenAPISpec.from_spec(spec)
274247

275248
# Builtin definitions
276249
self.schemas = SchemaRegistry(schemas=schemas)

flama/schemas/modules.py

+5-19
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from pathlib import Path
33
from types import ModuleType
44

5-
from flama import http, pagination, schemas
5+
from flama import http, pagination, schemas, types
66
from flama.modules import Module
77
from flama.schemas.generator import SchemaGenerator
88

@@ -14,22 +14,13 @@
1414
class SchemaModule(Module):
1515
name = "schema"
1616

17-
def __init__(
18-
self,
19-
title: str,
20-
version: str,
21-
description: str,
22-
schema: t.Optional[str] = None,
23-
docs: t.Optional[str] = None,
24-
):
17+
def __init__(self, openapi: types.OpenAPISpec, *, schema: t.Optional[str] = None, docs: t.Optional[str] = None):
2518
super().__init__()
2619
# Schema definitions
2720
self.schemas: dict[str, t.Any] = {}
2821

2922
# Schema
30-
self.title = title
31-
self.version = version
32-
self.description = description
23+
self.openapi = openapi
3324
self.schema_path = schema
3425
self.docs_path = docs
3526

@@ -48,9 +39,7 @@ def schema_generator(self) -> SchemaGenerator:
4839
:return: API Schema Generator.
4940
"""
5041
self.schemas.update({**schemas.schemas.SCHEMAS, **pagination.paginator.schemas})
51-
return SchemaGenerator(
52-
title=self.title, version=self.version, description=self.description, schemas=self.schemas
53-
)
42+
return SchemaGenerator(spec=self.openapi, schemas=self.schemas)
5443

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

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

9078
def docs_view(self) -> http.HTMLResponse:
91-
return http._FlamaTemplateResponse(
92-
"schemas/docs.html", {"title": self.title, "schema_url": self.schema_path, "docs_url": self.docs_path}
93-
)
79+
return http._FlamaTemplateResponse("schemas/docs.html", {"schema": self.schema})

0 commit comments

Comments
 (0)