Flask: How to use url_for() outside the app context?

Viewed 502

I'm writing a script to collect the emails of those users that didn't receive an email confirmation email and resend it to them. The script works obviously outside of flask app context. I would like to use url_for() but can't get it right.

def resend(self, csv_path):
    self.ctx.push()
    with open(csv_path) as csv_file:
        csv_reader = csv.reader(csv_file)
        for row in csv_reader:
            email = row[0]
            url_token = AccountAdmin.generate_confirmation_token(email)
            confirm_url = url_for('confirm_email', token=url_token, _external=True)
        ...
    self.ctx.pop()

The first thing I had to do was to set SERVER_NAME in config. But then I get this error message:

werkzeug.routing.BuildError: Could not build url for endpoint 'confirm_email' with values ['token']. Did you mean 'static' instead?

This is how it's defined, but I don't think it can even find this, because it's not registered when ran as script:

app.add_url_rule('/v5/confirm_email/<token>', view_func=ConfirmEmailV5.as_view('confirm_email'))

Is there a way to salvage url_for() or do I have to build my own url? Thanks

1 Answers

It is much easier and proper to get the URL from the application context.

You can either import the application and manually push context with app_context https://flask.palletsprojects.com/en/2.0.x/appcontext/#manually-push-a-context

from flask import url_for
from whereyoudefineapp import application
application.config['SERVER_NAME'] = 'example.org'
with application.app_context():
    url_for('yourblueprint.yourpage')

Or you can redefine your application and register the wanted blueprint.

from flask import Flask, url_for
from whereyoudefineyourblueprint import myblueprint
application = Flask(__name__)
application.config['SERVER_NAME'] = 'example.org'
application.register_blueprint(myblueprint)
with application.app_context():
    url_for('myblueprint.mypage')

We can also imagine different ways to do it without the application, but I don't see any adequate / proper solution.

Despite everything, I will still suggest this dirty solution.

Let's say you have the following blueprint with the following routes inside routes.py.

from flask import Blueprint

frontend = Blueprint('frontend', __name__)

@frontend.route('/mypage')
def mypage():
    return 'Hello'

@frontend.route('/some/other/page')
def someotherpage():
    return 'Hi'

@frontend.route('/wow/<a>')
def wow(a):
    return f'Hi {a}'

You could use the library inspect to get the source code and then parse it in order to build the URL.

import inspect
import re

BASE_URL = "https://example.org"

class FailToGetUrlException(Exception):
    pass

def get_url(function, complete_url=True):
    source = inspect.getsource(function)
    lines = source.split("\n")

    for line in lines:
        r = re.match(r'^\@[a-zA-Z]+\.route\((["\'])([^\'"]+)\1', line)
        if r:
            if complete_url:
                return BASE_URL + r.group(2)
            else:
                return r.group(2)
    
    raise FailToGetUrlException


from routes import *
print(get_url(mypage))
print(get_url(someotherpage))
print(get_url(wow).replace('<a>', '456'))

Output:

https://example.org/mypage
https://example.org/some/other/page
https://example.org/wow/456
Related