Skip to content

Commit

Permalink
Merge pull request #29 from dapper91/dev
Browse files Browse the repository at this point in the history
specification generation implemented

- openapi specification generation implemented
- openrpc specification generation implemented
- web ui support added (SwaggerUI, RapiDoc, ReDoc)
  • Loading branch information
dapper91 committed Aug 15, 2021
2 parents d0e7504 + 28d0723 commit 464018d
Show file tree
Hide file tree
Showing 34 changed files with 4,841 additions and 13 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Changelog
=========

1.3.0 (2021-08-13)
------------------

- openapi specification generation implemented
- openrpc specification generation implemented
- web ui support added (SwaggerUI, RapiDoc, ReDoc)


1.2.3 (2021-08-10)
------------------

Expand Down
2 changes: 2 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pydantic = "~=1.0"
requests = "~=2.0"
kombu = "~=5.0"
httpx = "~=0.0"
docstring-parser = "~=0.0"
openapi-ui-bundles = "~=0.0"

[dev-packages]
aioresponses = "~=0.0"
Expand Down
279 changes: 279 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Features:
- popular frameworks integration
- builtin parameter validation
- pytest integration
- openapi schema generation support
- web ui support (SwaggerUI, RapiDoc, ReDoc)

Installation
------------
Expand All @@ -53,6 +55,7 @@ Extra requirements
- `pydantic <https://pydantic-docs.helpmanual.io/>`_
- `requests <https://requests.readthedocs.io>`_
- `httpx <https://www.python-httpx.org/>`_
- `openapi-ui-bundles <https://github.com/dapper91/openapi_ui_bundles>`_


Documentation
Expand Down Expand Up @@ -420,3 +423,279 @@ On the server side everything is also pretty straightforward:
if __name__ == "__main__":
app.run(port=80)
Open API specification
______________________

``pjrpc`` has built-in `OpenAPI <https://swagger.io/specification/>`_ and `OpenRPC <https://spec.open-rpc.org/#introduction>`_
specification generation support and integrated web UI as an extra dependency. Three UI types are supported:

- SwaggerUI (`<https://swagger.io/tools/swagger-ui/>`_)
- RapiDoc (`<https://mrin9.github.io/RapiDoc/>`_)
- ReDoc (`<https://github.com/Redocly/redoc>`_)

Web UI extra dependency can be installed using the following code:

.. code-block:: console
$ pip install pjrpc[openapi-ui-bundles]
The following example illustrates how to configure OpenAPI specification generation
and Swagger UI web tool with basic auth:

.. code-block:: python
import uuid
from typing import Any, Optional
import flask
import flask_httpauth
import pydantic
import flask_cors
from werkzeug import security
import pjrpc.server.specs.extractors.pydantic
from pjrpc.server.integration import flask as integration
from pjrpc.server.validators import pydantic as validators
from pjrpc.server.specs import extractors, openapi as specs
app = flask.Flask('myapp')
flask_cors.CORS(app, resources={"/myapp/api/v1/*": {"origins": "*"}})
methods = pjrpc.server.MethodRegistry()
validator = validators.PydanticValidator()
auth = flask_httpauth.HTTPBasicAuth()
credentials = {"admin": security.generate_password_hash("admin")}
@auth.verify_password
def verify_password(username: str, password: str) -> Optional[str]:
if username in credentials and security.check_password_hash(credentials.get(username), password):
return username
class AuthenticatedJsonRPC(integration.JsonRPC):
@auth.login_required
def _rpc_handle(self) -> flask.Response:
return super()._rpc_handle()
class JSONEncoder(pjrpc.JSONEncoder):
def default(self, o: Any) -> Any:
if isinstance(o, pydantic.BaseModel):
return o.dict()
if isinstance(o, uuid.UUID):
return str(o)
return super().default(o)
class UserIn(pydantic.BaseModel):
"""
User registration data.
"""
name: str
surname: str
age: int
class UserOut(UserIn):
"""
Registered user data.
"""
id: uuid.UUID
class AlreadyExistsError(pjrpc.exc.JsonRpcError):
"""
User already registered error.
"""
code = 2001
message = "user already exists"
class NotFoundError(pjrpc.exc.JsonRpcError):
"""
User not found error.
"""
code = 2002
message = "user not found"
@specs.annotate(
tags=['users'],
errors=[AlreadyExistsError],
examples=[
specs.MethodExample(
summary="Simple example",
params=dict(
user={
'name': 'Alex',
'surname': 'Smith',
'age': 25,
},
),
result={
'id': 'c47726c6-a232-45f1-944f-60b98966ff1b',
'name': 'Alex',
'surname': 'Smith',
'age': 25,
},
),
],
)
@methods.add
@validator.validate
def add_user(user: UserIn) -> UserOut:
"""
Creates a user.
:param object user: user data
:return object: registered user
:raise AlreadyExistsError: user already exists
"""
for existing_user in flask.current_app.users_db.values():
if user.name == existing_user.name:
raise AlreadyExistsError()
user_id = uuid.uuid4().hex
flask.current_app.users_db[user_id] = user
return UserOut(id=user_id, **user.dict())
@specs.annotate(
tags=['users'],
errors=[NotFoundError],
examples=[
specs.MethodExample(
summary='Simple example',
params=dict(
user_id='c47726c6-a232-45f1-944f-60b98966ff1b',
),
result={
'id': 'c47726c6-a232-45f1-944f-60b98966ff1b',
'name': 'Alex',
'surname': 'Smith',
'age': 25,
},
),
],
)
@methods.add
@validator.validate
def get_user(user_id: uuid.UUID) -> UserOut:
"""
Returns a user.
:param object user_id: user id
:return object: registered user
:raise NotFoundError: user not found
"""
user = flask.current_app.users_db.get(user_id)
if not user:
raise NotFoundError()
return UserOut(**user.dict())
@specs.annotate(
tags=['users'],
errors=[NotFoundError],
examples=[
specs.MethodExample(
summary='Simple example',
params=dict(
user_id='c47726c6-a232-45f1-944f-60b98966ff1b',
),
result=None,
),
],
)
@methods.add
@validator.validate
def delete_user(user_id: uuid.UUID) -> None:
"""
Deletes a user.
:param object user_id: user id
:raise NotFoundError: user not found
"""
user = flask.current_app.users_db.pop(user_id, None)
if not user:
raise NotFoundError()
json_rpc = AuthenticatedJsonRPC(
'/api/v1',
json_encoder=JSONEncoder,
spec=specs.OpenAPI(
info=specs.Info(version="1.0.0", title="User storage"),
servers=[
specs.Server(
url='http://127.0.0.1:8080',
),
],
security_schemes=dict(
basicAuth=specs.SecurityScheme(
type=specs.SecuritySchemeType.HTTP,
scheme='basic',
),
),
security=[
dict(basicAuth=[])
],
schema_extractor=extractors.pydantic.PydanticSchemaExtractor(),
ui=specs.SwaggerUI(),
# ui=specs.RapiDoc(),
# ui=specs.ReDoc(),
),
)
json_rpc.dispatcher.add_methods(methods)
app.users_db = {}
myapp = flask.Blueprint('myapp', __name__, url_prefix='/myapp')
json_rpc.init_app(myapp)
app.register_blueprint(myapp)
if __name__ == "__main__":
app.run(port=8080)
Specification is available on http://localhost:8080/myapp/api/v1/openapi.json

Web UI is running on http://localhost:8080/myapp/api/v1/ui/

Swagger UI:
~~~~~~~~~~~

.. image:: docs/source/_static/swagger-ui-screenshot.png
:width: 1024
:alt: Open API full example

RapiDoc:
~~~~~~~~

.. image:: docs/source/_static/rapidoc-screenshot.png
:width: 1024
:alt: Open API cli example

Redoc:
~~~~~~

.. image:: docs/source/_static/redoc-screenshot.png
:width: 1024
:alt: Open API method example
Binary file added docs/source/_static/rapidoc-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/redoc-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/swagger-ui-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ Features:
- :doc:`popular frameworks integration <pjrpc/server>` (aiohttp, flask, kombu, aio_pika)
- :doc:`builtin parameter validation <pjrpc/validation>`
- :doc:`pytest integration <pjrpc/testing>`
- :doc:`openapi schema generation support <pjrpc/specification>`
- :doc:`openrpc schema generation support <pjrpc/specification>`
- :doc:`web ui support (SwaggerUI, RapiDoc, ReDoc) <pjrpc/webui>`


Extra requirements
Expand All @@ -48,6 +51,7 @@ Extra requirements
- `pydantic <https://pydantic-docs.helpmanual.io/>`_
- `requests <https://requests.readthedocs.io>`_
- `httpx <https://www.python-httpx.org/>`_
- `openapi-ui-bundles <https://github.com/dapper91/python-openapi-ui-bundles>`_


The User Guide
Expand All @@ -65,6 +69,8 @@ The User Guide
pjrpc/extending
pjrpc/testing
pjrpc/tracing
pjrpc/specification
pjrpc/webui
pjrpc/examples


Expand Down
32 changes: 32 additions & 0 deletions docs/source/pjrpc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,35 @@ ________

.. automodule:: pjrpc.server.validators.pydantic
:members:


Specification
~~~~~~~~~~~~~

.. automodule:: pjrpc.server.specs
:members:

extractors
__________

.. automodule:: pjrpc.server.specs.extractors
:members:


.. automodule:: pjrpc.server.specs.extractors.pydantic
:members:


.. automodule:: pjrpc.server.specs.extractors.docstring
:members:

schemas
_______


.. automodule:: pjrpc.server.specs.openapi
:members:


.. automodule:: pjrpc.server.specs.openrpc
:members:
Loading

0 comments on commit 464018d

Please sign in to comment.