How to call ...users.operations.create_user "silient'ly"?

Hello,

We are creating a script to import users. When we call …users.operations.create_user it raises an error due to the call to the signal @signals.users.email_added.connect being invoked and triggering: _convert_email_principals … but since silent=False the call to flash on line 72 is throwing an error (as flash can’t get called from a batch script).

Is there any way to call users.operations.create_user silent’ly so that it doesn’t throw errors?

Thanks!
Dustin.

When asking something about errors, including the traceback is generally useful, because that way we don’t have to dig out the relevant parts from the code…

You can simply wrap your script in a test request context which should avoid errors related to accessign the session:

with current_app.test_request_context(base_url=config.BASE_URL):
    ...

PS: Why do you need to bulk-create/import users? Usually they are created on demand, in particular when the instance is connected to LDAP or a similar service that has a searchable list of user.

So we have a script that goes like this (there’re some missing parts and vars but that’s not important)

@click.command()
def cli():
    with make_app().app_context():
        user_details_path = 'users.json'
        with open(user_details_path, 'r') as file:
            users_data = json.load(file)
        for user_details in users_data:
            email = user_details.get('email', '')
            identity = Identity(provider='indico', identifier=username, password=password)
            
            user = create_user(email, {'first_name': user_details.get('firstname', ''),
                                    'last_name': user_details.get('lastname', ''),
                                    'affiliation': user_details.get('affiliation')}, identity, from_moderation=False)
            
if __name__ == '__main__':
    cli()

when that run we get this error:

File "//dev/indico/custom/scripts/test2.py", line 36, in create_new_user
    user = create_user(email, {'first_name': user_details.get('firstname', ''),
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/dev/indico/src/indico/modules/users/operations.py", line 60, in create_user
    signals.users.registered.send(user, from_moderation=from_moderation, identity=identity)
  File "/dev/indico/env/lib/python3.12/site-packages/blinker/base.py", line 307, in send
    result = receiver(sender, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/dev/indico/src/indico/modules/events/contributions/__init__.py", line 51, in _convert_email_principals
    flash(ngettext('You have been granted manager/submission privileges for a contribution.',
  File "/dev/indico/env/lib/python3.12/site-packages/flask/helpers.py", line 321, in flash
    flashes = session.get("_flashes", [])
              ^^^^^^^^^^^
  File "/dev/indico/env/lib/python3.12/site-packages/werkzeug/local.py", line 311, in __get__
    obj = instance._get_current_object()
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/dev/indico/env/lib/python3.12/site-packages/werkzeug/local.py", line 508, in _get_current_object
    raise RuntimeError(unbound_message) from None
RuntimeError: Working outside of request context.

This typically means that you attempted to use functionality that needed
an active HTTP request. Consult the documentation on testing for
information about how to avoid this problem.

It happens because the flask flash function can’t execute from a command line scripts.
It is triggered when we are creating accounts for users that are speakers or chairs of Contributions or Events but don’t have an account. So from a line like this:

the parameter list to the “_convert_email_principals” function includes “silent=False” but I see no where to pass it along from our script.

IIRC silent=True is only used for background syncs of user data (not done in the core).

I think using a request context in your script is the best option, the only alternatives I see would be forcing silent to True when creating a user outside a request context (also fine for me if you want to send a PR for that) or checking in the code above whether there’s a request context and flashing only in that case (feels rather ugly),

We do have the code wrapped in a
with make_app().app_context():

I can get the script to work if either I change the parameter to silent=True or if wrap the call from our script in an ugly try/catch and ignore the specific error, something like this:

   except RuntimeError as e:
        error = e.args
        if not (error and 'Working outside of request context' in error[0]):
            db.session.rollback()

The request context is different from the app context. Try this:

with make_app().app_context():
    with current_app.test_request_context(base_url=config.BASE_URL):
        ...

(you can also pass localhost instead of the configured BASE_URL, shouldn’t matter in this case)

Perfect! That did work. We’ll try it more tomorrow.
Thanks for you help!
Cheers,
Luis