GithubHelp home page GithubHelp logo

identixone / fastapi_contrib Goto Github PK

View Code? Open in Web Editor NEW
601.0 8.0 29.0 220 KB

Opinionated set of utilities on top of FastAPI

Home Page: https://fastapi-contrib.readthedocs.io

License: MIT License

Makefile 1.80% Python 98.20%
fastapi pydantic ujson mongodb fastapi-template starlette python

fastapi_contrib's Introduction

FastAPI Contrib

image

image

Documentation Status

Updates

Opinionated set of utilities on top of FastAPI

Features

  • Auth Backend & Middleware (User or None in every request object)
  • Permissions: reusable class permissions, specify multiple as FastAPI Dependency
  • ModelSerializers: serialize (pydantic) incoming request, connect data with DB model and save
  • UJSONResponse: correctly show slashes in fields with URLs
  • Limit-Offset Pagination: use it as FastAPI Dependency (works only with ModelSerializers for now)
  • MongoDB integration: Use models as if it was Django (based on pydantic models)
  • MongoDB indices verification on startup of the app
  • Custom Exceptions and Custom Exception Handlers
  • Opentracing middleware & setup utility with Jaeger tracer + root span available in every Request's state
  • StateRequestIDMiddleware: receives configurable header and saves it in request state

Roadmap

See GitHub Project Roadmap.

Installation

To install just Contrib (without mongodb, pytz, ujson):

$ pip install fastapi_contrib

To install contrib with mongodb support:

$ pip install fastapi_contrib[mongo]

To install contrib with ujson support:

$ pip install fastapi_contrib[ujson]

To install contrib with pytz support:

$ pip install fastapi_contrib[pytz]

To install contrib with opentracing & Jaeger tracer:

$ pip install fastapi_contrib[jaegertracing]

To install everything:

$ pip install fastapi_contrib[all]

Usage

To use Limit-Offset pagination:

from fastapi import FastAPI
from fastapi_contrib.pagination import Pagination
from fastapi_contrib.serializers.common import ModelSerializer
from yourapp.models import SomeModel

app = FastAPI()

class SomeSerializer(ModelSerializer):
    class Meta:
        model = SomeModel

@app.get("/")
async def list(pagination: Pagination = Depends()):
    filter_kwargs = {}
    return await pagination.paginate(
        serializer_class=SomeSerializer, **filter_kwargs
    )

Subclass this pagination to define custom default & maximum values for offset & limit:

class CustomPagination(Pagination):
    default_offset = 90
    default_limit = 1
    max_offset = 100
    max_limit = 2000

To use State Request ID Middleware:

from fastapi import FastAPI
from fastapi_contrib.common.middlewares import StateRequestIDMiddleware

app = FastAPI()

@app.on_event('startup')
async def startup():
    app.add_middleware(StateRequestIDMiddleware)

To use Authentication Middleware:

from fastapi import FastAPI
from fastapi_contrib.auth.backends import AuthBackend
from fastapi_contrib.auth.middlewares import AuthenticationMiddleware

app = FastAPI()

@app.on_event('startup')
async def startup():
    app.add_middleware(AuthenticationMiddleware, backend=AuthBackend())

Define & use custom permissions based on FastAPI Dependency framework:

from fastapi import FastAPI
from fastapi_contrib.permissions import BasePermission, PermissionsDependency

class TeapotUserAgentPermission(BasePermission):

    def has_required_permissions(self, request: Request) -> bool:
        return request.headers.get('User-Agent') == "Teapot v1.0"

app = FastAPI()

@app.get(
    "/teapot/",
    dependencies=[Depends(
        PermissionsDependency([TeapotUserAgentPermission]))]
)
async def teapot() -> dict:
    return {"teapot": True}

Setup uniform exception-handling:

from fastapi import FastAPI
from fastapi_contrib.exception_handlers import setup_exception_handlers

app = FastAPI()

@app.on_event('startup')
async def startup():
    setup_exception_handlers(app)

If you want to correctly handle scenario when request is an empty body (IMPORTANT: non-multipart):

from fastapi import FastAPI
from fastapi_contrib.routes import ValidationErrorLoggingRoute

app = FastAPI()
app.router.route_class = ValidationErrorLoggingRoute

Or if you use multiple routes for handling different namespaces (IMPORTANT: non-multipart):

from fastapi import APIRouter, FastAPI
from fastapi_contrib.routes import ValidationErrorLoggingRoute

app = FastAPI()

my_router = APIRouter(route_class=ValidationErrorLoggingRoute)

To correctly show slashes in fields with URLs + ascii locking:

from fastapi import FastAPI
from fastapi_contrib.common.responses import UJSONResponse

app = FastAPI()

@app.get("/", response_class=UJSONResponse)
async def root():
    return {"a": "b"}

Or specify it as default response class for the whole app (FastAPI >= 0.39.0):

from fastapi import FastAPI
from fastapi_contrib.common.responses import UJSONResponse

app = FastAPI(default_response_class=UJSONResponse)

To setup Jaeger tracer and enable Middleware that captures every request in opentracing span:

from fastapi import FastAPI
from fastapi_contrib.tracing.middlewares import OpentracingMiddleware
from fastapi_contrib.tracing.utils import setup_opentracing

app = FastAPI()

@app.on_event('startup')
async def startup():
    setup_opentracing(app)
    app.add_middleware(OpentracingMiddleware)

To setup mongodb connection at startup and never worry about it again:

from fastapi import FastAPI
from fastapi_contrib.db.utils import setup_mongodb

app = FastAPI()

@app.on_event('startup')
async def startup():
    setup_mongodb(app)

Use models to map data to MongoDB:

from fastapi_contrib.db.models import MongoDBModel

class MyModel(MongoDBModel):
    additional_field1: str
    optional_field2: int = 42

    class Meta:
        collection = "mymodel_collection"


mymodel = MyModel(additional_field1="value")
mymodel.save()

assert mymodel.additional_field1 == "value"
assert mymodel.optional_field2 == 42
assert isinstance(mymodel.id, int)

Or use TimeStamped model with creation datetime:

from fastapi_contrib.db.models import MongoDBTimeStampedModel

class MyTimeStampedModel(MongoDBTimeStampedModel):

    class Meta:
        collection = "timestamped_collection"


mymodel = MyTimeStampedModel()
mymodel.save()

assert isinstance(mymodel.id, int)
assert isinstance(mymodel.created, datetime)

Use serializers and their response models to correctly show Schemas and convert from JSON/dict to models and back:

from fastapi import FastAPI
from fastapi_contrib.db.models import MongoDBModel
from fastapi_contrib.serializers import openapi
from fastapi_contrib.serializers.common import Serializer

from yourapp.models import SomeModel

app = FastAPI()


class SomeModel(MongoDBModel):
    field1: str


@openapi.patch
class SomeSerializer(Serializer):
    read_only1: str = "const"
    write_only2: int
    not_visible: str = "42"

    class Meta:
        model = SomeModel
        exclude = {"not_visible"}
        write_only_fields = {"write_only2"}
        read_only_fields = {"read_only1"}


@app.get("/", response_model=SomeSerializer.response_model)
async def root(serializer: SomeSerializer):
    model_instance = await serializer.save()
    return model_instance.dict()

POST-ing to this route following JSON:

{"read_only1": "a", "write_only2": 123, "field1": "b"}

Should return following response:

{"id": 1, "field1": "b", "read_only1": "const"}

Auto-creation of MongoDB indexes

Suppose we have this directory structure:

-- project_root/
     -- apps/
          -- app1/
               -- models.py (with MongoDBModel inside with indices declared)
          -- app2/
               -- models.py (with MongoDBModel inside with indices declared)

Based on this, your name of the folder with all the apps would be "apps". This is the default name for fastapi_contrib package to pick up your structure automatically. You can change that by setting ENV variable CONTRIB_APPS_FOLDER_NAME (by the way, all the setting of this package are overridable via ENV vars with CONTRIB_ prefix before them).

You also need to tell fastapi_contrib which apps to look into for your models. This is controlled by CONTRIB_APPS ENV variable, which is list of str names of the apps with models. In the example above, this would be CONTRIB_APPS=["app1","app2"].

Just use create_indexes function after setting up mongodb:

from fastapi import FastAPI
from fastapi_contrib.db.utils import setup_mongodb, create_indexes

app = FastAPI()

@app.on_event("startup")
async def startup():
    setup_mongodb(app)
    await create_indexes()

This will scan all the specified CONTRIB_APPS in the CONTRIB_APPS_FOLDER_NAME for models, that are subclassed from either MongoDBModel or MongoDBTimeStampedModel and create indices for any of them that has Meta class with indexes attribute:

models.py:

import pymongo
from fastapi_contrib.db.models import MongoDBTimeStampedModel


class MyModel(MongoDBTimeStampedModel):

    class Meta:
        collection = "mymodel"
        indexes = [
            pymongo.IndexModel(...),
            pymongo.IndexModel(...),
        ]

This would not create duplicate indices because it relies on pymongo and motor to do all the job.

Credits

This package was created with Cookiecutter and the audreyr/cookiecutter-pypackage project template.

fastapi_contrib's People

Contributors

bumagniypacket avatar gasabr avatar haider8 avatar mumtozvalijonov avatar pyup-bot avatar rsommerard avatar yarara avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fastapi_contrib's Issues

Importing models as a package and creating indexes

  • FastAPI Contrib version: 0.2.8
  • FastAPI version: 0.60.1
  • Python version: 3.7.3
  • Operating System: MacOS

Description

Describe what you were trying to get done.
Tell us what happened, what went wrong, and what you expected to happen.

As some of my model classes are big enough (~50-100) lines I wanted to divide models into files, one file for each MongoDBModel. The case is that these new models are not listed while create_index() is called, I even debugged the sources and it seems like fastapi_contrib cannot find those model classes anymore. Creating an init.py and importing those models there did not help as well.

What I Did

Investigation shows that the fastapi_contrib.db.utils.get_models() is searching for a signle *.py file to contain all the models. What could be better is, in my opinion to alter a code a bit, so that the function get_models will look like the following:

    from fastapi_contrib.db.models import MongoDBModel
    import inspect

    apps_folder_name = settings.apps_folder_name
    models = []
    for app in settings.apps:
        app_path = f"{apps_folder_name}/{app}"
        modules = [f for f in pkgutil.walk_packages(path=[app_path]) if f.name == 'models']
        if not modules:
            continue
        for module in modules:
            path_to_models = f"{apps_folder_name}.{app}.models"
            mudule = importlib.import_module(path_to_models)
            if module.ispkg:
                module_models = [x[0] for x in inspect.getmembers(mudule, inspect.isclass)]
            else:
                try:
                    module_models = pyclbr.readmodule(path_to_models).keys()
                except (AttributeError, ImportError):
                    logger.warning(
                        f"Unable to read module attributes in {path_to_models}"
                    )
                    continue
            models.extend([getattr(mudule, model) for model in module_models])

Paste the command(s) you ran and the output.
If there was a crash, please include the traceback here.

instance of child ModelSerializer not implement new methods

  • FastAPI Contrib version:
  • Python version:
  • Operating System:

Description

Instance of child ModelSerializer have not own methods, only methods of Serializer or its parent.

What I Did

class DeviceSerializer(ModelSerializer):
    def bla(self):
        return "bla"

    class Meta:
        model = Device
        exclude = ()

Optimize $set & $push for updateone, updatemany

Description

Decide whether we need $set or $push based on field type and positional arguments criteria.
There is potential room for separate primitives module as part of $or, $ne and others. Discussion is appreciated.

Allow overriding settings from fastAPI app

  • FastAPI Contrib version: 0.2.9
  • FastAPI version: 0.52.0
  • Python version: 3.8.2
  • Operating System: Linux

Description

Currently we can only override settings with settings from the fastAPI app by using environment variables (See the Todo).
Can we implement this functionaility, so we do not have to go through the environment variables?

Issue with updating a document

Hi, I am having trouble to update a document.
My model:

class MyModel(MongoDBTimeStampedModel):
    id: UUID
    other: str

Usage:

MyModel.update_one(
     {'id': UUID(id)},
     update={
            '$set': { 'other' : 'something'}
        }
)

Is it a bug in passing kwargs to await collection.update_one( filter_kwargs, kwargs, session=session ) should not it be await collection.update_one( filter_kwargs, **kwargs, session=session ) or am I getting something wrong?
Ref: Source code

fastapi version 0.65.1 compatibility

  • FastAPI Contrib version: 0.2.10
  • FastAPI version: 0.65.1
  • Python version: 3.7
  • Operating System: Ubuntu

Version Compatibility

What are the things needed to be updated in order for contrib to work properly with the new fastapi version?

ModuleNotFoundError: No module named 'fastapi_contrib'

  • FastAPI Contrib version: 0.2.11
  • FastAPI version: 0.78.0
  • Python version: 3.8.13
  • Operating System: mac

Description

I cloned fastapi project which uses fastapi-contrib library.

and then i installed fastapi-contrib with $ pip install fastapi_contrib.

but ModuleNotFoundError: No module named 'fastapi_contrib' was logged repeatly. ๐Ÿ˜…

Thank you for your busy schedule. ๐Ÿ™๐Ÿผ

What I Did

  • Pycharm turned off and on.
  • $ pip install fastapi_contrib[all] (but it says zsh: no matches found: fastapi_contrib[all])

Initial Update

The bot created this issue to inform you that pyup.io has been set up on this repo.
Once you have closed it, the bot will open pull requests for updates as soon as they are available.

Firestore DB Support

Hi,

This library looks fantastic and I'm going to try and use it for the easy mongodb integration with FastAPI.
What I would be interested in (and may look to contribute myself) is a FirestoreClient (GCP's serverless document db) that could work as a drop in replacement for the mongodb client as I'm seeking to support both in a project i'm working on. I can see the intention of your design is to provide a nice abstraction to the DB technology, this use case would put that to the test with a second implementation.

has_required_permisions typo or bypass of naming conflict?

  • FastAPI Contrib version: 0.2.8
  • FastAPI version: 0.61.1
  • Python version: 3.8
  • Operating System: Ubuntu 20.04

has_required_permisions method from BasePermission class should be has_required_permissions, is it a typo or is it a trick to bypass a naming conflict?

Add Changelog

Description

What do you think about adding a changelog so users know what changes to expect from a version update?

Why Serializer do not output "_id" field?

My model need output _id field to frontend of my project.

    def dict(self, *args, **kwargs) -> dict:
        """
        Removes excluded fields based on `Meta` and `kwargs`
        :return: dict of serializer data fields
        """
        exclude = kwargs.get("exclude")
        if not exclude:
            exclude = set()

        **exclude.update({"_id"})**

        if hasattr(self.Meta, "exclude") and self.Meta.exclude:
            exclude.update(self.Meta.exclude)

        if (
            hasattr(self.Meta, "write_only_fields")
            and self.Meta.write_only_fields
        ):
            exclude.update(self.Meta.write_only_fields)

        kwargs.update({"exclude": exclude})
        original = super().dict(*args, **kwargs)
        return original

integration of flatbuffers in request and response

Flatbuffers is the high performance serialization library from Google - https://google.github.io/flatbuffers/flatbuffers_guide_use_python.html

It has compatible libraries for java (android), javascript and python.

Is it possible for you to create functionality that happens automatically for each endpoint - that it accepts data in flatbuffers and gives response in flatbuffers if the request has Accept: application/x-flatbuffers in it.

e.g. curl -D- -H "Accept: application/x-flatbuffers" http://localhost:7002

if the header is not present, then the endpoint works with default json

Support docker secrets in configuration

Description

In order to use this library successfully as part of another system that is deployed via Docker Swarm, we need to support reading configuration variables from secrets file

Bug with List of response_models taken from Serializer

Description

When trying to use List of generated response_model model as response_model in route, you get in response

{
  "code": 400,
  "detail": "Validation error",
  "fields": [
    {
      "name": "response",
      "message": "Value is not a valid list"
    }
  ]
}

What I Did

from typing import List
from fastapi import APIRouter, Depends

from fastapi_contrib.pagination import Pagination
from fastapi_contrib.serializers import openapi
from fastapi_contrib.serializers.common import Serializer


@openapi.patch
class MySerializer(Serializer):
    ...


router = APIRouter()


@router.get("/", response_model=List[MySerializer.response_model])
async def lst(pagination: Pagination = Depends()):
    return await pagination.paginate(MySerializer)

How to save data in Mongo?

  • FastAPI Contrib version: 0.2.7
  • FastAPI version: 0.59.0
  • Python version: 3.7.7
  • Operating System: MacOS

Description

Hello. I was looking for tools with what will be possible to play FastAPI with MongoDB in nice, simple way, because I never before using NoSQL. I found FastAPI_Contrib, my attention takes especially two of features:

  1. ModelSerializers: serialize (pydantic) incoming request, connect data with DB model and save

  2. MongoDB integration: Use models as if it was Django (based on pydantic models)

I was trying all day to understand the documentation how to use FastAPI_Contrib, unfortunately documentation is so hard for entry-level users. What I want to achieve at this time is nothing more than just:

  1. Create Model
  2. Create Serializer
  3. Send the request with data from for example Postman
  4. Save that data in MongoDB

Just a first step of CRUD...

What I Did

I was trying take it in very different ways, but for this issue I will present the most simply way based on documentation...

Project Structure:
project/
- project/
-- sample/
--- __init__.py
--- serializers.py
- __init__.py
- main.py

project/main.py:

import os

from fastapi_contrib.db.utils import setup_mongodb, create_indexes
import motor.motor_asyncio
from fastapi import FastAPI
from dotenv import load_dotenv

from project.serializers import sample_router

load_dotenv(verbose=True)
DATABASE_URL =os.getenv("DB_URL")

SECRET = os.getenv("SECRET")

CONTRIB_APPS = os.getenv("CONTRIB_APPS")
CONTRIB_APPS_FOLDER_NAME = os.getenv("CONTRIB_APPS_FOLDER_NAME")

client = motor.motor_asyncio.AsyncIOMotorClient(
    DATABASE_URL, uuidRepresentation="standard"
)
db = client["sample"]

app = FastAPI()

@app.on_event("startup")
async def startup():
    setup_mongodb(db)
    await create_indexes()
    print('Is it connected to DB?')

app.include_router(sample_router)

project/sample/serializers.py

from fastapi import APIRouter
from fastapi_contrib.db.models import MongoDBModel
from fastapi_contrib.serializers import openapi
from fastapi_contrib.serializers.common import Serializer

# from yourapp.models import SomeModel

sample_router = APIRouter()


class SomeModel(MongoDBModel):
    field1: str

    class Meta:
        collection = "test"


@openapi.patch
class SomeSerializer(Serializer):
    read_only1: str = "const"
    write_only2: int
    not_visible: str = "42"

    class Meta:
        model = SomeModel
        exclude = {"not_visible"}
        write_only_fields = {"write_only2"}
        read_only_fields = {"read_only1"}


@sample_router.post("/test/", response_model=SomeSerializer.response_model)
async def root(serializer: SomeSerializer):
    model_instance = await serializer.save()
    return model_instance.dict()

When I send data as POST to /test/ from postman

{
  "write_only2": 2,
  "field1": "string"
}

or by curl

curl -X POST "http://127.0.0.1:8000/test/" -H  "accept: application/json" -H  "Content-Type: application/json" -d "{\"id\":0,\"field1\":\"string\",\"write_only2\":0}"

Then I got errors:
Screenshot 1

if I will remove:

    class Meta:
        collection = "test"

as it in an example in documentation then I got other error:
Screenshot 2

I will be grateful if someone will explain to me using simple examples how to properly combine models, serializers, and perform CRUD operations on them reflected in MongoDB.

By the way.
I think is it good idea to rewrite documentation to be more affordable to not so advenced users. And add to them a tutorial, awesome will be to see there real world examples. I think, good documentation can make this package a very popular.

Regards,
Oskar

Passing extra arguments to MongoDBModel.list()

  • FastAPI Contrib version: 0.1.14
  • FastAPI version: 0.42
  • Python version: 3.7.4
  • Operating System: Debian 10

Description

Hi,
I am trying to get a list of entries from a collection sorted in descending order according to a recorded date (not a creation timestamp, I am not using the timestamped model). Pymongo sort should work passing a sort=[(key, value)] argument, but when I do that, I get back an empty list. Am I doing anything wrong?

What I Did

This is my function. The field transmit_time is a MongoDB date field.

@router.get("/")
async def get_packets(limit: int = 100, offset: int = 0):
    sort_desc = [('transmit_time', -1)]
    packets = await Packet.list(_limit=limit, _offset=offset, sort=sort_desc)
    return packets

Why is the version of FastAPI fixed in the install_requires?

Currently fastapi/pydantic versions are fixed in setup.py:

requirements = [
    'fastapi==0.52.0',
    'pydantic==1.4',
    'contextvars;python_version<"3.7"'
]

Is there a reason not to use >= instead?

If compatibility is a concern, it's possible to test against different versions of FastAPI on TravisCI.

Add Support for FastAPI latest version

  • FastAPI Contrib version:
  • FastAPI version: 0.45.0
  • Python version: 3.7
  • Operating System: Linux

Description

pipenv.exceptions.ResolutionFailure: ERROR: ERROR: Could not find a version that matches fastapi==0.42.0,==0.45.0

What I Did

Trying to use lib with FastAPI 0.45.0 and up

How to mock or override PermissionsDependency

  • FastAPI Contrib version: Latest
  • FastAPI version: Latest
  • Python version: 3.9.7
  • Operating System: Mac Os

Description

Hello guys I need help with testing PermissionsDependency.

What I Did

My current code is :

api_router.include_router(
    article_deviation.router,
    prefix="{Confidential information}",
    tags=["{confidential information}"],
    dependencies=[
        Depends(azure_scheme),
        Depends(PermissionsDependency([QCPermission])),
    ],
)

In the test I override azure_scheme as :


api_client = TestClient(app)
app.dependency_overrides[azure_scheme] = {"Authorized": "Yes"}

but with


api_client = TestClient(app)
app.dependency_overrides[PermissionsDependency([QCPermission])] = True

It does not work.

Thank you in advance! Wish you all best!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.