I'm a Django developer and recently stumbled onto the FastAPI framework.
Then I decided to give it a shot. But usually when you talk about building RESTful APIs with Django you usually use the Django Rest Framework (DRF).
Is anybody aware if it is possible to substitute DRF by FastAPI using Django perks, like its ORM, and still have access to all of FastAPI's async features?
Up until now I only found one article on this. But in the process of integration the author lost most of the features of FastAPI.
You can find it here.
In the FastAPI docs, they do mention that it is possible to redirect certain request to a WSGI application here.
Short Answer
Yes it's possible with WSGIMiddleware.
For example, you can use all Django features (yes admin too) with mounting, with this example code.
import os
from importlib.util import find_spec
from configurations.wsgi import get_wsgi_application
from fastapi import FastAPI
from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.staticfiles import StaticFiles
from api import router
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myapp.settings")
os.environ.setdefault("DJANGO_CONFIGURATIN", "Localdev")
application = get_wsgi_application()
app = FastAPI()
app.mount("/admin", WSGIMiddleware(application))
app.mount("/static",
StaticFiles(
directory=os.path.normpath(
os.path.join(find_spec("django.contrib.admin").origin, "..", "static")
)
),
name="static",
)
Also this one is from WSGIMiddleware documentation, it's a more straight-forward example (This one is for Flask but it demonstrates the same idea.).
from fastapi import FastAPI
from fastapi.middleware.wsgi import WSGIMiddleware
from flask import Flask, escape, request
flask_app = Flask(__name__)
#flask_app.route("/")
def flask_main():
name = request.args.get("name", "World")
return f"Hello, {escape(name)} from Flask!"
app = FastAPI()
#app.get("/v2")
def read_main():
return {"message": "Hello World"}
app.mount("/v1", WSGIMiddleware(flask_app))
Update
While it is possible in the approach listed below, I genuinely think that we should avoid coupling different frameworks in such a monolith. Doing so could lead to unexpected bugs and make it harder to scale.
Instead, we could build 1 backend service in FastAPI, 1 Django Admin service for example, and then use NGINX to route traffic to these backend services. Using NGINX to route traffic to different backend services in production is common anyway.
Integration Of FastAPI With Django (WSGI)
https://github.com/jordaneremieff/django-fastapi-example.git
After hours of searching finally I found a great implementation in the link above. It worked seamlessly for me!
Testing
In order to make this simple to test, below are some few adjustments I made to the above reference:
api/models.py
class Item(models.Model):
title = models.CharField(max_length=50)
description = models.TextField()
# owner = models.ForeignKey(
# settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="items"
# )
api/schemas.py
class Item(ItemBase):
# id: int
# owner_id: int
class Config:
orm_mode = True
POST
curl -d "{\"title\":\"le titre\", \"description\":\"la description\"}" -H "Content-Type: application/json" -X POST http://127.0.0.1:8000/api/items
GET
curl http://127.0.0.1:8000/api/items
Thank you for the awesome answers. Here is a little tweaked answer where I have fixed some imports as well as I have used a model from a Django app.
from fastapi import FastAPI
from fastapi.middleware.wsgi import WSGIMiddleware
from django.core.wsgi import get_wsgi_application
import os
from importlib.util import find_spec
from fastapi.staticfiles import StaticFiles
from django.conf import settings
# Export Django settings env variable
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
# Get Django WSGI app
django_app = get_wsgi_application()
# Import a model
# And always import your models after you export settings
# and you get Django WSGI app
from accounts.models import Account
# Create FasatAPI instance
app = FastAPI()
# Serve Django static files
app.mount('/static',
StaticFiles(
directory=os.path.normpath(
os.path.join(find_spec('django.contrib.admin').origin, '..', 'static')
)
),
name='static',
)
# Define a FastAPI route
#app.get('/fastapi-test')
def read_main():
return {
'total_accounts': Account.objects.count(),
'is_debug': settings.DEBUG
}
# Mount Django app
app.mount('/django-test', WSGIMiddleware(django_app))
Hint: I created a file named app.py in my Django project's root directory and it worked. Here is my directory structure:
.
├── accounts
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── app.py
├── db.sqlite3
├── project
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
And run your FastAPI app:
(myvenv) ➜ project uvicorn --host 0.0.0.0 --port 8000 app:app --reload
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: Started reloader process [48366] using statreload
INFO: Started server process [48368]
INFO: Waiting for application startup.
INFO: Application startup complete.
Hopefully, this will work for you. Now accessing /django-test will serve your Django project and /fastapi-test will serve the FastAPI part.
This configuration serves Django static files too and we can use our Django models in our FastAPI code as well. I'll test it further and I'll update this answer if I find any possibilities for improvement.
As Sumitrhan pointed out in a commet: There is also Django Ninja a Project that uses very similar concepts like Fast API (Routes, Pydandantic Model Validation) but simply is a Django App.
Given that current Django supports also async views in I see no point in mixing django and FastApi, to get the same feature set only much more complicated as not as good integrated.
there is good resource as follows:
Combine the power of FastAPI and Django
Related
1.I was trying to use Pymongo to connect to my MongoDB atlas database. I wrote this file into the connect.py file. Here is the tree folder structure. The connect.py contains the URL information to connect the database.
└── uploading
├── __pycache__
│ └── upload.cpython-38.pyc
└── upload.py
├── Database
│ ├── __pycache__
│ │ └── connect.cpython-38.pyc
│ └── connect.py
├── app.py
2.Here is code for connect.py
import pymongo
client = pymongo.MongoClient("URL",ssl=True, ssl_cert_reqs='CERT_NONE')
db = client.datasets
Here is the code for uplod.py
from flask import Blueprint, current_app
from Database.connect import db
sample = Blueprint('sample', __name__)
#sample.route('/')
def index():
x = current_app.config['UPLOAD_PATH']
return str(db)
Here is the code for app.py:
app = Flask(__name__)
app.secret_key = b'pj&\xe9\xd7\xd7\xabc\xe6KX\xbe\x9f<\x9f\x87'
app.config['UPLOAD_PATH'] = 'public' # to create a folder which is used to save the uploaded file
CORS(app)
app.register_blueprint(sample)
'''
Datasets and model upload
'''
#app.route('/connect-upload', methods=["POST"])
#cross_origin()
def connect_upload():
index=0
# get username
return "result"
When I tried to use : from Database.connect import db to import database in the upload.py, it showed error message :ImportError: cannot import name 'db' from 'Database.connect'. How can I solve this problem?
You are not referencing your module correctly. Your module should be the filename. You should use from connect import db.
Intro
I've read like a thousand posts on SO and other sites trying to figure out what's wrong with my Flask structure and why I can't seem to figure out something. As a last resort, I decided to finally ask the question here.
My project is really simple:
I have to fetch some data from some networking devices via API, process the data and store it in a Postgresql DB (most part of the code is in lib/).
This project is going to be deployed on multiple environments (test, dev, staging and prod).
To do the above I'm using the following:
Flask-SQLAlchemy + Flask-Migrate + Flask-Script - all of these for handling migrations and DB related operations;
Python-dotenv - for handling sensitive configuration data
Project details
My project structure looks like this:
my_project/
├── api/
├── app.py
├── config.py
├── __init__.py
├── lib/
│ ├── exceptions.py
│ └── f5_bigip.py
├── log.py
├── logs/
├── manage.py
├── migrations/
├── models/
│ ├── __init__.py
│ ├── model1.py
│ └── model2.py
└── run.py
My app.py looks like this:
import os
import sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from dotenv import load_dotenv
from flask import Flask
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
migrate = Migrate()
def create_app():
load_dotenv()
app = Flask(__name__)
environment = app.config['ENV']
if environment == 'production':
app.config.from_object('config.ProductionConfig')
elif environment == 'testing':
app.config.from_object('config.TestingConfig')
else:
app.config.from_object('config.DevelopmentConfig')
db.init_app(app)
migrate.init_app(app, db)
return app
My config.py looks like this:
import os
from sqlalchemy.engine.url import URL
PROJECT_ROOT = os.path.dirname(os.path.realpath(__file__))
class BaseConfig:
DEBUG = False
TESTING = False
DB_DRIVERNAME = os.getenv('DB_DRIVERNAME')
DB_HOST = os.getenv('DB_HOST')
DB_PORT = os.getenv('DB_PORT')
DB_NAME = os.getenv('DB_NAME')
DB_USERNAME = os.getenv('DB_USERNAME')
DB_PASSWORD = os.getenv('DB_PASSWORD')
DB = {
'drivername': DB_DRIVERNAME,
'host': DB_HOST,
'port': DB_PORT,
'database': DB_NAME,
'username': DB_USERNAME,
'password': DB_PASSWORD,
}
SQLALCHEMY_DATABASE_URI = URL(**DB)
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(BaseConfig):
DEVELOPMENT = True
DEBUG = True
class TestingConfig(BaseConfig):
TESTING = True
class StagingConfig(BaseConfig):
DEVELOPMENT = True
DEBUG = True
class ProductionConfig(BaseConfig):
pass
My __init__.py looks like this:
from contextlib import contextmanager
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
# here, create_engine needs the SQLALCHEMY_DATABASE_URI
# how do I get it from the proper config?
engine = create_engine()
Session = sessionmaker(bind=engine)
#contextmanager
def session_scope():
"""
Provide a transactional scope around a series of operations.
"""
session = Session()
try:
yield session
session.commit()
except Exception as e:
print(f'Something went wrong here: {str(e)}. rolling back.')
session.rollback()
raise
finally:
session.close()
My manage.py looks like this:
from flask_script import Manager
from flask_migrate import MigrateCommand
from app import create_app
from models import *
manager = Manager(create_app)
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
My models/model1.py looks like this:
from sqlalchemy.dialects.postgresql import INET
from sqlalchemy.sql import func
from app import db
class Model1(db.Model):
__tablename__ = 'model1'
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
ip_address = db.Column(INET, unique=True, nullable=False)
last_update = db.Column(db.DateTime(), server_default=func.now())
def __repr__(self):
return f'<Model1: {self.ip_address}>'
def __init__(self, ip_address):
self.ip_address = ip_address
Questions
Now, I have three main questions:
In my main __init__.py how can I import SQLALCHEMY_DATABASE_URI from the app's config?
Having the Session() object in the __init__.py doesn't seem too intuitive. Should it be placed in other places? For more context, the session is used in lib/f5_bigip.py and is probably going to be used in the api/ as well.
Is the overall project structure ok?
Your questions 1 and 2 are directly related to the part of your project that I found strange, so instead of answering those questions I'll just give you a much simpler and better way.
It seems in __init__.py you are implementing your own database sessions, just so that you can create a scoped session context manager. Maybe you've got that code from another project? It is not nicely integrated with the rest of your project, which uses the Flask-SQLAlchemy extension, you are just ignoring Flask-SQLAlchemy, which manages your database connections and sessions, and basically creating another connection into the database and new sessions.
What you should do instead is leverage the connections and sessions that Flask-SQLAlchemy provides. I would rewrite __init__.py as follows (doing this by memory, so excuse minor mistakes):
from contextlib import contextmanager
from app import db
#contextmanager
def session_scope():
"""
Provide a transactional scope around a series of operations.
"""
try:
yield db.session
session.commit()
except Exception as e:
print(f'Something went wrong here: {str(e)}. rolling back.')
db.session.rollback()
raise
finally:
db.session.close()
With this you are reusing the connection/sessions from Flask-SQLAlchemy. Your question 1 then is not a problem anymore. For question 2, you would use db.session anywhere in your app where you need database sessions.
Regarding question 3 I think you're mostly okay. I would suggest you do not use Flask-Script, which is a fairly old and unmaintained extension. Instead you can move your CLI to Flask's own CLI support.
I have the static folder in my app folder and that's where I have js, css, images etc. folders.
I reference them like this in my templates:
<script src="/static/js/main.js"></script>
and it works locally. But, when the app is published to production, I get a 404 for all static files. I'm new to flask so I guess I'm serving them wrong.
I don't have the static folder set as a designated static files folder anywhere in the app - is it done automatically if it's called 'static' or do I need to set it in code?
My project structure:
app/
├── static/
│ └── js/
| |__ css/
├── templates/
│ └── index.html
└── __init__.py
application.py
application.py:
from app import create_app
application = create_app()
init.py:
from flask import Flask
from config import BaseConfig
def create_app():
server = Flask(__name__)
server.config.from_object(BaseConfig) # this is just for the secret key
return server
Flask automatically adds a static view that takes a path relative to the $YOUR_APP/static directory and serves it.
Typically you would include your js/css files in your templates like so:
{{ url_for('static', filename='main.js') }}
First part of the solution was Bert's answer above which described how to correctly reference assets from the templates.
The other part I was missing was this:
server = Flask(__name__, static_url_path="", static_folder="static")
instead of what I had before:
server = Flask(__name__)
Adding static_url_path and static_folder fixed the issue for me.
I'm writing a django rest framework API (backend) with a react SPA frontend. For the production environment, the front end is served up via nginx at http://example.com/ and the backend is proxied to gunicorn and served from the same domain with a different path - http://example.com/api/
This is all working fine in production, there are no CORS issues as the frontend and backend are both served up under the same domain via nginx.
For local development, I want to be able to replicate a similar setup using ./manage.py runserver and have django serve up the frontend from project/frontend/dist (built by npm build seperately).
Has anyone got any urlpattern wizardry that will allow this to happen and also let the react-router play nicely? I just can't figure it out and it's starting to drive me nuts...
The project structure is something like this if it helps in any explanations.
Project
|
── backend
│ ├── apps
│ ├── config
| | ├── settings.py
| | ├── settings_local.py
| | ├── urls.py
│ ├── manage.py
│ ├── requirements.txt
│ └── venv
├── frontend
│ ├── dist (contains the npm build webpack)
│ ├── node_modules
│ ├── package.json
│ ├── package-lock.json
│ ├── scripts
│ ├── src
│ ├── webpack.config.js
Edit #1
Thanks to another post I was able to get partially the way there by adding this to my urls.py
if settings.DEBUG:
from django.views.generic import RedirectView
from django.contrib.staticfiles.views import serve
bundle_path = os.path.abspath(os.path.join(root_dir, 'frontend', 'dist'))
settings.STATICFILES_DIRS += (bundle_path,)
urlpatterns += [url(r'^$', serve, kwargs={'path': 'index.html'})]
urlpatterns += [url(r'^(?!/?static/)(?!/?media/)(?P<path>.*\..*)$',
RedirectView.as_view(url='/static/%(path)s', permanent=False))]
The only issue I have here is that if I go directly to one of the JS router links, then django tries to interpret it and can't find the route.
What I need now is a catch-all that will redirect to '/' but also keep any extra URL path information on the url
Finally got this to work with all the routing.
Added the webpack dist folder to settings.STATICFILES_DIRS
Added the webpack frontend path to settings.TEMPLATES
Added 3 new urlpatterns to urls.py ( based on a conditional, settings.DEBUG)
urlpatterns += [
url(r'^(?!/?static/)(?!/?media/)(?P<path>.*\..*)$',
RedirectView.as_view(url='/static/%(path)s', permanent=False)),
url(r'^$', TemplateView.as_view(template_name='dist/index.html'), name='frontend'),
url(r'^(?P<path>.*)/$', TemplateView.as_view(template_name='dist/index.html'), name='frontend'),
]
The first redirects any of the asssets for the bundle to be served using django's static files.
The second pattern serves the root path / as a template from the webpack dist folder
The final piece handles all the leftover JS routing paths to go back to the initial root view.
You can use nginx reverse proxy to serve them.
No need config Django's url.
See example here:
http://www.codingpedia.org/ama/how-to-configure-nginx-in-production-to-serve-angular-app-and-reverse-proxy-nodejs
I am starting with Django development as a hobby project. So far I have happily worked with SQLite, both in development (i.e. with py manage.py runserver) and in deployment (on Nginx + uWSGI). Now I would also like to learn to use a more robust PostgreSQL database. However, if possible, I would like to skip installing it locally, to avoid installing Postgres on Windows.
I was wondering if it was possible, by means of Django, to use SQLite whenever I use the built-in server and Postgres in deployment, without changing the project code. Couldn't find how to do it.
I can use a workaround and make my deployment procedure change the settings on server each time I deploy. But that's kind of a hack.
You could split your settings.py in multiple settings file e.g.
[projectname]/
├── [projectname]/
│ ├── __init__.py
│ ├── settings/
│ │ │── base.py
│ │ │── development.py
│ │ │── production.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
your base.py includes all code from your current settings.py. specify the different databases in your development.py(sqlite3) and in your production.py (postgresql) and import the base.py in each
from .base import *
Last but not least you'll have to tell django which file it should use. Add
export DJANGO_SETTINGS_MODULE="projectname.settings.development"
to your postactivate of your virtualenv on your development server and
export DJANGO_SETTINGS_MODULE="projectname.settings.production"
on your production server. Don't forget to
unset DJANGO_SETTINGS_MODULE
in your predeactivate.
More infos are here: http://www.marinamele.com/taskbuster-django-tutorial/settings-different-environments-version-control
btw. a great tutorial with a lot of best practices
replace settings.py with
settings/
│── __init__.py
│── base.py
│── development.py
│── production.py
in __init__.py
import os
app_stage = os.environ.get('DJANGO_APP_STAGE', 'dev')
if app_stage == 'prod':
from .production import *
else:
from .development import *
And finally when you launch app for production, make sure you set env DJANGO_APP_STAGE='prod'
Create your database settings in respective files and you are good to go.
Very late reply, but I hope this helps someone in the future.
I found this article on Digital Ocean that has the database option split happening in one settings file. https://docs.digitalocean.com/tutorials/app-deploy-django-app/#configuring-database-access
Makes use of a DEVELOPMENT_MODE system environment variable to split settings. They also included a check to prevent a connection when a command is run like collectstatic.
settings.py file:
import os
import sys
import dj_database_url
DEVELOPMENT_MODE = os.getenv("DEVELOPMENT_MODE", "False") == "True"
if DEVELOPMENT_MODE is True:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
elif len(sys.argv) > 0 and sys.argv[1] != 'collectstatic':
if os.getenv("DATABASE_URL", None) is None:
raise Exception("DATABASE_URL environment variable not defined")
DATABASES = {
"default": dj_database_url.parse(os.environ.get("DATABASE_URL")),
}
Note: This is also making use of the Django Database URL package, but you could easily adapt for separated database arguments.