I'm new to fastapi, which is really great so far, but I struggle to find a clean way to import my app config to in another module.
EDIT: I need to be able to change the config when running unit test
Here is my dir tree:
/app
| __init__.py
| /router
| | __init__.py
| | my_router.py
| /test
| | test_api.py
| config.py
| main.py
Here is my main.py file:
from functools import lru_cache
from fastapi import FastAPI
from .router import my_router
from . import config
app = FastAPI()
app.include_router(
my_router.router,
prefix="/r",
tags=["my-router"],
)
#lru_cache()
def get_setting():
return config.Settings(admin_email="admin#domain.com")
#app.get('/')
def hello():
return 'Hello world'
Here is the router.py:
from fastapi import APIRouter
from ..main import get_setting
router = APIRouter()
#router.get('/test')
def get_param_list(user_id: int):
config = get_setting()
return 'Import Ok'
And here is the config file
from pydantic import BaseSettings
class Settings(BaseSettings):
param_folder: str = "param"
result_folder: str = "output"
class Config:
env_prefix = "APP_"
Then runing uvicorn app.main:app --reload I got : ERROR: Error loading ASGI app. Could not import module "app.main".
I guess because of a kind of circular import. But then I don't how to pass my config to my router ?
Thanks for your help :)
How about setting up the lru cache directly inside the config.py.
from functools import lru_cache
from pydantic import BaseSettings
class Settings(BaseSettings):
admin_email: str = "admin#example.com"
param_folder: str = "param"
result_folder: str = "output"
class Config:
env_prefix = "APP_"
#lru_cache()
def get_setting():
return Settings()
And my_router.py
from fastapi import APIRouter, Depends
from ..config import Settings, get_setting
router = APIRouter()
#router.get('/test')
def get_param_list(config: Settings = Depends(get_setting)):
return config
And test.py
from fastapi.testclient import TestClient
from . import config, main
client = TestClient(main.app)
def get_settings_override():
return config.Settings(admin_email="testing_admin#example.com")
main.app.dependency_overrides[config.get_settings] = get_settings_override
def test_app():
response = client.get("/r/test")
data = response.json()
assert data == config.Settings(admin_email="testing_admin#example.com")
The only draw back with this is that I must add the setting: config.Setting = Depends(config.get_setting), which is quite "heavy", to every function call that needs the setting.
You can use Class Based Views from the fastapi_utils package:
from fastapi import APIRouter, Depends
from fastapi_utils.cbv import cbv
from starlette import requests
from logging import Logger
from .. import config
router = APIRouter()
#cbv(router)
class MyQueryCBV:
settings: config.Setting = Depends(config.get_setting) # you can introduce settings dependency here
def __init__(self, r: requests.Request): # called for each query
self.logger: Logger = self.settings.logger
self.logger.warning(str(r.headers))
#router.get('/test')
def get_param_list(self, user_id: int)
self.logger.warning(f"get_param_list: {user_id}")
return self.settings
#router.get("/test2")
def get_param_list2(self):
self.logger.warning(f"get_param_list2")
return self.settings
I got it working using the FastAPI Dependency system and, as suggested by #Kassym Dorsel, by moving the lru_cache to the config.py.
The only draw back with this is that I must add the setting: config.Setting = Depends(config.get_setting), which is quite "heavy", to every function call that needs the setting.
Here is how I did it:
config.py file:
from pydantic import BaseSettings
class Settings(BaseSettings):
param_folder: str = "param"
result_folder: str = "output"
class Config:
env_prefix = "APP_"
#lru_cache()
def get_setting():
return config.Settings(admin_email="admin#domain.com")
main.py file:
from functools import lru_cache
from fastapi import FastAPI
from .router import my_router
app = FastAPI()
app.include_router(
my_router.router,
prefix="/r",
tags=["my-router"],
)
#app.get('/')
def hello():
return 'Hello world'
router.py file:
from fastapi import APIRouter, Depends
from .. import config
router = APIRouter()
#router.get('/test')
def get_param_list(user_id: int, setting: config.Setting = Depends(config.get_setting)):
return setting
This way I can use the dependency_overrides in my test_api.py to change the config for the test:
from fastapi.testclient import TestClient
from .. import config, server
client = TestClient(server.app)
TEST_PARAM_FOLDER = 'server/test/param'
TEST_RESULT_FOLDER = 'server/test/result'
def get_setting_override():
return config.Setting(param_folder=TEST_PARAM_FOLDER, result_folder=TEST_RESULT_FOLDER)
server.app.dependency_overrides[config.get_setting] = get_setting_override
def test_1():
...
Related
Writting UniTTest For Flask Application
I have a simple Flask Application below , flask application will work perfectly fine without any issues
import sys , os , os.path , time , datetime , json , logging, warnings
from os import listdir
from os.path import isfile, join
from flask_wtf.csrf import CSRFProtect
from flask import Flask
from flask import render_template
from flask import request
from datetime import datetime
from logging import FileHandler
from logging import Formatter
# --> Required Host and Env
_cloudhost='dev.net'
_cloudenv='dev'
_portNumber=4444
#CRF Compliant / (.)
app = Flask(__name__)
csrf = CSRFProtect()
csrf.init_app(app)
#app.route('/')
#app.route('/index')
def index():
project = "CFTP - TBRP & TBLP "
framework = "Publish Framework"
version = '0.1'
hostname = os.popen("echo $(hostname)").read().split('\n')[0]
return render_template('index.html', title=project , description=f'{project} : {framework} v - {version}' , hostname=hostname , logFile=readlogs(), get_env=_cloudenv)
app.run(host=_cloudhost, port=_portNumber)
UnitTest Function . I am struggling to understand on how i can create unit test cases for flask application. Inside my unittest file which i created , i was wondering if i am doing this right .
1 -> To test the csrf if that i included above and also on how i can test my index page can result as pass
test_flask.py
import unittest , sys , tempfile, os , json , shutil
from unittest import mock
from subprocess import Popen, PIPE
import logging
with mock.patch.dict(os.environ, {'PROJECT_FOLDER':'CitiFTP Reports','RPM_ENVIRONMENT': 'DEV', 'HOST_NAME': 'sd-nzvp-czog.nam.nsroot.net', 'USER': 'citiftptabhyper','TABLEAU_PUBLISH_TYPE':'TABCMD','TABCMD_PATH':'/home/citiftptabhyper/tabcmd','CYBERARK_TYPE':'DYNAMIC','JOB_TYPE':'workbook_TP','JOB_FILE':'publish_workbook_TP','RESTAPI_LOG_TYPE':'TP'}):
sys.path.insert(2, 'C:/Users/mm13854/Desktop/FileCopy_Dec_122/cftp_tableau_filecopy/deploy/src')
sys.path.insert(1, 'C:/Users/mm13854/Desktop/FileCopy_Dec_122/cftp_tableau_filecopy/deploy/app')
import env_conf as conf
import run_flask as flk_app
from run_flask import app
class test_tbrp_case(unittest.TestCase):
def setUp(self):
self.ctx = app.app_context()
self.ctx.push()
self.client = app.test_client()
def tearDown(self):
self.ctx.pop()
def test_home(self): ## Fail
response = self.client.post("/index", data={"title": "CFTP - TBRP & TBLP "})
assert response.status_code == 200
## test flask port used -- PASS
def test_port(self):
self.assertEqual(flk_app._portNumber,4444)
## Test host is returning the same value -- PASS
def test_host(self):
self.assertEqual(flk_app._cloudhost,conf.FLASK_CONFIGURATION.get('ENV_URL'))
def test_csrf(self):
pass
if __name__=='__main__':
unittest.main()
I'm reading env variables from .prod.env file in my config.py:
from pydantic import BaseSettings
class Settings(BaseSettings):
A: int
class Config:
env_file = ".prod.env"
env_file_encoding = "utf-8"
settings = Settings()
in my main.py I'm creating the app like so:
from fastapi import FastAPI
from app.config import settings
app = FastAPI()
print(settings.A)
I am able to override settings variables like this in my conftest.py:
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.config import settings
settings.A = 42
#pytest.fixture(scope="module")
def test_clinet():
with TestClient(app) as client:
yield client
This works fine, whenever I use settings.A I get 42.
But is it possible to override the whole env_file from .prod.env to another env file .test.env?
Also I probably want to call settings.A = 42 in conftest.py before I import app, right?
You can override the env file you use by creating a Settings instance with the _env_file keyword argument.
From documentation:
Passing a file path via the _env_file keyword argument on instantiation (method 2) will override the value (if any) set on the Config class. If the above snippets were used in conjunction, prod.env would be loaded while .env would be ignored.
For example, this should work for your test -
import pytest
from fastapi.testclient import TestClient
import app.config as conf
from app.config import Settings
# replace the settings object that you created in the module
conf.settings = Settings(_env_file='.test.env')
from app.main import app
# just to show you that you changed the module-level
# settings
from app.config import settings
#pytest.fixture(scope="module")
def test_client():
with TestClient(app) as client:
yield client
def test_settings():
print(conf.settings)
print(settings)
And you could create a .test.env, set A=10000000, and run with
pytest -rP conftest.py
# stuff
----- Captured stdout call -----
A=10000000
A=10000000
This looks a little messy (though this is probably only used for test purposes), so I'd recommend not creating a settings object that is importable by everything in your code, and instead making it something you create in, say, your __main__ that actually creates and runs the app, but that's a design choice for you to make.
I ran into the same issue today, really annoying. My goal was to set a different postgresql database for the unit tests. The default configuration comes from a .env file. But when you think of it thoroughly it is not that difficult to understand. It all boils down to the order of the imported modules in conftest.py. I based the example below on the answer of #wkl:
import pytest
from typing import Generator
from fastapi.testclient import TestClient
import app.core.config as config
from app.core.config import Settings
# Replace individual attribute in the settings object
config.settings = Settings(
POSTGRES_DB="test_db")
# Or replace the env file in the settings object
config.settings = Settings(_env_file='.test.env')
# All other modules that import settings are imported here
# This ensures that those modules will use the updated settings object
# Don't forget to use "noqa", otherwise a formatter might put it back on top
from app.main import app # noqa
from app.db.session import SessionLocal # noqa
#pytest.fixture(scope="session")
def db() -> Generator:
try:
db = SessionLocal()
yield db
finally:
db.close()
#pytest.fixture(scope="module")
def client() -> Generator:
with TestClient(app) as c:
yield c
One workaround I have found is to remove the env_file from Config completely and replace it's functionality with load_dotenv() from dotenv like this:
config.py:
from pydantic import BaseSettings
from dotenv import load_dotenv
load_dotenv(".prod.env")
class Settings(BaseSettings):
A: int
settings = Settings()
conftest.py:
import pytest
from fastapi.testclient import TestClient
from dotenv import load_dotenv
load_dotenv("test.env")
from app.config import settings
from app.main import app
#pytest.fixture(scope="module")
def test_clinet():
with TestClient(app) as client:
yield client
Please note, that calling load_dotenv("test.env") happens before importing the settings (from app.config import settings)
and also note that load_dotenv() will load environment variables globally for the whole python script.
Loading env variables like this will not override already exported variables, same as using the env_file in pydantic's BaseSettings
I've been following those tutorials on (unit) testing for dependency injections and they're using pytest fixtures and I'm trying to replicate something similar in my Flask app. This is what my app looks like:
# all imports
class Container(containers.DeclarativeContainer):
wiring_config = containers.WiringConfiguration(modules=[".routes", ".scheduler"])
config = providers.Configuration(yaml_files=["src/conf/config.yaml"])
config.load(envs_required=True)
s3_repository = providers.Resource(
S3Repository, config.get("app.my_service.s3_bucket")
)
my_service = providers.Singleton(
MyService, config, s3_repository
)
My app.py:
container = Container()
container.init_resources()
app = Flask(__name__)
app.container = container
# connect url rules and register error handlers
routes.configure(app)
# schedule and kickoff background jobs
scheduler.schedule(app)
# set flask configuration and logging
app.config.from_mapping(app.container.config.get("app"))
setup_logging(app)
return app
my_service.py
class MyService:
def __init__(self, config: dict, s3_repository: S3Repository) -> None:
self.s3 = s3_repository
self.config = config
# other logic/methods
My S3Repository:
class S3Repository:
def __init__(self, bucket):
self.bucket = bucket
def fetch(self, object_key, columns, filters):
# code to fetch
I'm trying to write my tests and working with pytest for the first time and this is what I have so far:
# TODO - re-write tests for since we're now using dependency injection
import unittest
from unittest.mock import Mock
import pytest as pytest
from src.repository.s3_repository import S3Repository
from src.service.HealthSignalService import HealthSignalService
class TestApp(unittest.TestCase):
def something(self):
pass
#pytest.fixture
def mock_config(mocker):
return mocker.patch("providers.Configuration")
def test_app(mock_config):
from src import create_app
create_app()
When I run this I see:
#pytest.fixture
def mock_config(mocker):
E fixture 'mocker' not found
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, mock_config, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory
> use 'pytest --fixtures [testpath]' for help on them.
What am I doing wrong? What am I missing? Right now, I'm following this tutorial - https://docs.pytest.org/en/7.1.x/explanation/fixtures.html
You never define a pytest.fixture with the name of mocker. The arguments passing to the pytest function arguments must be defined by pytest.fixture
for example
#pytest.fixture
def connection():
...
#pytest.fixture
def database(connection):
...
def test_somecase(connection, database):
...
those arguments already defined by the pytest.fixture
main.py
import os
import psycopg2
import gunicorn
from flask import Flask
from app.routes.universities_routes import app_universities_routes
from app.routes.degrees_routes import app_degrees_routes
from app.routes.curriculum_ratings_routes import app_curriculum_ratings_routes
from app.routes.departments_route import app_departments_routes
from app.routes.user_route import app_users_routes
from app.routes.history_route import app_history_routes
from app.routes.curriculums_route import app_curriculum_routes
from app.routes.courses_route import app_course_routes
from flask_cors import CORS, cross_origin
app = Flask(__name__)
CORS(app)
app.register_blueprint(app_universities_routes)
app.register_blueprint(app_degrees_routes)
app.register_blueprint(app_curriculum_ratings_routes)
app.register_blueprint(app_departments_routes)
app.register_blueprint(app_users_routes)
app.register_blueprint(app_history_routes)
app.register_blueprint(app_curriculum_routes)
app.register_blueprint(app_course_routes)
#app.route('/')
def hello():
return 'Hello, Class Track!'
...
test_routes.py
import pytest
from flask import current_app as app
#pytest.fixture
def app():
app.testing = True
yield app
#pytest.fixture
def client(app):
return app.test_client()
#pytest.fixture
def runner(app):
return app.test_cli_runner()
def test_hello(client):
response = client.get('/')
assert response.status_code == 200
File Structure
backend
app
routes
models
tests
test_routes.py
main.py
I'm trying to run the most simple test for routes, but for some reason this doesn't work and throws this error:
app = <function app at 0x000001FD18A0EF80>
#pytest.fixture
def client(app):
> return app.test_client()
E AttributeError: 'function' object has no attribute 'test_client'
backend\app\tests\test_routes.py:24: AttributeError
I run this through VSCode and through the terminal with the 'pytest' command
Is there a way to make this work without changing the code in main.py
I have a question regarding the flask_track_usage module.
All my blueprints should have the Trackusage function included.
Unfortunately i didn't find a way to solve my problem.
Why isn't it possible to simply use the flask.current_app?
route.py
import datetime
from uuid import uuid4
from flask import Blueprint, session, render_template, url_for, flash, redirect, current_app, request, jsonify
from flask_template.main.utils import my_func
import os
import json
from flask_track_usage import TrackUsage
from flask_track_usage.storage.printer import PrintWriter
from flask_track_usage.storage.output import OutputWriter
main = Blueprint('main', __name__)
t = TrackUsage(current_app, [
PrintWriter(),
OutputWriter(transform=lambda s: "OUTPUT: " + str(s))
])
#t.include
#main.before_request
def session_management():
now = datetime.datetime.now()
session_lifetime = current_app.config['SESSION_DURATION']
# Create session, if not already existing
if session.get('session_ID') is None:
# Initiate session, set a random UUID as Session ID
session['session_ID'] = str(uuid4())
session.permanent = True # will expire after 30 minutes of inactivity
session['timeout'] = False
print(f'Initated session with ID:', session.get('session_ID'))
return redirect(url_for('main.index'))
else:
try:
last_active = session.get('last_active')
delta = now - last_active
if delta.seconds > 1740:
print(f'Note: Session lifetime less than one minute. Expires in:',
session_lifetime - delta.seconds, 'sec')
if delta.seconds > session_lifetime:
session['last_active'] = now
session['timeout'] = True
print(f'Your session has expired after 30 minutes, you have been logged out (files are deleted).')
return redirect(url_for('main.logout'))
except:
pass
try:
session['last_active'] = now
except:
pass
#main.route('/')
def index():
return redirect(url_for('main.home'))
#main.route('/home')
def home():
return render_template('home.html', title='home', subheader='Template Main Page')
__init__.py
from flask import Flask
from flask_template.config import Config
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(Config)
from flask_template.sample_extension.routes import sample_extension
from flask_template.main.routes import main
from flask_template.errors.handlers import errors
app.register_blueprint(sample_extension)
app.register_blueprint(main)
app.register_blueprint(errors)
return app
Config
import os
import json
from datetime import timedelta
with open(os.path.join(os.getcwd(), 'etc', 'app_config.json')) as config_file:
config = json.load(config_file)
class Config:
SECRET_KEY = config.get('SECRET_KEY')
SESSION_DURATION = 1800 # 30 minutes for delete function
PERMANENT_SESSION_LIFETIME = timedelta(minutes=30) # session lifetime
root_dir = os.path.join(os.path.realpath('.'), 'flask_template')
# Path to template to illustrate download functionality
TEMPLATE_FOLDER = os.path.join(root_dir, 'sample_extension', 'assets', 'template')
TRACK_USAGE_USE_FREEGEOIP = False
TRACK_USAGE_INCLUDE_OR_EXCLUDE_VIEWS = 'include'
run.py
from flask_template import create_app
from flask_track_usage import TrackUsage
from flask_track_usage.storage.printer import PrintWriter
from flask_track_usage.storage.output import OutputWriter
app = create_app()
if __name__ == '__main__':
app.run(ssl_context=('******'),
host='0.0.0.0',
port=5000,
debug=True)
error
RuntimeError: Working outside of application context.
There are several global Flask variables such as current_app and g that can be accessed only while the application is running (more about them in the Flask Application Context documentation entry). Using them outside the application context raises RuntimeError.
You can instantiate TrackUsage without parameters in your routes.py module:
track_usage = TrackUsage()
#track_usage.include
#main.before_request
def session_management():
...
And then you can import it in your __init__.py module and apply to your application instance:
from flask import Flask
from flask_template.config import Config
from flask_track_usage.storage.printer import PrintWriter
from flask_track_usage.storage.output import OutputWriter
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(Config)
from flask_template.sample_extension.routes import sample_extension, track_usage
from flask_template.main.routes import main
from flask_template.errors.handlers import errors
track_usage.init_app(app, [
PrintWriter(),
OutputWriter(transform=lambda s: "OUTPUT: " + str(s)),
])
app.register_blueprint(sample_extension)
app.register_blueprint(main)
app.register_blueprint(errors)
return app
I have not found this solution in the Flask-Track-Usage documentation but it is a common interface for Flask extentions. It allows to instantiate an extension in one module and connect it to Flask application in main module.