Flask-Rebar combines flask, marshmallow, and swagger for robust REST services.

Overview

Flask-Rebar

Documentation Status CI Status PyPI status Code style Code of Conduct

Flask-Rebar combines flask, marshmallow, and swagger for robust REST services.

Features

  • Request and Response Validation - Flask-Rebar relies on schemas from the popular Marshmallow package to validate incoming requests and marshal outgoing responses.
  • Automatic Swagger Generation - The same schemas used for validation and marshaling are used to automatically generate OpenAPI specifications (a.k.a. Swagger). This also means automatic documentation via Swagger UI.
  • Error Handling - Uncaught exceptions from Flask-Rebar are converted to appropriate HTTP errors.

Example

from flask import Flask
from flask_rebar import errors, Rebar
from marshmallow import fields, Schema

from my_app import database


rebar = Rebar()

# All handler URL rules will be prefixed by '/v1'
registry = rebar.create_handler_registry(prefix='/v1')

class TodoSchema(Schema):
    id = fields.Integer()
    complete = fields.Boolean()
    description = fields.String()

# This schema will validate the incoming request's query string
class GetTodosQueryStringSchema(Schema):
    complete = fields.Boolean()

# This schema will marshal the outgoing response
class GetTodosResponseSchema(Schema):
    data = fields.Nested(TodoSchema, many=True)


@registry.handles(
    rule='/todos',
    method='GET',
    query_string_schema=GetTodosQueryStringSchema(),
    response_body_schema=GetTodosResponseSchema(), # for versions <= 1.7.0, use marshal_schema
)
def get_todos():
    """
    This docstring will be rendered as the operation's description in
    the auto-generated OpenAPI specification.
    """
    # The query string has already been validated by `query_string_schema`
    complete = rebar.validated_args.get('complete')

    ...

    # Errors are converted to appropriate HTTP errors
    raise errors.Forbidden()

    ...

    # The response will be marshaled by `marshal_schema`
    return {'data': []}


def create_app(name):
    app = Flask(name)
    rebar.init_app(app)
    return app


if __name__ == '__main__':
    create_app(__name__).run()

For a more complete example, check out the example app at examples/todo.py. Some example requests to this example app can be found at examples/todo_output.md.

Installation

pip install flask-rebar

Documentation

More extensive documentation can be found here.

Extensions

Flask-Rebar is extensible! Here are some open source extensions:

Contributing

There is still work to be done, and contributions are encouraged! Check out the contribution guide for more information.

Comments
  • DELETE requests should return specified Content-Type

    DELETE requests should return specified Content-Type

    DELETE requests are returning Content-Type: text/html, this is breaking some clients that expect application/json, even though there is no actual content, simply the type in the header broke a lookup. Looks like this was introduced here, as text/html is the Flask response default mimetype.

    Rolling that back here and adding a test.

    opened by joeb1415 20
  • Prepare for Marshmallow 3

    Prepare for Marshmallow 3

    https://github.com/plangrid/flask-rebar/blob/master/setup.py#L19 specifies marshmallow>=2.13. Right now this is picking up Marshmallow 2, but as soon as Marshmallow 3 final comes out, this will start picking up v3, which has several backwards-incompatible changes:

    https://marshmallow.readthedocs.io/en/3.0/changelog.html

    Are any updates necessary before then?

    v2.0 triaged swagger-generation validation 
    opened by twosigmajab 20
  • Create a more complete example

    Create a more complete example

    I just discovered flask-rebar and I think it could very well become the new standard for REST with flask. The documentation is very well made, but a more complete "real-life" example/template would help a lot to get started. I am creating one for my project that i could share, but I don't know all the best practices for the framework. Keep up the good work, cheers!

    wip 
    opened by Sytten 19
  • Fix bug that prevents returning `Flask.Response`s.

    Fix bug that prevents returning `Flask.Response`s.

    https://flask-rebar.readthedocs.io/en/latest/quickstart/basics.html#marshaling currently documents the following: "This means the if response_body_schema is None, the return value must be a return value that Flask supports, e.g. a string or a Flask.Response object.

    However, Flask-Rebar currently fails to interoperate properly with a Flask.Response object, as demonstrated by the included test. Without this bugfix to rebar.py, the included test fails with KeyError: 200. This is because the code currently passes rv to _unpack_view_func_return_value, which always returns a 200 status when rv is not a tuple, not realizing that a Flask.Response instance can get passed through as rv which can have a non-200 .status_code (and also custom .headers as well).

    opened by twosigmajab 13
  • Adding pep8 compliance test case?

    Adding pep8 compliance test case?

    Since this came up in the discussion, would it be a good idea to just jump into the deep and make add a test to ensure pep8 compliance going forward? Do people have a strong opinion about pep8 settings or should we just go with the vanilla version for now?

    Thoughts?

    opened by kazamatzuri 12
  • BP-763: Add support for multiple authenticators

    BP-763: Add support for multiple authenticators

    Corresponds to Issue #121

    • Extended deprecated_parameters decorator to allow a coercion function to convert between old and new styles of parameters.
    • Adds support for handlers to have multiple Authenticators.
    • Adds support for registries to have multiple Authenticators as their default authentication.
    • Should mark authenticator parameters as deprecated using the deprecation utils

    JIRA Tickets | --------------| BP-763|

    enhancement 
    opened by airstandley 11
  • Customize error returned in Flask-rebar

    Customize error returned in Flask-rebar

    Today we discussed creating our own class of errors because we wanted to have uniforms errors with a machine parsable type. Though we could add it just fine with the additional_data field, it is a bit painful to enforce it on every error thrown. The problem is that we can't currently modify the default errors that Flask-Rebar raise in the case of an invalid schema for example. I don't have a clear answer as to how we should handle this, but I figured I could ask if someone had an idea as to how we could do this.

    v2.0 triaged 
    opened by Sytten 11
  • Integrate marshmallow-objects

    Integrate marshmallow-objects

    One annoying thing about marshmallow is the fast that you get a dictionary instead of an object with attributes (I am bit jealous of pydantic). We started using marshmallow-objects in our project. Since we already define some custom schemas, it would be very valuable to either use this lib or make something similar. Especially since we are going toward a v2 with some breaking changes anyway.

    enhancement v2.0 triaged v2-breaking-change 
    opened by Sytten 10
  • sort required array

    sort required array

    I believe obj.fields.items(): does not consistently return objects in the same order. When committing the generated swagger.json file, it causes some redundant diffs in the required fields array

    e.g: https://github.com/plangrid/informant/pull/419

    opened by BrandonWeng 10
  • registry.add_supplemental_schemas

    registry.add_supplemental_schemas

    Add registry.add_supplemental_schemas function to export supplemental schemas to swagger, in addition to those used by the API handlers.

    In my app, I have a handler:

    @registry.handles(
        marshal_schema={201: FooSchema}
    )
    def create_foo():
        pass
    

    With:

    class FooSchema(Schema):
        template = fields.String()
        vars = fields.Dict()
    

    Elsewhere, I validate the vars against the template according to various template schemas:

    class Template_1_Schema(Schema):
        var_1 = fields.String()
        var_2 = fields.String()
    
    class Template_2_Schema(Schema):
        var_3 = fields.Boolean()
        var_4 = fields.Integer()
    

    I would like to export these template schemas in the swagger definition, so others can know what to expect to send to my handler in the vars dictionary param.

    This PR introduces a registry function add_supplemental_schemas to support this. The function can accept a single schema, or for convenience, a directory containing all the supplemental schemas.

    cc: @barakalon

    opened by joeb1415 10
  • Allow disabling OrderedDicts in generated swagger

    Allow disabling OrderedDicts in generated swagger

    If you try to yaml.dump a rebar-generated swagger object, you end up with yaml that looks like this:

    !!python/object/apply:collections.OrderedDict
    - - - consumes
        - [application/json]
      - - definitions
        - !!python/object/apply:collections.OrderedDict
          - - - Error
              - !!python/object/apply:collections.OrderedDict
                - - - properties
                    - !!python/object/apply:collections.OrderedDict
                      - - - errors
                          - !!python/object/apply:collections.OrderedDict
                            - - [type, object]
                        - - message
                          - !!python/object/apply:collections.OrderedDict
                            - - [type, string]
                  - - required
                    - [message]
                  - [title, Error]
                  - [type, object]
    ...
    

    It looks like this is due to the current behavior of unconditionally converting dicts to OrderedDicts.

    This PR allows passing order_dicts=False to swagger_generator.generate(...) to suppress the conversion.

    This may also be desired by users who want to avoid the extra work if they're using a Python implementation where dicts preserve insertion order already (e.g. PyPy or CPython >= 3.6).

    opened by twosigmajab 10
  • Flask signals not triggered on unhandled exceptions

    Flask signals not triggered on unhandled exceptions

    Flask has a signal got_request_exception intended to signify that an unhandled exception occurred in a handler. This signal is triggered by the default error handler, ref: https://github.com/pallets/flask/blob/0d8c8ba71bc6362e6ea9af08146dc97e1a0a8abc/src/flask/app.py#L1675. This means that to Flask, adding error handlers is equivalent to catching an exception; their documentation attempts to explains this, but it can be easy to miss the implication that the exception signal is not sent if a registered error handler matches the exception.

    Existing instrumentation/observability tooling uses this signal to detect when an unexpected error has occurred in a service.

    Rebar currently registers a generic Exception error handler, ref: https://github.com/plangrid/flask-rebar/blob/63922a17379a5b5a99c7422b7f7b881f8c08eb13/flask_rebar/rebar.py#L818-L827.

    This causes tools/plugins that rely on the got_request_exception signal to fail to work with a Rebar service.

    Rebar should either

    1. Switch the generic error handler to register for InternalServerError; meaning that the rebar handler would only run after the default flask handler had run and sent the got_request_exception signal.
    2. Send the got_request_exception signal as part of it's generic exception handling.
    bug error-handling 
    opened by airstandley 0
  • marshmallow-to-swagger RecursionError if schema has self references

    marshmallow-to-swagger RecursionError if schema has self references

    I am getting a RecursionError trying to generate swagger with this schema:

    class ZipStructureRowSchema(Schema):
        name = fields.String(required=True)
        sub_rows = fields.List(
            fields.Nested("ZipStructureRowSchema"), load_from="subRows", dump_to="subRows"
        )
    
    
    class ZipPreviewResponseSchema(Schema):
        preview = fields.List(fields.Nested(ZipStructureRowSchema))
    

    As far as I can tell, this is valid according to Marshmallow. Thank you!

    opened by AutodeskAbe 1
  • AttributeError: 'AuthenticatorConverterRegistry' object has no attribute 'get_security_schemes_legacy'

    AttributeError: 'AuthenticatorConverterRegistry' object has no attribute 'get_security_schemes_legacy'

    I have a custom authenticator that works, but I am trying to get the swagger docs to work as well but I keep getting the above exception.

    Here is my custom authenticator:

    class CustomAuthenticator(authenticators.Authenticator):
        def authenticate(self):
            if current_user and current_user.is_authenticated:
                pass
            elif current_app.config["AUTH_OFF"]:
                try:
                    login_user(default_user)
                except AttributeError:
                    raise ValueError("Trying to log into user that doesn't exist!")
    

    This part I took directly from the flask_rebar docs:

    class SwaggerAuthConverter(AuthenticatorConverter):
        AUTHENTICATOR_TYPE = CustomAuthenticator
    
        def get_security_schemes(self, obj, context):
            return {
                obj.name: {sw.type_: sw.api_key, sw.in_: sw.header, sw.name: obj.header}
            }
    
        def get_security_requirements(self, obj, context):
            return [{obj.name: []}]
    
    
    custom_auth_registry = AuthenticatorConverterRegistry()
    custom_auth_registry.register_type(SwaggerAuthConverter())
    

    Finally, this is where I set up the app:

    rebar = Rebar()
    swagger_generator = SwaggerV2Generator(authenticator_converter_registry=custom_auth_registry)
    registry = rebar.create_handler_registry(swagger_generator=swagger_generator)
    registry.set_default_authenticator(CustomAuthenticator)
    

    Here is the full backtrace:

    127.0.0.1 - - [04/Nov/2021 09:33:13] "GET /swagger HTTP/1.0" 500 -
    Traceback (most recent call last):
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask/app.py", line 2464, in __call__
        return self.wsgi_app(environ, start_response)
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask/app.py", line 2450, in wsgi_app
        response = self.handle_exception(e)
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask/app.py", line 1867, in handle_exception
        reraise(exc_type, exc_value, tb)
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
        raise value
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask/app.py", line 2447, in wsgi_app
        response = self.full_dispatch_request()
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask/app.py", line 1952, in full_dispatch_request
        rv = self.handle_user_exception(e)
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask/app.py", line 1822, in handle_user_exception
        return handler(e)
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask_rebar/rebar.py", line 831, in handle_generic_error
        raise error
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask/app.py", line 1950, in full_dispatch_request
        rv = self.dispatch_request()
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask/app.py", line 1936, in dispatch_request
        return self.view_functions[rule.endpoint](**req.view_args)
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask_rebar/rebar.py", line 612, in get_swagger
        registry=self, host=request.host_url.rstrip("/")
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask_rebar/swagger_generation/swagger_generator_v2.py", line 100, in generate_swagger
        return self.generate(registry=registry, host=host)
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask_rebar/swagger_generation/swagger_generator_v2.py", line 131, in generate
        self.authenticator_converter.get_security_schemes(authenticator)
      File "/home/canopy/canopy/venv/lib/python3.6/site-packages/flask_rebar/swagger_generation/authenticator_to_swagger.py", line 168, in get_security_schemes
        return self.get_security_schemes_legacy(registry=authenticator)
    AttributeError: 'AuthenticatorConverterRegistry' object has no attribute 'get_security_schemes_legacy'
    
    bug 
    opened by mattcarp12 5
  • Feature: Add SchemaOpts to opt-in to validation

    Feature: Add SchemaOpts to opt-in to validation

    Currently (technically, when 2.0.1 is merged and released) it is possible to opt in to response validation at the schema level by inclusion of `RequireOnDumpMixin. While this works, a more natural fit would be to allow this to be directly specified in the schema definition itself by including a "custom class Meta option" as described here: https://marshmallow.readthedocs.io/en/stable/extending.html

    opened by RookieRick 0
  • Rebar can generate invalid OpenAPI specs: non-unique operationId's

    Rebar can generate invalid OpenAPI specs: non-unique operationId's

    https://swagger.io/docs/specification/paths-and-operations/#operationid

    operationId is an optional unique string used to identify an operation. If provided, these IDs must be unique among all operations described in your API.

    This is important because things downstream of the spec (such as generated clients) may use the operationId (e.g. to code-generate methods) in such a way that depends on unique operationIds.

    However, it looks like Rebar's OpenAPI spec generation uses the function name to generate operationIds, which need not be unique:

    # my_rebar_registry.py 
    from flask_rebar import Rebar
    
    rebar = Rebar()
    registry = rebar.create_handler_registry(prefix="/api")
    
    # handlers1.py 
    from my_rebar_registry import registry
    
    @registry.handles(
        rule="/foo",
    )
    def foo():
        return "foo"
    
    # handlers2.py 
    from my_rebar_registry import registry
    
    @registry.handles(
        rule="/bar",
    )
    def foo():
        return "bar"
    
    # my_api_spec.py
    import json
    
    from my_rebar_registry import registry
    import handlers1
    import handlers2
    
    if __name__ == "__main__":
        print(json.dumps(registry.swagger_generator.generate_swagger(registry), indent=2))
    
    > python -m my_api_spec | grep operationId  # thither be duplicates
            "operationId": "foo",
            "operationId": "foo",
    
    (Click for full spec output)
    {
      "consumes": [
        "application/json"
      ],
      "definitions": {
        "Error": {
          "properties": {
            "errors": {
              "type": "object"
            },
            "message": {
              "type": "string"
            }
          },
          "required": [
            "message"
          ],
          "title": "Error",
          "type": "object"
        }
      },
      "host": "localhost",
      "info": {
        "description": "",
        "title": "My API",
        "version": "1.0.0"
      },
      "paths": {
        "/api/bar": {
          "get": {
            "operationId": "foo",
            "responses": {
              "default": {
                "description": "Error",
                "schema": {
                  "$ref": "#/definitions/Error"
                }
              }
            }
          }
        },
        "/api/foo": {
          "get": {
            "operationId": "foo",
            "responses": {
              "default": {
                "description": "Error",
                "schema": {
                  "$ref": "#/definitions/Error"
                }
              }
            }
          }
        }
      },
      "produces": [
        "application/json"
      ],
      "schemes": [],
      "securityDefinitions": {},
      "swagger": "2.0"
    }
    

    This may not be a terribly big deal in practice, since the moment you try to use such a registry with a Flask app, Flask's own duplication checking (of endpoint names) prevents you from doing so:

    # app.py 
    from flask import Flask
    
    from my_rebar_registry import rebar
    import handlers1
    import handlers2
    
    app = Flask("app")
    rebar.init_app(app)  # kaboom
    
    > python -m app
    Traceback (most recent call last):
      File "/usr/local/Cellar/[email protected]/3.9.6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 197, in _run_module_as_main
        return _run_code(code, main_globals, None,
      File "/usr/local/Cellar/[email protected]/3.9.6/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code
        exec(code, run_globals)
      File "/Users/jab/tmp/rebarv2/app.py", line 9, in <module>
        rebar.init_app(app)  # kaboom
      File "/Users/jab/tmp/rebarv2/.venv/lib/python3.9/site-packages/flask_rebar/rebar.py", line 784, in init_app
        registry.register(app=app)
      File "/Users/jab/tmp/rebarv2/.venv/lib/python3.9/site-packages/flask_rebar/rebar.py", line 554, in register
        self._register_routes(app=app)
      File "/Users/jab/tmp/rebarv2/.venv/lib/python3.9/site-packages/flask_rebar/rebar.py", line 576, in _register_routes
        app.add_url_rule(
      File "/Users/jab/tmp/rebarv2/.venv/lib/python3.9/site-packages/flask/app.py", line 98, in wrapper_func
        return f(self, *args, **kwargs)
      File "/Users/jab/tmp/rebarv2/.venv/lib/python3.9/site-packages/flask/app.py", line 1282, in add_url_rule
        raise AssertionError(
    AssertionError: View function mapping is overwriting an existing endpoint function: api.foo
    

    However, it still seems preferable to make Rebar's OpenAPI spec generation smart enough to not generate invalid specs in this way. I think Rebar should instead raise an error telling the user to provide a unique function name to avoid generating duplicate operationIds, or perhaps better yet, the @registry.handles() decorators (and so forth) could accept an operation_id param that allows the user to explicitly set the associated operationId that gets generated into the spec, making the operationId no longer tied to the Python function name 1-to-1. (A non-option, IMO, would be for Rebar to silently append some number to deduplicate what would otherwise be a duplicate operationId, which I've seen some Swagger tooling do, and it causes all kinds of madness.)

    opened by jab 0
Releases(v2.2.1)
Owner
PlanGrid
PlanGrid
Python Advanced --- numpy, decorators, networking

Python Advanced --- numpy, decorators, networking (and more?) Hello everyone ๐Ÿ‘‹ This is the project repo for the "Python Advanced - ..." introductory

Andreas Poehlmann 2 Nov 05, 2021
Comprehensive Python Cheatsheet

Comprehensive Python Cheatsheet Download text file, Buy PDF, Fork me on GitHub or Check out FAQ. Contents 1. Collections: List, Dictionary, Set, Tuple

Jefferson 1 Jan 23, 2022
Tutorial for STARKs with supporting code in python

stark-anatomy STARK tutorial with supporting code in python Outline: introduction overview of STARKs basic tools -- algebra and polynomials FRI low de

121 Jan 03, 2023
Create docsets for Dash.app-compatible API browser.

doc2dash: Create Docsets for Dash.app and Clones doc2dash is an MIT-licensed extensible Documentation Set generator intended to be used with the Dash.

Hynek Schlawack 498 Dec 30, 2022
Python solutions to solve practical business problems.

Python Business Analytics Also instead of "watching" you can join the link-letter, it's already being sent out to about 90 people and you are free to

Derek Snow 357 Dec 26, 2022
level2-data-annotation_cv-level2-cv-15 created by GitHub Classroom

[AI Tech 3๊ธฐ Level2 P Stage] ๊ธ€์ž ๊ฒ€์ถœ ๋Œ€ํšŒ ํŒ€์› ์†Œ๊ฐœ ๊น€๊ทœ๋ฆฌ_T3016 ๋ฐ•์ •ํ˜„_T3094 ์„์ง„ํ˜_T3109 ์†์ •๊ท _T3111 ์ดํ˜„์ง„_T3174 ์ž„์ข…ํ˜„_T3182 Overview OCR (Optimal Character Recognition) ๊ธฐ์ˆ 

6 Jun 10, 2022
Portfolio project for Code Institute Full Stack software development course.

Comic Sales tracker This project is the third milestone project for the Code Institute Diploma in Full Stack Software Development. You can see the fin

1 Jan 10, 2022
30 days of Python programming challenge is a step-by-step guide to learn the Python programming language in 30 days

30 days of Python programming challenge is a step-by-step guide to learn the Python programming language in 30 days. This challenge may take more than100 days, follow your own pace.

Asabeneh 17.7k Jan 07, 2023
Fastest Git client for Emacs.

EAF Git Client EAF Git is git client application for the Emacs Application Framework. The advantages of EAF Git are: Large log browse: support 1 milli

Emacs Application Framework 31 Dec 02, 2022
๐Ÿ“š Papers & tech blogs by companies sharing their work on data science & machine learning in production.

applied-ml Curated papers, articles, and blogs on data science & machine learning in production. โš™๏ธ Figuring out how to implement your ML project? Lea

Eugene Yan 22.1k Jan 03, 2023
Documentation for the lottie file format

Lottie Documentation This repository contains both human-readable and machine-readable documentation about the Lottie format The documentation is avai

LottieFiles 25 Jan 05, 2023
Showing potential issues with merge strategies

Showing potential issues with merge strategies Context There are two branches in this repo: main and a feature branch feat/inverting-method (not the b

Rubรฉn 2 Dec 20, 2021
1.3k Jan 08, 2023
A web app builds using streamlit API with python backend to analyze and pick insides from multiple data formats.

Data-Analysis-Web-App Data Analysis Web App can analysis data in multiple formates(csv, txt, xls, xlsx, ods, odt) and gives shows you the analysis in

Kumar Saksham 19 Dec 09, 2022
Data Inspector is an open-source python library that brings 15++ types of different functions to make EDA, data cleaning easier.

Data Inspector Data Inspector is an open-source python library that brings 15 types of different functions to make EDA, data cleaning easier. Author:

Kazi Amit Hasan 38 Nov 24, 2022
A simple tutorial to get you started with Discord and it's Python API

Hello there Feel free to fork and star, open issues if there are typos or you have a doubt. I decided to make this post because as a newbie I never fo

Sachit 1 Nov 01, 2021
[Unofficial] Python PEP in EPUB format

PEPs in EPUB format This is a unofficial repository where I stock all valid PEPs in the EPUB format. Repository Cloning git clone --recursive Mickaรซl Schoentgen 9 Oct 12, 2022

Documentation generator for C++ based on Doxygen and mosra/m.css.

mosra/m.css is a Doxygen-based documentation generator that significantly improves on Doxygen's default output by controlling some of Doxygen's more unruly options, supplying it's own slick HTML+CSS

Mark Gillard 109 Dec 07, 2022
step by step guide for beginners for getting started with open source

Step-by-Step Guide for beginners for getting started with Open-Source Here The Contribution Begins ๐Ÿ’ป If you are a beginner then this repository is fo

Arpit Jain 66 Jan 03, 2023
A tool that allows for versioning sites built with mkdocs

mkdocs-versioning mkdocs-versioning is a plugin for mkdocs, a tool designed to create static websites usually for generating project documentation. mk

Zayd Patel 38 Feb 26, 2022