web-dev-qa-db-fra.com

Quel est le but des piles de contexte de Flask?

Cela fait quelque temps que j'utilise le contexte requête/application sans bien comprendre son fonctionnement ni la raison pour laquelle il a été conçu. Quel est le but de la "pile" en ce qui concerne le contexte de la demande ou de l'application? S'agit-il de deux piles distinctes, ou s'agit-il d'une pile? Le contexte de la demande est-il placé sur une pile ou s'agit-il d'une pile? Suis-je capable de pousser/sauter plusieurs contextes les uns sur les autres? Si oui, pourquoi voudrais-je faire cela?

Désolé pour toutes les questions, mais je suis toujours perplexe après avoir lu la documentation de Request Context et Application Context.

135
Ben Davis

Plusieurs applications

Le contexte de l'application (et son objectif) prête effectivement à confusion jusqu'à ce que vous réalisiez que Flask peut avoir plusieurs applications. Imaginez la situation dans laquelle vous voulez avoir un seul WSGI Python = L’interprète exécute plusieurs Flask. Nous ne parlons pas de plans ici, nous parlons d’applications complètement différentes Flask Flask.

Vous pouvez configurer ceci de la même manière que exemple de la section de documentation de Flask sur "Distribution de l'application" :

from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend

application = DispatcherMiddleware(frontend, {
    '/backend':     backend
})

Notez qu'il y a deux applications Flask complètement différentes en cours de création "frontend" et "backend". En d'autres termes, le constructeur de l'application Flask(...) a été appelé deux fois, créant ainsi deux instances. d'une application Flask.

Les contextes

Lorsque vous travaillez avec Flask, vous finissez souvent par utiliser des variables globales pour accéder à diverses fonctionnalités. Par exemple, vous avez probablement un code qui lit ...

from flask import request

Ensuite, lors d’une vue, vous pouvez utiliser request pour accéder aux informations de la requête en cours. De toute évidence, request n'est pas une variable globale normale; en réalité, il s'agit d'une valeur de contexte local . En d'autres termes, il y a une certaine magie dans les coulisses qui dit "quand j'appelle request.path, Obtenez l'attribut path de l'objet request de la demande CURRENT". Deux requêtes différentes auront des résultats différents pour request.path.

En fait, même si vous exécutez Flask avec plusieurs threads, Flask est suffisamment intelligent pour garder les objets de requête isolés. Ce faisant, il devient possible pour deux les threads, chacun traitant une requête différente, à appeler simultanément request.path et à obtenir les informations correctes pour leurs requêtes respectives.

Mettre ensemble

Donc, nous avons déjà vu que Flask peut gérer plusieurs applications dans le même interpréteur, et aussi en raison de la façon dont Flask vous permet d'utiliser le "contexte" globals "locaux" il doit exister un mécanisme permettant de déterminer ce qu'est la demande "actuelle" (afin de faire des choses telles que request.path) .

En réunissant ces idées, il devrait également être logique que Flask doit avoir un moyen de déterminer l’application "actuelle"!

Vous avez probablement aussi un code similaire au suivant:

from flask import url_for

Comme notre exemple request, la fonction url_for A une logique qui dépend de l'environnement actuel. Dans ce cas, toutefois, il est clair que la logique dépend fortement de l'application considérée comme "actuelle". Dans l'exemple frontend/backend illustré ci-dessus, les applications "frontend" et "backend" peuvent toutes les deux avoir une route "/ login", et donc url_for('/login') devrait renvoyer quelque chose de différent selon que la vue traite la demande. pour l'application frontend ou backend.

Pour répondre à vos questions...

Quel est le but de la "pile" en ce qui concerne le contexte de la demande ou de l'application?

À partir des documents de contexte de demande:

Étant donné que le contexte de la demande est géré en interne sous forme de pile, vous pouvez appuyer et décompresser plusieurs fois. C'est très pratique pour implémenter des choses comme les redirections internes.

En d'autres termes, même si vous avez généralement 0 ou 1 élément sur cette pile de requêtes "en cours" ou d'applications "en cours", il est possible que vous en ayez plus.

L'exemple donné est celui où votre demande renverrait les résultats d'une "redirection interne". Supposons qu'un utilisateur demande A, mais que vous souhaitiez le retourner à l'utilisateur B. Dans la plupart des cas, vous envoyez une redirection à l'utilisateur et pointez l'utilisateur sur la ressource B, ce qui signifie que l'utilisateur exécutera une seconde demande pour extraire B. A Une manière légèrement différente de gérer cela serait de faire une redirection interne, ce qui signifie que pendant le traitement de A, Flask se fera une nouvelle demande pour la ressource B et utilisera les résultats de cette seconde demande. en tant que résultat de la demande initiale de l'utilisateur.

S'agit-il de deux piles distinctes, ou s'agit-il d'une pile?

Ils sont deux piles séparées . Cependant, il s'agit d'un détail de mise en œuvre. Ce qui est plus important, ce n’est pas tant qu’il existe une pile, mais bien le fait qu’à tout moment, vous pouvez obtenir l’application ou la requête "actuelle" (en haut de la pile).

Le contexte de la demande est-il placé sur une pile ou s'agit-il d'une pile?

Un "contexte de demande" est l'un des éléments de la "pile de contextes de demande". De même avec le "contexte d'application" et la "pile de contexte d'application".

Suis-je capable de pousser/sauter plusieurs contextes les uns sur les autres? Si oui, pourquoi voudrais-je faire cela?

En règle générale, dans une application Flask, vous ne le feriez pas. Par exemple, vous souhaiterez peut-être utiliser une redirection interne (décrite ci-dessus). Même dans ce cas, cependant, vous termineriez probablement up ayant Flask gérer une nouvelle demande, et ainsi Flask ferait tout ce que vous souhaitiez.

Cependant, dans certains cas, vous voudriez manipuler vous-même la pile.

Exécution de code en dehors d'une demande

Un problème typique des gens est qu’ils utilisent l’extension Flask-SQLAlchemy pour configurer une base de données SQL et une définition de modèle à l’aide d’un code similaire à celui présenté ci-dessous ...

app = Flask(__name__)
db = SQLAlchemy() # Initialize the Flask-SQLAlchemy extension object
db.init_app(app)

Ensuite, ils utilisent les valeurs app et db dans un script devant être exécuté à partir du shell. Par exemple, un script "setup_tables.py" ...

from myapp import app, db

# Set up models
db.create_all()

Dans ce cas, l'extension Flask-SQLAlchemy est au courant de l'application app, mais lors de create_all(), une erreur se plaindre en se plaignant de l'absence de contexte d'application. Cette erreur est justifiée. vous n'avez jamais dit à Flask quelle application il devrait traiter lors de l'exécution de la méthode create_all.

Vous vous demandez peut-être pourquoi vous n'avez pas besoin de cet appel with app.app_context() lorsque vous exécutez des fonctions similaires dans vos vues. La raison en est que Flask gère déjà la gestion du contexte de l'application lorsque vous gérez des demandes Web réelles. Le problème ne survient vraiment qu'en dehors de ces fonctions d'affichage (ou de tels rappels), comme lorsque vous utilisez vos modèles dans un script unique.

La solution est de pousser le contexte de l'application vous-même, ce qui peut être fait en faisant ...

from myapp import app, db

# Set up models
with app.app_context():
    db.create_all()

Cela poussera un nouveau contexte d'application (en utilisant l'application de app, rappelez-vous qu'il pourrait y avoir plus d'une application).

Essai

Un autre cas où vous voudriez manipuler la pile est pour le test. Vous pouvez créer un test unitaire qui gère une demande et vérifier les résultats:

import unittest
from flask import request

class MyTest(unittest.TestCase):
    def test_thing(self):
        with app.test_request_context('/?next=http://example.com/') as ctx:
            # You can now view attributes on request context stack by using `request`.

        # Now the request context stack is empty
207
Mark Hildreth

Les réponses précédentes donnent déjà un bon aperçu de ce qui se passe à l'arrière-plan de Flask lors d'une demande. Si vous ne l'avez pas encore lu, je recommande la réponse de @ MarkHildreth avant de lire ceci. En bref, un nouveau contexte (thread) est créé pour chaque requête http, raison pour laquelle il est nécessaire de disposer d'une fonctionnalité thread Local qui autorise des objets tels que request et g être accessible globalement à travers les threads, tout en conservant leur contexte spécifique à la requête.En outre, lors du traitement d'une requête http Flask peut émuler des requêtes supplémentaires de l'intérieur, d'où la nécessité de stocker leur contexte respectif sur une pile. De plus, Flask permet à plusieurs applications wsgi de s’exécuter au sein d’un même processus, et plusieurs peuvent être appelées à agir pendant une requête (chaque requête crée un nouveau contexte d’application), d’où la besoin d'une pile de contexte pour les applications: c'est un résumé de ce qui a été couvert dans les réponses précédentes.

Mon objectif est maintenant de compléter notre compréhension actuelle en expliquant comment ​​Flask et Werkzeug font ce qu'ils font avec ces sections locales de contexte. J'ai simplifié le code pour améliorer la compréhension de ses logique, mais si vous obtenez cela, vous devriez pouvoir facilement saisir la plupart de ce qui se trouve dans la source réelle (werkzeug.local et flask.globals).

Voyons d'abord comment Werkzeug implémente les sections locales de threads.

Local

Lorsqu'une requête http arrive, elle est traitée dans le contexte d'un seul thread. Autre moyen alternatif de générer un nouveau contexte lors d'une requête http, Werkzeug permet également l'utilisation de greenlets (une sorte de "micro-threads" plus légers) à la place des threads normaux. Si vous n'avez pas installé de greenlets, il utilisera plutôt des threads. Chacun de ces threads (ou greenlets) est identifiable par un identifiant unique, que vous pouvez récupérer avec la fonction get_ident() du module. Cette fonction est le point de départ de la magie derrière le fait d'avoir request, current_app, url_for, g et d'autres objets globaux liés au contexte.

try:
    from greenlet import get_ident
except ImportError:
    from thread import get_ident

Maintenant que nous avons notre fonction d’identité, nous pouvons savoir quel fil nous sommes à tout moment et nous pouvons créer ce qu’on appelle un fil Local, un objet contextuel accessible de manière globale, mais quand vous y accédez. les attributs qu'ils résolvent à leur valeur pour ce thread spécifique. par exemple.

# globally
local = Local()

# ...

# on thread 1
local.first_name = 'John'

# ...

# on thread 2
local.first_name = 'Debbie'

Les deux valeurs sont présentes simultanément sur l'objet Local globalement accessible, mais l'accès à local.first_name dans le contexte du fil 1 vous donnera 'John', alors qu'il retournera 'Debbie' sur le fil 2.

Comment est-ce possible? Regardons un code (simplifié):

class Local(object)
    def __init__(self):
        self.storage = {}

    def __getattr__(self, name):
        context_id = get_ident() # we get the current thread's or greenlet's id
        contextual_storage = self.storage.setdefault(context_id, {})
        try:
            return contextual_storage[name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        context_id = get_ident()
        contextual_storage = self.storage.setdefault(context_id, {})
        contextual_storage[name] = value

    def __release_local__(self):
        context_id = get_ident()
        self.storage.pop(context_id, None)

local = Local()

Dans le code ci-dessus, nous pouvons voir que la magie se résume à get_ident(), qui identifie le green ou le fil actuel. Le stockage Local l'utilise alors simplement comme clé pour stocker toutes les données contextuelles du thread actuel.

Vous pouvez avoir plusieurs Local objets par processus et request, g, current_app et d'autres auraient simplement été créés de cette façon. Mais ce n'est pas comme cela que l'on fait Flask dans lequel ce ne sont pas des objets techniquement ​​Local, mais plus précisément LocalProxy objets Qu'est-ce qu'un LocalProxy?

LocalProxy

Un LocalProxy est un objet qui interroge un Local pour trouver un autre objet d’intérêt (c’est-à-dire l’objet auquel il est mandaté). Jetons un coup d'oeil pour comprendre:

class LocalProxy(object):
    def __init__(self, local, name):
        # `local` here is either an actual `Local` object, that can be used
        # to find the object of interest, here identified by `name`, or it's
        # a callable that can resolve to that proxied object
        self.local = local
        # `name` is an identifier that will be passed to the local to find the
        # object of interest.
        self.name = name

    def _get_current_object(self):
        # if `self.local` is truly a `Local` it means that it implements
        # the `__release_local__()` method which, as its name implies, is
        # normally used to release the local. We simply look for it here
        # to identify which is actually a Local and which is rather just
        # a callable:
        if hasattr(self.local, '__release_local__'):
            try:
                return getattr(self.local, self.name)
            except AttributeError:
                raise RuntimeError('no object bound to %s' % self.name)

        # if self.local is not actually a Local it must be a callable that 
        # would resolve to the object of interest.
        return self.local(self.name)

    # Now for the LocalProxy to perform its intended duties i.e. proxying 
    # to an underlying object located somewhere in a Local, we turn all magic
    # methods into proxies for the same methods in the object of interest.
    @property
    def __dict__(self):
        try:
            return self._get_current_object().__dict__
        except RuntimeError:
            raise AttributeError('__dict__')

    def __repr__(self):
        try:
            return repr(self._get_current_object())
        except RuntimeError:
            return '<%s unbound>' % self.__class__.__name__

    def __bool__(self):
        try:
            return bool(self._get_current_object())
        except RuntimeError:
            return False

    # ... etc etc ... 

    def __getattr__(self, name):
        if name == '__members__':
            return dir(self._get_current_object())
        return getattr(self._get_current_object(), name)

    def __setitem__(self, key, value):
        self._get_current_object()[key] = value

    def __delitem__(self, key):
        del self._get_current_object()[key]

    # ... and so on ...

    __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
    __delattr__ = lambda x, n: delattr(x._get_current_object(), n)
    __str__ = lambda x: str(x._get_current_object())
    __lt__ = lambda x, o: x._get_current_object() < o
    __le__ = lambda x, o: x._get_current_object() <= o
    __eq__ = lambda x, o: x._get_current_object() == o

    # ... and so forth ...

Maintenant, pour créer des proxies accessibles globalement, vous feriez

# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')

et maintenant un peu de temps au début d'une requête, vous stockez des objets à l'intérieur du local auxquels les proxys créés précédemment peuvent accéder, quel que soit le fil sur lequel nous sommes.

# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()

L’utilisation de LocalProxy comme objets globalement accessibles au lieu de les rendre Locals _ plus simples est que leur gestion est simplifiée. Vous n'avez besoin que d'un seul objet Local pour créer de nombreux serveurs proxy globalement accessibles. À la fin de la demande, pendant le nettoyage, vous libérez simplement le fichier Local (c’est-à-dire que vous extrayez le context_id de son stockage) sans vous soucier des proxy, ils restent globalement accessibles et diffèrent à celui Local pour trouver son objet d’intérêt pour les requêtes http suivantes.

# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()

Pour simplifier la création d'un LocalProxy lorsque nous avons déjà un Local, Werkzeug implémente la méthode magique Local.__call__() de la manière suivante:

class Local(object):
    # ... 
    # ... all same stuff as before go here ...
    # ... 

    def __call__(self, name):
        return LocalProxy(self, name)

# now you can do
local = Local()
request = local('request')
g = local('g')

Cependant, si vous regardez dans la source Flask source (flask.globals)), vous ne trouvez toujours pas comment request, g, current_app et session sont créés. Comme nous l'avons établi, Flask peut générer plusieurs "fausses" demandes (à partir d'une seule demande http véritable) et, dans le processus, également appliquer plusieurs contextes d'application. Ce n'est pas un cas d'utilisation courant, mais c'est une fonctionnalité du framework: étant donné que ces demandes et applications "simultanées" sont toujours limitées pour s'exécuter avec une seule "cible" à tout moment, il est logique d'utiliser une pile. Chaque fois qu’une nouvelle demande est générée ou qu’une des applications est appelée, ils placent leur contexte en haut de leur pile respective. Flask utilise LocalStack objets À la fin de leur activité, ils sortent le contexte de la pile.

LocalStack

Voici à quoi ressemble un LocalStack (encore une fois, le code est simplifié pour faciliter la compréhension de sa logique).

class LocalStack(object):

    def __init__(self):
        self.local = Local()

    def Push(self, obj):
        """Pushes a new item to the stack"""
        rv = getattr(self.local, 'stack', None)
        if rv is None:
            self.local.stack = rv = []
        rv.append(obj)
        return rv

    def pop(self):
        """Removes the topmost item from the stack, will return the
        old value or `None` if the stack was already empty.
        """
        stack = getattr(self.local, 'stack', None)
        if stack is None:
            return None
        Elif len(stack) == 1:
            release_local(self.local) # this simply releases the local
            return stack[-1]
        else:
            return stack.pop()

    @property
    def top(self):
        """The topmost item on the stack.  If the stack is empty,
        `None` is returned.
        """
        try:
            return self.local.stack[-1]
        except (AttributeError, IndexError):
            return None

Notez ci-dessus qu'un LocalStack est une pile stockée dans un local, pas un groupe de locaux stockés dans une pile. Cela implique que, même si la pile est globalement accessible, elle est différente dans chaque thread.

Le flacon n'a pas ses objets request, current_app, g, et session résolus directement en LocalStack, utilise plutôt les objets LocalProxy qui encapsulent une fonction de recherche (au lieu d'un objet Local) qui recherchent l'objet sous-jacent à partir de LocalStack:

_request_ctx_stack = LocalStack()
def _find_request():
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of request context')
    return top.request
request = LocalProxy(_find_request)

def _find_session():
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of request context')
    return top.session
session = LocalProxy(_find_session)

_app_ctx_stack = LocalStack()
def _find_g():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of application context')
    return top.g
g = LocalProxy(_find_g)

def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of application context')
    return top.app
current_app = LocalProxy(_find_app)

Tous ces éléments sont déclarés au démarrage de l'application, mais ne sont résolus en rien tant qu'un contexte de demande ou un contexte d'application n'est pas poussé vers leur pile respective.

Si vous êtes curieux de voir comment un contexte est réellement inséré dans la pile (et par la suite sauté), regardez dans flask.app.Flask.wsgi_app() qui est le point d'entrée de l'application wsgi (c'est-à-dire ce que le serveur Web appelle et passez l'environnement http à lorsqu'une requête arrive), et suivez la création de l'objet RequestContext tout au long de sa suivante Push() dans _request_ctx_stack. Une fois placé en haut de la pile, il est accessible via _request_ctx_stack.top. Voici un code abrégé pour illustrer le flux:

Donc, vous démarrez une application et la mettez à la disposition du serveur WSGI ...

app = Flask(*config, **kwconfig)

# ...

Plus tard, une requête http arrive et le serveur WSGI appelle l'application avec les paramètres habituels ...

app(environ, start_response) # aka app.__call__(environ, start_response)

C'est à peu près ce qui se passe dans l'application ...

def Flask(object):

    # ...

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)

    def wsgi_app(self, environ, start_response):
        ctx = RequestContext(self, environ)
        ctx.Push()
        try:
            # process the request here
            # raise error if any
            # return Response
        finally:
            ctx.pop()

    # ...

et c'est à peu près ce qui se passe avec RequestContext ...

class RequestContext(object):

    def __init__(self, app, environ, request=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = app.create_url_adapter(self.request)
        self.session = self.app.open_session(self.request)
        if self.session is None:
            self.session = self.app.make_null_session()
        self.flashes = None

    def Push(self):
        _request_ctx_stack.Push(self)

    def pop(self):
        _request_ctx_stack.pop()

Supposons qu'une requête ait fini de s'initialiser, la recherche de request.path à partir de l'une de vos fonctions d'affichage se présentera donc comme suit:

  • partir de l'objet LocalProxy globalement accessible request.
  • pour trouver l'objet d'intérêt sous-jacent (l'objet auquel il est mandataire), il appelle sa fonction de recherche _find_request() (la fonction qu'il a enregistrée sous le nom self.local).
  • cette fonction interroge l'objet LocalStack_request_ctx_stack pour connaître le contexte supérieur de la pile.
  • pour trouver le contexte de haut, l'objet LocalStack interroge d'abord son attribut Local intérieur (self.local) pour la propriété stack qui y était précédemment stockée.
  • du stack il obtient le contexte supérieur
  • et top.request est donc résolu en tant qu'objet d'intérêt sous-jacent.
  • à partir de cet objet, nous obtenons l'attribut path

Nous avons donc vu comment Local, LocalProxy et LocalStack travaillaient, réfléchissons maintenant un instant aux implications et aux nuances de la récupération du path de:

  • un objet request qui serait un simple objet accessible globalement.
  • un objet request qui serait un local.
  • un objet request stocké comme attribut d'un local.
  • un objet request qui est un proxy pour un objet stocké dans un local.
  • un objet request stocké dans une pile, elle-même stockée dans un local.
  • un objet request qui est un proxy pour un objet sur une pile stockée dans un local. <- c'est ce que fait Flask).
37
Michael Ekoka

Petit ajout @ La réponse de Mark Hildreth .

La pile de contexte ressemble à {thread.get_ident(): []}, où [] appelé "pile" car utilisé uniquement append (Push), pop et [-1] (__getitem__(-1)) opérations. Ainsi, la pile de contexte conservera les données réelles du fil ou du fil de la greenlet.

current_app, g, request, session et etc est LocalProxy objet qui vient de remplacer les méthodes spéciales __getattr__ , __getitem__, __call__, __eq__, etc., et renvoient une valeur depuis la pile de contexte ([-1]) par nom d'argument (current_app, request par exemple). LocalProxy devait importer ces objets une fois et ils ne manqueraient pas l'actualité. Il est donc préférable de simplement importer request où que vous soyez dans le code, jouez avec l'envoi de l'argument de requête à vos fonctions et méthodes. Vous pouvez facilement écrire vos propres extensions avec cela, mais n'oubliez pas qu'un usage frivole peut rendre le code plus difficile à comprendre.

Prenez le temps de comprendre https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/local.py .

Alors, comment peuplé les deux piles? Sur demande Flask:

  1. créer request_context par environnement (init map_adapter, chemin de correspondance)
  2. entrer ou pousser cette demande:
    1. effacer précédent request_context
    2. créer app_context s'il est manquant et poussé dans la pile de contexte d'application
    3. cette demande poussée à demander la pile de contexte
    4. init session si ça manquait
  3. demande d'expédition
  4. effacer la demande et la sortir de la pile
13
tbicr

Prenons un exemple. Supposons que vous souhaitiez définir un contexte utilisateur (avec flask construct of Local et LocalProxy).

Définir une classe d'utilisateurs:

class User(object):
    def __init__(self):
        self.userid = None

définir une fonction pour récupérer l'objet utilisateur dans le thread ou la greenlet en cours

def get_user(_local):
    try:
        # get user object in current thread or greenlet
        return _local.user
    except AttributeError:
        # if user object is not set in current thread ,set empty user object 
       _local.user = User()
    return _local.user

Définissez maintenant un LocalProxy

usercontext = LocalProxy(partial(get_user, Local()))

Maintenant, obtenir l'ID utilisateur de l'utilisateur dans le fil actuel usercontext.userid

explication:

1.Local a un dicton d'identité et d'objet, identity est un threadid ou un id de greenlet, dans cet exemple, _local.user = utilisateur () est équivalent à _local .___ stockage __ [id du thread actuel] ["utilisateur"] = utilisateur ()

  1. LocalProxy délégués opération pour encapsuler l'objet Local ou vous pouvez fournir une fonction qui renvoie l'objet cible. Dans l'exemple ci-dessus, la fonction get_user fournit l'objet utilisateur actuel à LocalProxy. Lorsque vous demandez l'ID utilisateur de l'utilisateur actuel par usercontext.userid, la fonction __getattr__ de LocalProxy appelle d'abord get_user pour obtenir l'objet User (utilisateur), puis appelle getattr (utilisateur, "IDutilisateur"). Pour définir l'ID utilisateur sur l'utilisateur (dans le thread ou la greenlet en cours), il vous suffit de le faire: usercontext.userid = "user_123"
2
Ratn Deo--Dev