Como agendar uma função para rodar a cada hora no Flask?

98

Eu tenho uma hospedagem na web Flask sem acesso a cron comando.

Como posso executar alguma função Python a cada hora?

RomaValcer
fonte

Respostas:

104

Você pode usar BackgroundScheduler()a partir APScheduler pacote (v3.5.3):

import time
import atexit

from apscheduler.schedulers.background import BackgroundScheduler


def print_date_time():
    print(time.strftime("%A, %d. %B %Y %I:%M:%S %p"))


scheduler = BackgroundScheduler()
scheduler.add_job(func=print_date_time, trigger="interval", seconds=3)
scheduler.start()

# Shut down the scheduler when exiting the app
atexit.register(lambda: scheduler.shutdown())

Observe que dois desses agendadores serão iniciados quando o Flask estiver no modo de depuração. Para mais informações, verifique esta questão.

tuomastik
fonte
1
@ user5547025 Como o cronograma funciona, suponha que coloquei o conteúdo em schedule.py, como ele será executado automaticamente?
Kishan Mehta
2
Acho que a programação, conforme sugerido pelo usuário5547025, é para tarefas síncronas que podem bloquear o thread mestre. Você precisará ativar um thread de trabalho para que ele não bloqueie.
Simon
1
se flasktivesse um App.runonceou App.runForNsecondsvocê poderia alternar entre schedulee o corredor do frasco, mas esse não é o caso, então a única maneira por enquanto é usando isso
lurscher
Obrigado por isso! Onde devo inserir esta função, em if name __ == "__ main "? Também podemos substituir a função print_date_time pela nossa função, certo?
Ambleu
Como executar o planejador para todos os dias uma vez?
arun kumar
56

Você poderia fazer uso de APScheduler em seu aplicativo Flask e executar seus trabalhos por meio de sua interface:

import atexit

# v2.x version - see https://stackoverflow.com/a/38501429/135978
# for the 3.x version
from apscheduler.scheduler import Scheduler
from flask import Flask

app = Flask(__name__)

cron = Scheduler(daemon=True)
# Explicitly kick off the background thread
cron.start()

@cron.interval_schedule(hours=1)
def job_function():
    # Do your work here


# Shutdown your cron thread if the web process is stopped
atexit.register(lambda: cron.shutdown(wait=False))

if __name__ == '__main__':
    app.run()
Sean Vieira
fonte
1
Posso fazer uma pergunta para novatos? Por que existe lambdadentro atexit.register?
Pigmalião
2
Porque atexit.registerprecisa de uma função para chamar. Se apenas cron.shutdown(wait=False)passarmos, estaremos passando o resultado da chamada cron.shutdown(que provavelmente é None). Então, em vez disso, passamos uma função de argumento zero, e em vez de dar a ela um nome e usar uma instrução def shutdown(): cron.shutdown(wait=False) e atexit.register(shutdown), em vez disso, registramos inline com lambda:(que é uma expressão de função de argumento zero .)
Sean Vieira
Obrigado. Portanto, o problema é que queremos passar o argumento para a função, se bem entendi.
Pigmalião
49

Eu sou um pouco novo com o conceito de agendadores de aplicativos, mas o que encontrei aqui para APScheduler v3.3.1 , é algo um pouco diferente. Acredito que para as versões mais recentes, a estrutura do pacote, os nomes das classes, etc., mudaram, então estou colocando aqui uma solução nova que fiz recentemente, integrada com um aplicativo Flask básico:

#!/usr/bin/python3
""" Demonstrating Flask, using APScheduler. """

from apscheduler.schedulers.background import BackgroundScheduler
from flask import Flask

def sensor():
    """ Function for test purposes. """
    print("Scheduler is alive!")

sched = BackgroundScheduler(daemon=True)
sched.add_job(sensor,'interval',minutes=60)
sched.start()

app = Flask(__name__)

@app.route("/home")
def home():
    """ Function for test purposes. """
    return "Welcome Home :) !"

if __name__ == "__main__":
    app.run()

Eu também estou deixando este Gist aqui , se alguém tiver interesse em atualizações para este exemplo.

Aqui estão algumas referências, para leituras futuras:

Ivanleoncz
fonte
2
Isso funciona muito bem, espero que ele seja votado em posição superior à medida que mais pessoas virem este tópico.
Mwspencer
1
Você já tentou usar isso em um aplicativo que fica na web, como PythonAnywhere ou algo assim?
Mwspencer
1
Obrigado, @Mwspencer. Sim, eu usei e funciona bem :), embora eu recomende que você explore mais opções fornecidas por apscheduler.schedulers.background, pois pode ser possível que você encontre outros cenários úteis para sua aplicação. Saudações.
ivanleoncz,
2
Não se esqueça de desligar o programador quando o aplicativo existir
Hanynowsky,
1
Olá! você pode dar alguns conselhos para uma situação em que há vários trabalhadores gunicorn? quero dizer, o planejador será executado uma vez por trabalhador?
ElPapi42
13

Você pode tentar usar o BackgroundScheduler do APScheduler para integrar o trabalho de intervalo em seu aplicativo Flask. Abaixo está o exemplo que usa blueprint e app factory ( init .py):

from datetime import datetime

# import BackgroundScheduler
from apscheduler.schedulers.background import BackgroundScheduler
from flask import Flask

from webapp.models.main import db 
from webapp.controllers.main import main_blueprint    

# define the job
def hello_job():
    print('Hello Job! The time is: %s' % datetime.now())

def create_app(object_name):
    app = Flask(__name__)
    app.config.from_object(object_name)
    db.init_app(app)
    app.register_blueprint(main_blueprint)
    # init BackgroundScheduler job
    scheduler = BackgroundScheduler()
    # in your case you could change seconds to hours
    scheduler.add_job(hello_job, trigger='interval', seconds=3)
    scheduler.start()

    try:
        # To keep the main thread alive
        return app
    except:
        # shutdown if app occurs except 
        scheduler.shutdown()

Espero que ajude :)

Ref:

  1. https://github.com/agronholm/apscheduler/blob/master/examples/schedulers/background.py
KD Chang
fonte
1
Tenho certeza de que uma declaração de retorno nunca levantará uma exceção
Tamas Hegedus
11

Para uma solução simples, você pode adicionar uma rota, como

@app.route("/cron/do_the_thing", methods=['POST'])
def do_the_thing():
    logging.info("Did the thing")
    return "OK", 200

Em seguida, adicione um cron job do Unix que efetua POST neste terminal periodicamente. Por exemplo, para executá-lo uma vez por minuto, no tipo de terminal crontab -ee adicionar esta linha:

* * * * * /opt/local/bin/curl -X POST https://YOUR_APP/cron/do_the_thing

(Observe que o caminho para curl deve ser concluído, pois quando o trabalho for executado, ele não terá seu PATH. Você pode descobrir o caminho completo para curl em seu sistema which curl)

Eu gosto disso porque é fácil testar o trabalho manualmente, não tem dependências extras e como não há nada de especial acontecendo, é fácil de entender.

Segurança

Se quiser proteger seu cron job com senha, você pode pip install Flask-BasicAuthe, em seguida, adicione as credenciais à configuração do aplicativo:

app = Flask(__name__)
app.config['BASIC_AUTH_REALM'] = 'realm'
app.config['BASIC_AUTH_USERNAME'] = 'falken'
app.config['BASIC_AUTH_PASSWORD'] = 'joshua'

Para proteger com senha o endpoint do trabalho:

from flask_basicauth import BasicAuth
basic_auth = BasicAuth(app)

@app.route("/cron/do_the_thing", methods=['POST'])
@basic_auth.required
def do_the_thing():
    logging.info("Did the thing a bit more securely")
    return "OK", 200

Então, para chamá-lo de seu cron job:

* * * * * /opt/local/bin/curl -X POST https://falken:joshua@YOUR_APP/cron/do_the_thing
Bemmu
fonte
1
Você é um génio! dica realmente útil.
Sharl Sherif
6

Outra alternativa pode ser usar Flask-APScheduler, que funciona bem com o Flask, por exemplo:

  • Carrega a configuração do agendador da configuração do Flask,
  • Carrega definições de trabalho da configuração do Flask

Mais informações aqui:

https://pypi.python.org/pypi/Flask-APScheduler

Mads Jensen
fonte
4

Um exemplo completo usando agendamento e multiprocessamento, com controle ligado e desligado e parâmetro para run_job (), os códigos de retorno são simplificados e o intervalo é definido para 10seg, mude para every(2).hour.do()para 2 horas. O cronograma é bastante impressionante, ele não oscila e eu nunca vi isso com mais de 100ms de atraso ao programar. Usando multiprocessamento em vez de threading porque tem um método de terminação.

#!/usr/bin/env python3

import schedule
import time
import datetime
import uuid

from flask import Flask, request
from multiprocessing import Process

app = Flask(__name__)
t = None
job_timer = None

def run_job(id):
    """ sample job with parameter """
    global job_timer
    print("timer job id={}".format(id))
    print("timer: {:.4f}sec".format(time.time() - job_timer))
    job_timer = time.time()

def run_schedule():
    """ infinite loop for schedule """
    global job_timer
    job_timer = time.time()
    while 1:
        schedule.run_pending()
        time.sleep(1)

@app.route('/timer/<string:status>')
def mytimer(status, nsec=10):
    global t, job_timer
    if status=='on' and not t:
        schedule.every(nsec).seconds.do(run_job, str(uuid.uuid4()))
        t = Process(target=run_schedule)
        t.start()
        return "timer on with interval:{}sec\n".format(nsec)
    elif status=='off' and t:
        if t:
            t.terminate()
            t = None
            schedule.clear()
        return "timer off\n"
    return "timer status not changed\n"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Você testa isso apenas emitindo:

$ curl http://127.0.0.1:5000/timer/on
timer on with interval:10sec
$ curl http://127.0.0.1:5000/timer/on
timer status not changed
$ curl http://127.0.0.1:5000/timer/off
timer off
$ curl http://127.0.0.1:5000/timer/off
timer status not changed

A cada 10 segundos, o cronômetro está ativado, ele emitirá uma mensagem de cronômetro para o console:

127.0.0.1 - - [18/Sep/2018 21:20:14] "GET /timer/on HTTP/1.1" 200 -
timer job id=b64ed165-911f-4b47-beed-0d023ead0a33
timer: 10.0117sec
timer job id=b64ed165-911f-4b47-beed-0d023ead0a33
timer: 10.0102sec
MortenB
fonte
Não sou um especialista em multiprocessamento, mas se você usar isso, provavelmente obterá erros de pickle.
Patrick Mutuku
@PatrickMutuku, o único problema que vejo com serialização digital (cookies, arquivos temporários) é assíncrono e websockets, mas o Flask não é sua API, olhe em github.com/kennethreitz/responder . O Flask é excelente em REST puro com um frontend simples no apache wsgi.
MortenB de
1

Você pode querer usar algum mecanismo de fila com agendador como o agendador RQ ou algo mais pesado como Celery (provavelmente um exagero).

Alexander Davydov
fonte