Flask app with sqlalchemy trying to schedule a job with Flask-APScheduler - python

It works well when executing the flask app, but we get the following error when executing in Flask-APScheduler context:
AttributeError: Neither 'InstrumentedAttribute' object nor 'Comparator' object associated with SMSMessage.logs has an attribute 'count'
We sub-classed the APScheduler class because we felt we could force it to use our flask context.
My model is defined as:
class SMSMessage(BaseModel):
__tablename__ = 'sms_message'
logs = relationship("SMSLog", backref='sms', lazy='dynamic')
#hybrid_property
def last_status(self):
if self.logs.count() > 0:
last_status = self.logs.order_by(SMSLog.date_created.desc()).first()
return last_status.at_status_code
return "NA"
The job function is defined in main function file as:
import os
import dateutil.parser
from flask_apscheduler import APScheduler as _BaseAPScheduler
from app import settings
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from flask import Blueprint, Flask
from datetime import datetime
from app.database import db
from app.database.models import SMSMessage, SMSLog
class APScheduler(_BaseAPScheduler):
def run_job(self, id, jobstore=None):
with self.app.app_context():
super().run_job(id=id, jobstore=jobstore)
application = Flask(__name__)
def run_queued_messages():
with application.app_context():
sms_messages = SMSMessage.query.filter(
SMSMessage.last_status == 'Queued',
SMSMessage.send_at <= datetime.now()
).all()
for sms_message in sms_messages:
print('do something')
def configure_app(flask_app):
DB_HOST = os.getenv("DB_HOST")
DB_PORT = os.getenv("DB_PORT")
DB_DATABASE = os.getenv("DB_DATABASE")
DB_USER = os.getenv("DB_USER")
DB_PASSWORD = os.getenv("DB_PASSWORD")
flask_app.config['SQLALCHEMY_DATABASE_URI'] = f'postgresql+psycopg2://{DB_USER}:{DB_PASSWORD}#{DB_HOST}:{DB_PORT}/{DB_DATABASE}'
flask_app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = settings.SQLALCHEMY_TRACK_MODIFICATIONS
flask_app.config['SCHEDULER_JOBSTORES'] = {
'default': SQLAlchemyJobStore(
url=flask_app.config['SQLALCHEMY_DATABASE_URI'],
tableschema=os.getenv('SCHEMA_NAME')
),
}
# do not allow for api access job management
flask_app.config['SCHEDULER_API_ENABLED'] = False
flask_app.config['JOBS'] = [
{
'id': None,
'func': run_queued_messages,
'trigger': 'interval',
'seconds': 1,
'replace_existing': True
}
]
flask_app.config['SCHEDULER_EXECUTORS'] = {
'default': {'type': 'threadpool', 'max_workers': 20}
}
flask_app.config['SCHEDULER_JOB_DEFAULTS'] = {
'coalesce': False,
'max_instances': 3
}
def initialize_app(flask_app):
configure_app(flask_app)
db.init_app(flask_app)
initialize_app(application)
db.application = application
scheduler = APScheduler()
scheduler.init_app(application)
scheduler.start()

self.logs is not a collection of logs, but a reference to Logs as a relationship. You actually need to fetch the count first;
It's bad practice to count() when you actually just need to know if any log exists(). If we were cooking dinner and I asked if there was still any rice, I wouldn't expect you to count the individual grains of rice, but to just check if the pot was empty;
If no match exists, .first() returns None. Use it to your advantage:
#hybrid_property
def last_status(self):
last_status = SMSLog.query\
.filter(SMSLog.message_id == self.id)\
.order_by(SMSLog.date_created.desc())\
.first()
return last_status.at_status_code if last_status is not None else "NA"

Related

Database Query returning empty in test cases - Django

I have updated the Django version for my project from Django-2.2.16 --> Django3.2.14.
But with this update, some of my test cases are failing and I cannot understand the reason for the failure.
My test-case file:
import json
from os import access
from unittest import mock
from unittest.mock import patch
import asyncio
from app.models import UserProfile
from django.test import TestCase, TransactionTestCase
from requests.models import Response
from services.check_status import check_status
loop = asyncio.get_event_loop()
#mock.patch("services.check_status.save_status")
#mock.patch("services..check_status.send_wss_message")
class TestUserStatus(TransactionTestCase):
def setUp(self):
super().setUp()
self.account_id = 4
self.sim_profile = UserProfile.objects.create()
def test_check_status_completed(
self,
mock_send_wss_message,
mock_save_status,
):
mock_save_status.return_value = {}
send_wss_message_future = asyncio.Future()
send_wss_message_future.set_result(True)
mock_send_wss_message.return_value = send_wss_message_future
loop.run_until_complete(
check_status(
self.sim_profile.id,
)
)
self.assertTrue(mock_save_status.called)
self.assertTrue(mock_send_wss_message.called)
My pseudo check_status file is :-
import logging
from app.models import UserProfile, UserStatus
from services.constants import WebsocketGroups
from services.user.constants import USER
from app.api.serializers import UserStatusSerializer
from services.utils import send_wss_message, Client
logger = logging.getLogger(__name__)
def save_status(**kwargs):
status = UserStatus.objects.filter(
status_id=kwargs.get("status_id")
).first()
data = kwargs
user_status_serializer = UserStatusSerializer(status, data, partial=True)
if user_status_serializer.is_valid():
user_status_serializer.save()
async def check_status(
profile_id
):
user_profile = UserProfile.objects.get(id=profile_id)
login_token = get_login_token(user_profile)
user_creds = env["user_api"]
headers = USER["headers"]
subscription_details = Client.get(
USER["url"], headers
)
transaction_status = subscription_details.json()["Status"]
subscription_data = subscription_details.json()["Data"][0]
transaction_status_details = subscription_data["TransactionStatusDetails"]
error_message = ""
status = ""
if transaction_status == "Success":
#perform some actions and save status...
message = {
"type": "user_profile",
"data": [user_profile.id, transaction_status, {"results": {}},],
}
await send_wss_message(
user_profile.id, message=message, group_name=WebsocketGroups.USER_PROFILE,
)
else:
#perform some actions ...
When I am running my test-case file it's creating the UserProfile object but when control goes to the check_status function in int UserProfile.objects.all returns <QuerySet []>.
I made a temporary sync function to return a list of all user profiles and called it inside my test_check_status_completed and it returned the list. But for async functions that are called through the loop.run_until_complete, they all returned <QuerySet []>.

Homework project. Can't figure out how sum values via flask_sqlalchemy. Flask tossing 'AttributeError: 'BaseQuery' object has no attribute 'sum''

This is the problem. I can do count() (the count of this query is 1617) but can't figure out how to do a sum. FWIW, this is from a job satisfaction survey. Lots of 1 and 0 depending on whether they provided a response to a specific question.
This works:
#app.route('/list') def list_respondents(): all_working = Jf_Q1.query.filter((Jf_Q1.working==1) & (Jf_Q1.good_job==1)).count() return render_template('list.html', all_working=all_working)
This code above works, but what I need to be able to replicate this from postgres:
select sum(moderatewellbeing)/sum(good_job) from jf_q1
where working=1
and
good_job=1;
I've tried:
all_working = Jf_Q1.query.filter(Jf_Q1.working==1).sum()
return render_template('list.html', all_working=all_working)
But flask tosses me:
'AttributeError: 'BaseQuery' object has no attribute 'sum'
Here is all my code:
from flask import Flask,render_template, url_for, redirect
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func
from flask_migrate import Migrate
######################################
#### SET UP OUR SQLite DATABASE #####
####################################
app = Flask(__name__)
app.config['SECRET_KEY'] = 'mysecretkey'
# Connects our Flask App to our Database
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://postgres:M1keD8nJ0e#localhost:5432/project2'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
Migrate(app,db)
#####################################
####################################
###################################
# Let's create our first model!
# We inherit from db.Model class
class Jf_Q1(db.Model):
__tablename__ = 'jf_q1'
#########################################
## CREATE THE COLUMNS FOR THE TABLE ####
#######################################
# Primary Key column, unique id for each puppy
id = db.Column(db.Integer,primary_key=True)
respondent_id = db.Column(db.Text)
good_job = db.Column(db.Numeric)
mediocre_job = db.Column(db.Numeric)
bad_job = db.Column(db.Numeric)
highwellbeing = db.Column(db.Numeric)
moderatewellbeing = db.Column(db.Numeric)
lowwellbeing = db.Column(db.Numeric)
working = db.Column(db.Numeric)
# This sets what an instance in this table will have
# Note the id will be auto-created for us later, so we don't add it here!
def __init__(self,respondent_id,good_job,mediocre_job,bad_job,highwellbeing,moderatewellbeing,lowwellbeing,working):
self.respondent_id = respondent_id
self.good_job = good_job
self.mediocre_job = mediocre_job
self.bad_job = bad_job
self.highwellbeing = highwellbeing
self.moderatewellbeing = moderatewellbeing
self.lowwellbeing = lowwellbeing
self.working = working
# def __repr__(self):
#app.route('/')
def index():
return render_template('home.html')
#app.route('/list')
def list_respondents():
# all_working = Jf_Q1.query.filter((Jf_Q1.working==1) & (Jf_Q1.good_job==1)).count()
# return render_template('list.html', all_working=all_working)
all_working = Jf_Q1.query.filter(Jf_Q1.working==1).sum()
return render_template('list.html', all_working=all_working)
# all_working = select([func.sum(Jf_Q1.working)]).\
# where(Jf_Q1.working==1)
# return render_template('list.html', all_working=all_working)
if __name__ == '__main__':
app.run(debug=True)
you can try this:-
from sqlalchemy.sql import func
all_working = session.query(func.sum(Jf_Q1.working)).filter(Jf_Q1.working==1)
or also you can use with_entities
all_working = Jf_Q1.with_entities(func.sum(jf_Q1.working)).filter(Jf_Q1.working==1)

Flask-SQLALchemy update record automatically after specific time

I have a db models like this:
class Payment(db.Model):
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))
ticket_status = db.Column(db.Enum(TicketStatus, name='ticket_status', default=TicketStatus.UNUSED))
departure_time = db.Column(db.Date)
I want to change the value from all ticket_status after datetime.utcnow() passed the date value from departure_time.
I tried to code like this:
class TicketStatus(enum.Enum):
UNUSED = 'UNUSED'
USED = 'USED'
EXPIRED = 'EXPIRED'
def __repr__(self):
return str(self.value)
class Payment(db.Model):
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))
ticket_status = db.Column(db.Enum(TicketStatus, name='ticket_status', default=TicketStatus.UNUSED))
departure_time = db.Column(db.Date)
# TODO | set ticket expirations time
def __init__(self):
if datetime.utcnow() > self.departure_time:
self.ticket_status = TicketStatus.EXPIRED.value
try:
db.session.add(self)
db.session.commit()
except Exception as e:
db.session.rollback()
I also tried like this:
def ticket_expiration(self, payment_id):
now = datetime.utcnow().strftime('%Y-%m-%d')
payment = Payment.query.filter_by(id=payment_id).first()
if payment.ticket_status.value == TicketStatus.USED.value:
pass
elif payment and str(payment.departure_time) < now:
payment.ticket_status = TicketStatus.EXPIRED.value
elif payment and str(payment.departure_time) >= now:
payment.ticket_status = TicketStatus.UNUSED.value
try:
db.session.commit()
except Exception as e:
db.session.rollback()
return str('ok')
But it seems no effect when the datetime.utcnow() passed the date value from departure_time.
So the point of my questions is, how to change the value from a row automatically after a set of times..?
Finally I figure out this by using flask_apscheduler, and here is the snippet of my code that solved this questions:
Install flask_apscheduler:
pip3 install flask_apscheduler
create new module tasks.py
from datetime import datetime
from flask_apscheduler import APScheduler
from app import db
from app.models import Payment, TicketStatus
scheduler = APScheduler()
def ticket_expiration():
utc_now = datetime.utcnow().strftime('%Y-%m-%d')
app = scheduler.app
with app.app_context():
payment = Payment.query.all()
for data in payment:
try:
if data.ticket_status.value == TicketStatus.USED.value:
pass
elif str(data.departure_time) < utc_now:
data.ticket_status = TicketStatus.EXPIRED.value
elif str(data.departure_time) >= utc_now:
data.ticket_status = TicketStatus.UNUSED.value
except Exception as e:
print(str(e))
try:
db.session.commit()
except Exception as e:
db.session.rollback()
return str('ok')
and then register the package with the flask app in the __init__.py
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(Config)
# The other packages...
# The other packages...
scheduler.init_app(app)
scheduler.start()
return app
# import from other_module...
# To avoid SQLAlchemy circular import, do the import at the bottom.
from app.tasks import scheduler
And here is for the config.py:
class Config(object):
# The others config...
# The others config...
# Flask-apscheduler
JOBS = [
{
'id': 'ticket_expiration',
'func': 'app.tasks:ticket_expiration',
'trigger': 'interval',
'hours': 1, # call the task function every 1 hours
'replace_existing': True
}
]
SCHEDULER_JOBSTORES = {
'default': SQLAlchemyJobStore(url='sqlite:///flask_context.db')
}
SCHEDULER_API_ENABLED = True
In the config above, we can call the function to update db every 1 hours, seconds or others time according to our case, for more informations to set the interval time we can see it here.
I hope this answer helps someone who facing this in the future.
You may replace your status column with just "used" column which will contain Boolean value and make a hybrid attribute for state. https://docs.sqlalchemy.org/en/13/orm/extensions/hybrid.html
class Payment(db.Model):
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey('user.id'))
used = db.Column(db.Boolean(), default=False)
departure_time = db.Column(db.Date)
#hybrid_property
def status(self):
if datetime.utcnow() > self.departure_time:
return "EXPIRED"
elif self.used:
return "USED"
return "UNUSED"

Testing routing Blueprint - Flask Restplus

I'm trying to test the example of ToDo in Flask-Restplus site, but it keeps on getting me 404...
Basically I have 3 files:
app.py
import sys
import os
import platform
import datetime
import logging
from logging import Formatter
from logging.handlers import RotatingFileHandler
from jinja2 import Environment, PackageLoader
from flask import Flask, url_for, render_template, abort, request, Blueprint
from flask.ext.restplus import Api, Resource, fields
from werkzeug.contrib.fixers import ProxyFix
api_v1 = Blueprint('api', __name__, url_prefix='/api/1')
ns = api.namespace('todos', description='TODO operations')
TODOS = {
'todo1': {'task': 'build an API'},
'todo2': {'task': '?????'},
'todo3': {'task': 'profit!'},
}
todo = api.model('Todo', {
'task': fields.String(required=True, description='The task details')
})
listed_todo = api.model('ListedTodo', {
'id': fields.String(required=True, description='The todo ID'),
'todo': fields.Nested(todo, description='The Todo')
})
def abort_if_todo_doesnt_exist(todo_id):
if todo_id not in TODOS:
api.abort(404, "Todo {} doesn't exist".format(todo_id))
parser = api.parser()
parser.add_argument('task', type=str, required=True, help='The task details', location='form')
#ns.route('/<string:todo_id>')
#api.doc(responses={404: 'Todo not found'}, params={'todo_id': 'The Todo ID'})
class Todo(Resource):
'''Show a single todo item and lets you delete them'''
#api.doc(description='todo_id should be in {0}'.format(', '.join(TODOS.keys())))
#api.marshal_with(todo)
def get(self, todo_id):
'''Fetch a given resource'''
abort_if_todo_doesnt_exist(todo_id)
return TODOS[todo_id]
#api.doc(responses={204: 'Todo deleted'})
def delete(self, todo_id):
'''Delete a given resource'''
abort_if_todo_doesnt_exist(todo_id)
del TODOS[todo_id]
return '', 204
#api.doc(parser=parser)
#api.marshal_with(todo)
def put(self, todo_id):
'''Update a given resource'''
args = parser.parse_args()
task = {'task': args['task']}
TODOS[todo_id] = task
return task
#ns.route('/')
class TodoList(Resource):
'''Shows a list of all todos, and lets you POST to add new tasks'''
#api.marshal_list_with(listed_todo)
def get(self):
'''List all todos'''
return [{'id': id, 'todo': todo} for id, todo in TODOS.items()]
#api.doc(parser=parser)
#api.marshal_with(todo, code=201)
def post(self):
'''Create a todo'''
args = parser.parse_args()
todo_id = 'todo%d' % (len(TODOS) + 1)
TODOS[todo_id] = {'task': args['task']}
return TODOS[todo_id], 201
app = Flask(__name__)
app.secret_key = "secretkey"
app.config.from_pyfile('settings.py')
if os.path.exists(os.path.join(app.root_path, 'local_settings.py')):
app.config.from_pyfile('local_settings.py')
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgres://%s#%s/%s' % (
app.config['DB_USER'], app.config['DB_HOST'], app.config['DB_NAME'])
main.py
import sys
from flask import Blueprint
from portal.app import app
from portal import libs
if __name__ == '__main__':
if len(sys.argv) == 2:
port = int(sys.argv[1])
else:
port = 5000
host = app.config.get('HOST', '127.0.0.1')
from portal.app import api_v1
app.register_blueprint(api_v1)
import os
app.root_path = os.getcwd()
print "Running in", app.root_path, " with DEBUG=", app.config.get('DEBUG', False)
app.run(host,
port,
app.config.get('DEBUG', False),
use_reloader=True
)
tests.py
class ApiTests(helpers.ViewBase):
############################
#### setup and teardown ####
############################
def setUp(self):
super(ApiTests, self).setUp()
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
app.config['DEBUG'] = False
self.assertEquals(app.debug, False)
# executed after to each test
def tearDown(self):
pass
###############
#### tests ####
###############
def test_can_obtain_todos(self):
response = self.client.get('/api/1/todos')
self.assertEqual(response.status_code, 200)
If I run the app I can access http://localhost:5000/api/1/todos without problems, but if I run the tests, I keep to getting 404 routing exception
Traceback (most recent call last):
File "/home/internetmosquito/python_envs/portal/local/lib/python2.7/site-packages/flask/app.py", line 1639, in full_dispatch_request
rv = self.dispatch_request()
File "/home/internetmosquito/python_envs/portal/local/lib/python2.7/site-packages/flask/app.py", line 1617, in dispatch_request
self.raise_routing_exception(req)
File "/home/internetmosquito/python_envs/portal/local/lib/python2.7/site-packages/flask/app.py", line 1600, in raise_routing_exception
raise request.routing_exception
NotFound: 404: Not Found
> /home/internetmosquito/git/wizbots/portal/src/portal/test/test_api.py(47)test_can_obtain_todos()
Any idea what I'm missing here? thanks!
Just for the record, test was failing because I wasn't properly initializing the blueprint again in the test file...this is done in main.py to avoid circular dependencies issues I was having.
So simply re-creating the blueprint and assigning it to app in setUp in the test file does the trick, but this is not efficient and should be avoided, guess I should check why the circular dependencies is happening and do everythng in the app.py file instead...

Python / Flask / MongoEngine DateTimeField

First of I'm new to python and flask. I've searched around and tried something things to no avail. I have a model that has a DateTimeField as one of the members, let's call it "created_at". When I go to return the query set as JSON I see this for the field
...
"created_at": {
"$date": 1412938697488
}
...
Is there anyway to get the output, either through a custom JSON encoder, etc to get it to look like this :
"created_at": "2014-10-10T07:33:04Z",
Any guidance or suggestions would be greatly appreciated.
Thanks!
Here is an example using flask and flask-mongoengine to get a date as ISO 8601 string
import datetime
from bson.json_util import dumps
from flask import Flask, Response, request
from flask_mongoengine import MongoEngine
app = Flask(__name__)
db = MongoEngine()
class Movie(db.Document):
name = db.StringField(required=True, unique=True)
casts = db.ListField(db.StringField(), required=True)
genres = db.ListField(db.StringField(), required=True)
created_at = db.DateTimeField(default=datetime.datetime.utcnow)
#app.route('/movies')
def get_movies():
movies = Movie.objects()
movies_list = []
for movie in movies:
movie_dict = movie.to_mongo().to_dict()
movie_dict['created_at'] = movie.created_at.isoformat()
movies_list.append(movie_dict)
movies_josn = dumps(movies_list)
return Response(movies_josn, mimetype="application/json", status=200)
#app.route('/movies', methods=['POST'])
def add_movie():
body = request.get_json()
movie = Movie(**body).save()
id = movie.id
return {'id': str(id)}, 200
if __name__ == '__main__':
app.config['MONGODB_SETTINGS'] = {
'host': 'mongodb://localhost/movie-bag'
}
db.init_app(app)
app.run()

Categories