can't POST data with Flask app using SQLAlchemy + Marshmallow - flask

I'd like to POST some data to a database on AWS RDS. I have a Flask application, and I am trying to commit a SQLAlchemy object to the db.
here is a sqlalchemy class with 3 fields, and the accompanying marshmallow schema class :
class Plant(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
dur = db.Column(db.Integer)
def __init__(self, name, dur):
self.name = name
self.dur = dur
class PlantSchema(ma.SQLAlchemySchema):
class Meta:
model = Plant
Here is the a POST request in app.py, in order to add a plant model to my database :
plant_schema = PlantSchema()
#app.route('/plants', methods=['POST'])
def add_plant():
name = request.json.get('name', '')
dur = request.json.get('dur', '')
plant = Plant(name=name, dur=dur)
db.session.add(plant)
db.session.commit()
return plant_schema.jsonify(plant)
unfortunately, when i POST my request in Postman, i get the following
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>Did not attempt to load JSON data because the request Content-Type was not 'application/json'.</p>
this is the JSON that i am giving to postman, for the POST request.
{
"name":"melon",
"dur":"1"
}
not sure where i went wrong, im not sure if the JSON is correctly structured, but i dont know the proper structure it should have, or where in the application the post is breaking down.

You need to specify in your request that you are sending a json.
If you send without Content-Type, your application will return that you need to specify the Content-Type to application/json:
You just need to add the content type header:
Code for the example:
from flask import Flask
from flask import request
app = Flask(__name__)
#app.post("/")
def teste():
a = request.json.get("teste", "")
return a, 200
if __name__ == "__main__":
app.run(debug=True)

Related

Mocking external API in Django

I am trying to mock external api in Django but not sure how to do it properly.
Basically, it must mock the json data from external API and then create a new object if all values are valid.
The program fetches the geolocation data based on given IP address and saves the object in database if response data includes all required fields. So, how I can mock this process to test a new object creation?
services.py
import os
import requests
from .exceptions import ExternalApiException
def get_location(ip):
url = f'http://api.ipstack.com/{ip}'
params = {'access_key': os.environ.get('ACCESS_KEY')}
try:
res = requests.get(url, params=params)
data = res.json()
return {
'ip':data['ip'],
'country_name':data['country_name'],
'region_code':data['region_code'],
'city':data['city'],
'latitude':data['latitude'],
'longitude':data['longitude'],
'zip_code':data['zip']
}
except requests.exceptions.ConnectionError:
raise ExternalApiException('Connection error occured during the fetch process')
except requests.exceptions.Timeout:
raise ExternalApiException("Connection timeout. Please check your internet connection and try again later")
except requests.exceptions.TooManyRedirects:
raise ExternalApiException("Too many redirects")
except requests.exceptions.RequestException:
raise SystemExit(e)
tests.py
#I am lost in this part
#patch('geolocation.services.get_location')
def test_create_basic_geolocation(self, mock_request):
"""Test creating geolocation data"""
payload = {
'ip': '',
}
res = self.client.post(LOCATIONS_URL, payload)
self.assertTrue(res.data['ip'])
Thanks for any help.
Just assign return_value on mocked instance like this
#patch('geolocation.services.get_location')
def test_create_basic_geolocation(self, mock_request):
"""Test creating geolocation data"""
mock_request.return_value = {"ip": "hello", "country_name": "test"}
payload = {
'ip': '',
}
res = self.client.post(LOCATIONS_URL, payload)
self.assertTrue(res.data['ip'])

Zoho CRM Python SDK v2 initialization problem for Django

Im trying to integrate the Zoho CRM v2 SDK with my Django app.
On the Django runserver, im able to get access tokens and using the refresh method and store them in the zcrm_oauthtokens.pkl file. The sdk then automatically refreshes the access token using the refresh token, so no problem here. However on my production server (heroku) im getting this error message:
2019-01-16T11:07:22.314759+00:00 app[web.1]: 2019-01-16 11:07:22,314 - Client_Library_OAUTH - ERROR - Exception occured while fetching oauthtoken from db; Exception Message::'NoneType' object has no attribute 'accessToken'
It seems to me that the tokens are being saved to file, but when the sdk try to access them it is looking for them in a DB and not the file specified in the token_persistence_path.
In my settings.py I have this:
ZOHO_CLIENT_ID = config('ZOHO_CLIENT_ID')
ZOHO_CLIENT_SECRET = config('ZOHO_CLIENT_SECRET')
ZOHO_REDIRECT_URI = config('ZOHO_REDIRECT_URI')
ZOHO_CURRENT_USER_EMAIL = 'jamesalexander#mylastwill.co.uk'
ZOHO_PATH = os.path.join(BASE_DIR, 'wills_online', 'zoho')
zoho_config = {'apiBaseUrl': "https://www.zohoapis.com",
'currentUserEmail': ZOHO_CURRENT_USER_EMAIL,
'client_id': ZOHO_CLIENT_ID,
'client_secret': ZOHO_CLIENT_SECRET,
'redirect_uri': ZOHO_REDIRECT_URI,
'token_persistence_path': ZOHO_PATH}
and in a views file I have this:
from zcrmsdk import *
import logging
from django.shortcuts import HttpResponse
from wills.models import PersonalDetails, ZoHoRecord, WillDocument
from wills_online.decorators import start_new_thread
from wills_online.settings import zoho_config
logger = logging.getLogger(__name__)
class ZohoRunOnce:
def __init__(self):
self.already_run = False
def run_once(self):
if not self.already_run:
print('zoho init run once')
ZCRMRestClient.initialize(zoho_config)
self.already_run = True
zoho_init = ZohoRunOnce()
zoho_init.run_once()
print(zoho_config['token_persistence_path'])
def zoho_callback():
return HttpResponse(200)
#start_new_thread
def zoho_personal_details(request):
""" updates or create a user account on zoho on profile completion """
personal_details_ob = PersonalDetails.objects.get(user=request.user)
zoho_ob = ZoHoRecord.objects.get(user=request.user)
try:
if zoho_ob.account:
record = ZCRMRecord.get_instance('Accounts', zoho_ob.account)
record.set_field_value('Account_Name', request.user.email)
record.set_field_value('Name', personal_details_ob.full_name)
record.set_field_value('Email', request.user.email)
record.set_field_value('Address_Line_1', personal_details_ob.address_line_1)
record.set_field_value('Address_Line_2', personal_details_ob.address_line_2)
record.set_field_value('Post_Town', personal_details_ob.post_town)
record.set_field_value('Post_Code', personal_details_ob.post_code)
record.set_field_value('Dob_Day', personal_details_ob.dob_day)
record.set_field_value('Dob_Month', personal_details_ob.dob_month)
record.set_field_value('Dob_Year', personal_details_ob.dob_year)
record.set_field_value('Gender', personal_details_ob.sex)
record.set_field_value('Marital_Status', personal_details_ob.marital_status)
record.set_field_value('Partner_Name', personal_details_ob.partner_full_name)
record.set_field_value('Partner_Gender', personal_details_ob.partner_gender)
record.set_field_value('Partner_Email', personal_details_ob.partner_email)
record.set_field_value('Children', personal_details_ob.children)
record.set_field_value('Pets', personal_details_ob.pets)
record.update()
else:
user = ZCRMUser.get_instance(name='James Alexander')
record = ZCRMRecord.get_instance('Accounts')
record.set_field_value('Account_Owner', user)
record.set_field_value('Account_Name', request.user.email)
record.set_field_value('Name', personal_details_ob.full_name)
record.set_field_value('Email', request.user.email)
record.set_field_value('Address_Line_1', personal_details_ob.address_line_1)
record.set_field_value('Address_Line_2', personal_details_ob.address_line_2)
record.set_field_value('Post_Town', personal_details_ob.post_town)
record.set_field_value('Post_Code', personal_details_ob.post_code)
record.set_field_value('Dob_Day', personal_details_ob.dob_day)
record.set_field_value('Dob_Month', personal_details_ob.dob_month)
record.set_field_value('Dob_Year', personal_details_ob.dob_year)
record.set_field_value('Gender', personal_details_ob.sex)
record.set_field_value('Marital_Status', personal_details_ob.marital_status)
record.set_field_value('Partner_Name', personal_details_ob.partner_full_name)
record.set_field_value('Partner_Gender', personal_details_ob.partner_gender)
record.set_field_value('Partner_Email', personal_details_ob.partner_email)
record.set_field_value('Children', personal_details_ob.children)
record.set_field_value('Pets', personal_details_ob.pets)
response = record.create()
# save account id to db for future updates
zoho_ob.account = response.details['id']
zoho_ob.save()
except ZCRMException as ex:
logger.log(1, ex.status_code)
logger.log(1, ex.error_message)
logger.log(1, ex.error_details)
logger.log(1, ex.error_content)
print(ex.status_code)
print(ex.error_message)
print(ex.error_content)
print(ex.error_details)
Ive tried running ZCRMRestClient.initialize(zoho_config) in settings.py, with no luck.
My method for getting the access token and refresh token, which seems to work is:
import os
import pprint
from sys import argv
import django
import requests
import zcrmsdk
from django.conf import settings
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wills_online.settings')
django.setup()
def zoho_refresh_token(code):
""" supply a self client token from the zoho api credentials from web site """
zoho_config = {"apiBaseUrl": "https://www.zohoapis.com",
"currentUserEmail": settings.ZOHO_CURRENT_USER_EMAIL,
"client_id": settings.ZOHO_CLIENT_ID,
"client_secret": settings.ZOHO_CLIENT_SECRET,
"redirect_uri": settings.ZOHO_REDIRECT_URI,
"token_persistence_path": settings.ZOHO_PATH}
pprint.pprint(zoho_config)
print('working')
address = f'https://accounts.zoho.com/oauth/v2/token?code={code}&redirect_uri={settings.ZOHO_REDIRECT_URI}&client_id={settings.ZOHO_CLIENT_ID}&client_secret={settings.ZOHO_CLIENT_SECRET}&grant_type=authorization_code'
response = requests.post(address)
data = response.json()
pprint.pprint(data)
zcrmsdk.ZCRMRestClient.initialize(zoho_config)
oauth_client = zcrmsdk.ZohoOAuth.get_client_instance()
refresh_token = data['refresh_token']
print(type(refresh_token))
oauth_client.generate_access_token_from_refresh_token(refresh_token, settings.ZOHO_CURRENT_USER_EMAIL)
print(refresh_token)
print('finished')
if name == 'main':
zoho_refresh_token(argv[1])
This is driving me mad. Help would be greatly appreciated. This is my first post so go easy, lol.
For future reference, you will need to define persistence_handler_class and persistence_handler_path in your configuration dictionary. You will also need a handler class and a user-defined model to store the results. Sample code follows:
# settings.py
import zcrmsdk
configuration_dictionary = {
'apiBaseUrl': 'https://www.zohoapis.com',
'apiVersion': 'v2',
'currentUserEmail': ZOHO_CURRENT_USER_EMAIL,
'sandbox': 'False',
'applicationLogFilePath': '',
'client_id': ZOHO_CLIENT_ID,
'client_secret': ZOHO_CLIENT_SECRET,
'redirect_uri': ZOHO_REDIRECT_URI,
'accounts_url': 'https://accounts.zoho.com',
'access_type': 'online',
'persistence_handler_class': ZOHO_HANDLER_CLASS,
'persistence_handler_path': ZOHO_HANDLER_PATH,
}
zcrmsdk.ZCRMRestClient.initialize(configuration_dictionary)
# zoho.models.py
from django.db import models
from zcrmsdk.OAuthClient import ZohoOAuthTokens
class ZohoOAuthHandler:
#staticmethod
def get_oauthtokens(email_address):
oauth_model_instance = ZohoOAuth.objects.get(user_email=email_address)
return ZohoOAuthTokens(oauth_model_instance.refresh_token,
oauth_model_instance.access_token,
oauth_model_instance.expiry_time,
user_email=oauth_model_instance.user_email)
#staticmethod
def save_oauthtokens(oauth_token):
defaults = {
'refresh_token': oauth_token.refreshToken,
'access_token': oauth_token.accessToken,
'expiry_time': oauth_token.expiryTime,
}
ZohoOAuth.objects.update_or_create(user_email=oauth_token.userEmail, defaults=defaults)
class ZohoOAuth(models.Model):
refresh_token = models.CharField(max_length=250)
access_token = models.CharField(max_length=250)
expiry_time = models.BigIntegerField()
user_email = models.EmailField()
In this example ZOHO_HANDLER_CLASS = 'ZohoOAuthHandler' and ZOHO_HANDLER_PATH = 'zoho.models'
The first time you go to use this you will need a grant_token from https://accounts.zoho.com/developerconsole. For the scope use aaaserver.profile.READ,ZohoCRM.modules.ALL to start (see https://www.zoho.com/crm/developer/docs/api/oauth-overview.html#scopes)
Before you can use the api you'll need to run the code below in a django shell. This uses a grant token to generate your initial access and refresh tokens. Afterwards, the api should handle refreshing your access token.
grant_token = GRANT_TOKEN
import zcrmsdk
oauth_client = zcrmsdk.ZohoOAuth.get_client_instance()
oauth_tokens = oauth_client.generate_access_token(grant_token)

Why i am getting 400 Bad Request error when sending json data in Flask?

I am trying to write a small restful api application, i am using Chrome Postman extension for sending requests to the app .
I believe that my code does not have mistakes but every time i am sending post request a 400 Bad Request error raising , here is my code:
#api_route.route('/api', methods=['GET'])
def api():
return jsonify({'message':'Api v1.0'})
#api_route.route('/api', methods=['POST'])
def create_user():
data = request.get_json()
if data:
hashed_password = generate_password_hash(data['password'], method='sha256')
api = Api(email=data['email'], password=hashed_password)
db.session.add(api)
db.session.commit()
return jsonify({'message', 'New User Created!'})
The json data that i am sending looks like this:
{"email" : "Test", "password" : "123123123"}
Why i am getting the 400 error ??
Update:
Screenshots for the requests using Postman:
GET Request
POST Request
Here i am initiating api route inside api controller :
from flask import Blueprint
api_route = Blueprint(
'api',
__name__
)
from . import views
then i am registering it inside def create_app() function :
from .api import api_route
app.register_blueprint(api_route)
Here are the extensions that i am using in my application:
toolbar = DebugToolbarExtension()
assets_env = Environment()
cache = Cache()
moment = Moment()
htmlminify = HTMLMIN()
csrf = CSRFProtect()
jac = JAC()
googlemap = GoogleMaps()
session = Session()
principal = Principal()
I solved the problem, i've initiated CSRFProtect with app so i need to include X-CSRFToken in all my requests, so i have two choices:
1 - To include the csrf_token in request.headers for all the requests
2 - Using #csrf.exempt decorator that coming with flask_wtf.csrf
For now i am using #csrf.exempt, so it become like this:
#api_route.route('/api', methods=['GET','POST'])
#csrf.exempt
def create_user():
if request.method == 'GET':
return jsonify({'message' : 'API v1.0'})
elif request.method == 'POST':
data = request.get_json()
hashed_password = generate_password_hash(data['password'], method='sha256')
new_user_api = Api(email=data['email'], password=hashed_password)
db.session.add(new_user_api)
db.session.commit()
return jsonify({'message' : 'New user created!'})
return return jsonify({'message' : 'No user has been added!'})
Thanks for #MrPyCharm for his interests , salute :) .
A good approach would be to structure your views as follows:
Instead of creating view with same route for different request methods, you can handle the request methods in the same view:
#api_route.route('/api', methods=['GET', 'POST'])
def api():
if request.method == 'GET':
return jsonify({'message':'Api v1.0'})
else:
data = request.get_json(force=True)
if data:
hashed_password = generate_password_hash(data['password'], method='sha256')
api = Api(email=data['email'], password=hashed_password)
db.session.add(api)
db.session.commit()
return jsonify({'message': 'New User Created!'})
# Just in case the if condition didn't satisfy
return None
A note for anyone else experiencing this with PostMan and Flask - you will also hit a HTTP 404 if your URL in PostMan is HTTPS but your Flask app only handles HTTP.

mocking a method on django model using post_save signal

So here's something I'm trying to figure out. I've got a method that is triggered by post_save
for this "Story" model. Works fine. What I need to do is figure out how to mock out the test, so I can fake the call and make assertions on my returns. I think I need to patch it somehow, but I've tried a couple different ways without much success. Best i can get is a object instance, but it ignores values I pass in.
I've commented in my test where my confusion lies. Any help would be welcome.
Here's my test:
from django.test import TestCase
from django.test.client import Client
from marketing.blog.models import Post, Tag
from unittest.mock import patch, Mock
class BlogTestCase(TestCase):
fixtures = [
'auth-test.json',
'blog-test.json',
]
def setUp(self):
self.client = Client()
def test_list(self):
# verify that we can load the list page
r = self.client.get('/blog/')
self.assertEqual(r.status_code, 200)
self.assertContains(r, "<h1>The Latest from Our Blog</h1>")
self.assertContains(r, 'Simple JavaScript Date Formatting')
self.assertContains(r, 'Page 1 of 2')
# loading a page out of range should redirect to last page
r = self.client.get('/blog/5/', follow=True)
self.assertEqual(r.redirect_chain, [
('http://testserver/blog/2/', 302)
])
self.assertContains(r, 'Page 2 of 2')
# verify that unpublished posts are not displayed
with patch('requests') as mock_requests:
# my futile attempt at mocking.
# creates <MagicMock> object but not able to call return_values
mock_requests.post.return_value = mock_response = Mock()
# this doesn't get to the magic mock object. Why?
mock_response.status_code = 201
p = Post.objects.get(id=5)
p.published = False
# post_save signal runs here and requests is called.
# Needs to be mocked.
p.save()
r = self.client.get('/blog/')
self.assertNotContains(r, 'Simple JavaScript Date Formatting')
Here's the model:
from django.db import models
from django.conf import settings
from django.db.models import signals
import requests
def update_console(sender, instance, raw, created, **kwargs):
# ignoring raw so that test fixture data can load without
# hitting this method.
if not raw:
update = instance
json_obj = {
'author': {
'alias': 'the_dude',
'token': 'the_dude'
},
'text': update.description,
}
headers = {'content-type': 'application/json'}
path = 'http://testserver.com:80/content/add/'
request = requests(path, 'POST',
json_obj, headers=headers,
)
if request.status_code < 299:
story_id = request.json().get('id')
if story_id:
# disconnect and reconnect signal so
# we don't enter recursion-land
signals.post_save.disconnect(
update_console,
sender = Story, )
update.story_id = story_id
update.save()
signals.post_save.connect(
update_console,
sender = Story, )
else:
raise AttributeError('Error Saving to console, '+ request.text)
class Story(models.Model):
"""Lets tell a story"""
story_id = models.CharField(
blank=True,
max_length=10,
help_text="This maps to the id of the post"
)
slug = models.SlugField(
unique=True,
help_text="This is used in URL and in code references.",
)
description = models.TextField(
help_text='2-3 short paragraphs about the story.',
)
def __str__(self):
return self.short_headline
# add/update this record as a custom update in console
signals.post_save.connect(update_console, sender = Story)
You need to patch requests in the module where it is actually used, i.e.
with patch('path.to.your.models.requests') as mock_requests:
mock_requests.return_value.status_code = 200
mock_requests.return_value.json.return_value = {'id': story_id'}
...
The documentation offers more detailed explanations on where to patch:
patch works by (temporarily) changing the object that a name points to with another one. There can be many names pointing to any individual object, so for patching to work you must ensure that you patch the name used by the system under test.
The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined.
Here, you need to patch the name requests inside the models module, hence the need to provide its full path.

Why does Flask-Security Cause a new KVSession Record for Each Request?

I'm trying out using Flask-KVSession as an alternative session implementation for a Flask web site. I've created a test website (see Code 1 below). When I run this, I can use the browser to store values into the session by navigating between the various resources in my web browser. This works correctly. Also, when I look at the sessions table in the resulting SQLite database, I see a single record that was being used to store this session the entire time.
Then I try to add Flask-Security to this (see Code 2 below). After running this site (making sure to first delete the existing test.db sqlite file), I am brought to the login prompt and I log in. Then I proceed to do the same thing of jumping back and forth between the resources. I get the same results.
The problem is that when I look in the sqlitebrowser sessions table, there are 8 records. It turns out a new session record was created for EACH request that was made.
Why does a new session record get created for each request when using Flask-Security? Why isn't the existing session updated like it was before?
Code 1 (KVSession without Flask-Security)
import os
from flask import Flask, session
app = Flask(__name__)
app.secret_key = os.urandom(64)
#############
# SQLAlchemy
#############
from flask.ext.sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
DB_DIR = os.path.dirname(os.path.abspath(__file__))
DB_URI = 'sqlite:////{0}/test.db'.format(DB_DIR)
app.config['SQLALCHEMY_DATABASE_URI'] = DB_URI
#app.before_first_request
def create_user():
db.create_all()
############
# KVSession
############
from simplekv.db.sql import SQLAlchemyStore
from flask.ext.kvsession import KVSessionExtension
store = SQLAlchemyStore(db.engine, db.metadata, 'sessions')
kvsession = KVSessionExtension(store, app)
#app.route('/a')
def a():
session['last'] = 'b'
return 'Thank you for visiting A!'
#app.route('/b')
def b():
session['last'] = 'b'
return 'Thank you for visiting B!'
#app.route('/c')
def c():
return 'You last visited "{0}"'.format(session['last'])
app.run(debug=True)
Code 2 (KVSession WITH Flask-Security)
import os
from flask import Flask, session
app = Flask(__name__)
app.secret_key = os.urandom(64)
#############
# SQLAlchemy
#############
from flask.ext.sqlalchemy import SQLAlchemy
db = SQLAlchemy(app)
DB_DIR = os.path.dirname(os.path.abspath(__file__))
DB_URI = 'sqlite:////{0}/test.db'.format(DB_DIR)
app.config['SQLALCHEMY_DATABASE_URI'] = DB_URI
###########
# Security
###########
# This import needs to happen after SQLAlchemy db is created above
from flask.ext.security import (
Security, SQLAlchemyUserDatastore, current_user,
UserMixin, RoleMixin, login_required
)
# Define models
roles_users = db.Table('roles_users',
db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))
class Role(db.Model, RoleMixin):
id = db.Column(db.Integer(), primary_key=True)
name = db.Column(db.String(80), unique=True)
description = db.Column(db.String(255))
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True)
password = db.Column(db.String(255))
active = db.Column(db.Boolean())
confirmed_at = db.Column(db.DateTime())
roles = db.relationship('Role', secondary=roles_users,
backref=db.backref('users', lazy='dynamic'))
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)
#app.before_first_request
def create_user():
db.create_all()
user_datastore.create_user(email='test#example.com', password='password')
db.session.commit()
############
# KVSession
############
from simplekv.db.sql import SQLAlchemyStore
from flask.ext.kvsession import KVSessionExtension
store = SQLAlchemyStore(db.engine, db.metadata, 'sessions')
kvsession = KVSessionExtension(store, app)
#app.route('/a')
#login_required
def a():
session['last'] = 'b'
return 'Thank you for visiting A!'
#app.route('/b')
#login_required
def b():
session['last'] = 'b'
return 'Thank you for visiting B!'
#app.route('/c')
#login_required
def c():
return 'You last visited "{0}"'.format(session['last'])
app.run(debug=True)
Version Info
Python 2.7.3
Flask==0.9
Flask==0.9
Flask-KVSession==0.3.2
Flask-Login==0.1.3
Flask-Mail==0.8.2
Flask-Principal==0.3.5
Flask-SQLAlchemy==0.16
Flask-Security==1.6.3
SQLAlchemy==0.8.1
Turns out this is related to a known problem with flask-login (which flask-security uses) when flask-login is used with a session storage library like KVSession.
Basically, KVSession needs to update the database with the new session information whenever data in the session is created or modified. And in the sample above, this happens correctly: the first time I hit a page, the session is created. After that, the existing session is updated.
However, in the background the browser sends a cookie-less request to my web server looking for my favicon. Therefore, flask is handling a request to /favicon.ico. This request (or any other request that would 404) is still handled by flask. This means that flask-login will look at the request and try to do its magic.
It so happens that flask-login doesn't TRY to put anything into the session, but it still LOOKS like the session has been modified as far as KVSession is concerned. Because it LOOKS like the session is modified, KVSession updates the database. The following is code from flask-login:
def _update_remember_cookie(self, response):
operation = session.pop("remember", None)
...
The _update_remember_cookie method is called during the request lifecycle. Although session.pop will not change the session if the session doesn't have the "remember" key (which in this case it doesn't), KVSession still sees a pop and assumes that the session changes.
The issue for flask-login provides the simple bug fix, but it has not been pushed into flask-login. It appears that the maintainer is looking for a complete rewrite, and will implement it there.