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

Related tags

Flask public acsbp
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
Flask + marshmallow for beautiful APIs

Flask-Marshmallow Flask + marshmallow for beautiful APIs Flask-Marshmallow is a thin integration layer for Flask (a Python web framework) and marshmal

marshmallow-code 659 Feb 15, 2021
Pf-flask-rest-com - Flask REST API Common Implementation by Problem Fighter Library

In the name of God, the Most Gracious, the Most Merciful. PF-Flask-Rest-Com Docu

Problem Fighter 3 Jan 15, 2022
flask-apispec MIT flask-apispec (🥉24 · ⭐ 520) - Build and document REST APIs with Flask and apispec. MIT

flask-apispec flask-apispec is a lightweight tool for building REST APIs in Flask. flask-apispec uses webargs for request parsing, marshmallow for res

Joshua Carp 617 Dec 30, 2022
SqlAlchemy Flask-Restful Swagger Json:API OpenAPI

SAFRS: Python OpenAPI & JSON:API Framework Overview Installation JSON:API Interface Resource Objects Relationships Methods Custom Methods Class Method

Thomas Pollet 365 Jan 6, 2023
Flask RESTful Web services using API to communicate between client and server.

Welcome! Open up two terminals, one for client and for server each Terminal 1 Terminal 2 Now navigate to the CW2_code directory in both like so $ cd C

Sehra Elahi 1 Nov 23, 2021
Learn REST API with Flask, Mysql and Docker

Learn REST API with Flask, Mysql and Docker A project for you to learn to work a flask REST api with docker and the mysql database manager! Table of C

Aldo Matus 0 Jul 31, 2021
REST API with Flask and SQLAlchemy. I would rather not use it anymore.

Flask REST API Python 3.9.7 The Flask experience, without data persistence :D First, to install all dependencies: python -m pip install -r requirement

Luis Quiñones Requelme 1 Dec 15, 2021
REST API with mongoDB and Flask.

Flask REST API with mongoDB py 3.10 First, to install all dependencies: python -m pip install -r requirements.txt Second, into the ./src/ folder, cop

Luis Quiñones Requelme 3 Mar 5, 2022
Curso Desenvolvimento avançado Python com Flask e REST API

Curso Desenvolvimento avançado Python com Flask e REST API Curso da Digital Innovation One Professor: Rafael Galleani Conteudo do curso Introdução ao

Elizeu Barbosa Abreu 1 Nov 14, 2021
REST API built using flask framework that used for managing bookmarks by individual users.

Bookmarks REST API REST API built using flask framework that used for managing bookmarks by individual users. API Consumers Note This app is built usi

Venkatesh Tantravahi 1 Dec 27, 2021
RestApi_flask_sql.alchemy - Product REST API With Flask & SQL Alchemy

REST API With Flask & SQL Alchemy Products API using Python Flask, SQL Alchemy and Marshmallow Quick Start Using Pipenv # Activate venv $ pipenv shell

amirwahla 1 Jan 1, 2022
Brandnew-flask is a CLI tool used to generate a powerful and mordern flask-app that supports the production environment.

Brandnew-flask is still in the initial stage and needs to be updated and improved continuously. Everyone is welcome to maintain and improve this CLI.

brandonye 4 Jul 17, 2022
Flask pre-setup architecture. This can be used in any flask project for a faster and better project code structure.

Flask pre-setup architecture. This can be used in any flask project for a faster and better project code structure. All the required libraries are already installed easily to use in any big project.

Ajay kumar sharma 5 Jun 14, 2022
Flask-Bcrypt is a Flask extension that provides bcrypt hashing utilities for your application.

Flask-Bcrypt Flask-Bcrypt is a Flask extension that provides bcrypt hashing utilities for your application. Due to the recent increased prevelance of

Max Countryman 310 Dec 14, 2022
Flask-Bcrypt is a Flask extension that provides bcrypt hashing utilities for your application.

Flask-Bcrypt Flask-Bcrypt is a Flask extension that provides bcrypt hashing utilities for your application. Due to the recent increased prevelance of

Max Countryman 282 Feb 11, 2021
Flask-Starter is a boilerplate starter template designed to help you quickstart your Flask web application development.

Flask-Starter Flask-Starter is a boilerplate starter template designed to help you quickstart your Flask web application development. It has all the r

Kundan Singh 259 Dec 26, 2022
Flask Project Template A full feature Flask project template.

Flask Project Template A full feature Flask project template. See also Python-Project-Template for a lean, low dependency Python app. HOW TO USE THIS

Bruno Rocha 96 Dec 23, 2022
A Fast API style support for Flask. Gives you MyPy types with the flexibility of flask

Flask-Fastx Flask-Fastx is a Fast API style support for Flask. It Gives you MyPy types with the flexibility of flask. Compatibility Flask-Fastx requir

Tactful.ai 18 Nov 26, 2022
Flask-app scaffold, generate flask restful backend

Flask-app scaffold, generate flask restful backend

jacksmile 1 Nov 24, 2021