0

I'm in the process of modifying the Flask app created in following along Miguel Grinberg's Flask Mega Tutorial such that it is possible to post tweets. I have imported tweepy for accessing the twitter api and modified the databases to hold the scheduled time of a tweet. I wish to iterate over the current_user's posts and the corresponding times from the SQLAlchemy database and post when the current time matches the scheduled time.

The database model modifications in model.py are as follows:

class Post(db.Model):
id = db.Column(db.Integer, primary_key = True)
body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
socialnetwork = db.Column(db.String(40))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
#This is the stuff for scheduling, just date
hour = db.Column(db.Integer)
minute = db.Column(db.Integer)
day = db.Column(db.Integer)
month = db.Column(db.Integer)
year = db.Column(db.Integer)
ampm = db.Column(db.String(2))

Just as a test, I wanted to iterate over the current user's posts and tweet them using tweepy:

@app.before_first_request
def activate_job():
    def run_job():
        posts = current_user.followed_posts().filter_by(socialnetwork ='Twitter')
        for post in posts:
            tweepy_api.update_status(message)

            time.sleep(30)
    thread = threading.Thread(target=run_job)
    thread.start()

However, this returned the error:

AttributeError: 'NoneType' object has no attribute 'followed_posts'

on the terminal. This is perplexing me as I have used current_user multiple times in the same file to filter the posts by social network.

As in the following case in routes.py

@app.route('/<username>')
@login_required
def user(username):
    user = User.query.filter_by(username = username).first_or_404()
    socialnetwork = request.args.get("socialnetwork")

    if socialnetwork == 'Facebook':

    posts = current_user.followed_posts().filter_by(socialnetwork = 'Facebook')

    elif socialnetwork == 'Twitter':
        posts = current_user.followed_posts().filter_by(socialnetwork = 'Twitter')
    else:
        posts = current_user.followed_posts()


    return render_template('user.html', user = user, posts = posts, form = socialnetwork)

The above yields no error and works perfectly.

If anyone could shed some light on what I am doing wrong, I'd be truly grateful.

AviS
  • 582
  • 7
  • 13

1 Answers1

0

You're likely running into issues because you're trying to get current_user on a different thread (see the Flask docs for more details). You're calling run_job() in a different context that doesn't have any current user (because there's no active request).

I'd rework it so that you get the current user's posts on the main thread (i.e. in activate_job(), then pass the list of posts to the background process to work on.

Something like:

def activate_job():
    posts = current_user.followed_posts().filter_by(socialnetwork ='Twitter')
    def run_job(posts):
        for post in posts:
            tweepy_api.update_status(message)

            time.sleep(30)
    thread = threading.Thread(target=run_job, args=[posts])
    thread.start()

It's also worth noting that you may want to rethink your overall approach. Rather than checking with each request if there are any scheduled tweets to send, you should use some sort of background task queue that an operate independently of the web process. That way, you're not checking redundantly on each request, and you're not dependant on the user making requests around the scheduled time.

See The Flask Mega-Tutorial Part XXII: Background Jobs for more details, and look into Celery.

robmathers
  • 3,028
  • 1
  • 26
  • 29
  • 1
    Thank you, I am still unclear as to how I should go about getting the current user's posts on the main thread. Perhaps I misunderstood, but shouldn't the thread created in my 'hacked-up' function be an auxiliary thread and the other thread(the one in which I execute the other functions, views etc.), the main thread? – AviS Jun 13 '18 at 19:50
  • I've added a quick example of how to change your code. It may still require some changes to actually work, but should get you started. – robmathers Jun 13 '18 at 19:55
  • 1
    Thank you so much, will try, tinker a little and let you know – AviS Jun 13 '18 at 19:57
  • 1
    It seems to work except for the face that, the following error is spawned: SQLite objects created in a thread can only be used in that same thread. As a remedial measure, I discovered that the following line should be added to the config.py: SQLALCHEMY_DATABASE_URI = 'sqlite:///app.db? check_same_thread=False'. However, I have connected the databse as follows: SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 'sqlite:///'+os.path.join(basedir, 'app.db'). I'd really appreciate it if you could offer any guidance to change the last statement to reflect the former. – AviS Jun 13 '18 at 20:40
  • 1
    I apologize about the clutter, I don't know how to include code in comments. – AviS Jun 13 '18 at 20:45
  • I'd suggest you rethink your approach, so you can avoid needing to access the database on the background thread. Ideally, look into the task queue approach I suggested. Additionally, if you need to post code blocks, updating the question is probably the best place for additional info. – robmathers Jun 14 '18 at 19:39
  • 1
    Thank you, I just needed something quick for a demonstration. I'm definitely going to use Celery/RQ as you suggested. – AviS Jun 15 '18 at 14:59