locust 2.20.1.dev28__py3-none-any.whl → 2.20.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. locust/__init__.py +7 -7
  2. locust/_version.py +2 -2
  3. locust/argument_parser.py +35 -18
  4. locust/clients.py +7 -6
  5. locust/contrib/fasthttp.py +42 -37
  6. locust/debug.py +14 -8
  7. locust/dispatch.py +25 -25
  8. locust/env.py +45 -37
  9. locust/event.py +13 -10
  10. locust/exception.py +0 -9
  11. locust/html.py +9 -8
  12. locust/input_events.py +7 -5
  13. locust/main.py +95 -47
  14. locust/rpc/protocol.py +4 -2
  15. locust/rpc/zmqrpc.py +6 -4
  16. locust/runners.py +69 -76
  17. locust/shape.py +7 -4
  18. locust/stats.py +73 -71
  19. locust/templates/index.html +8 -23
  20. locust/test/mock_logging.py +7 -5
  21. locust/test/test_debugging.py +4 -4
  22. locust/test/test_dispatch.py +10 -9
  23. locust/test/test_env.py +30 -1
  24. locust/test/test_fasthttp.py +9 -8
  25. locust/test/test_http.py +4 -3
  26. locust/test/test_interruptable_task.py +4 -4
  27. locust/test/test_load_locustfile.py +6 -5
  28. locust/test/test_locust_class.py +11 -5
  29. locust/test/test_log.py +5 -4
  30. locust/test/test_main.py +37 -7
  31. locust/test/test_parser.py +11 -15
  32. locust/test/test_runners.py +22 -21
  33. locust/test/test_sequential_taskset.py +3 -2
  34. locust/test/test_stats.py +16 -18
  35. locust/test/test_tags.py +3 -2
  36. locust/test/test_taskratio.py +3 -3
  37. locust/test/test_users.py +3 -3
  38. locust/test/test_util.py +3 -2
  39. locust/test/test_wait_time.py +3 -3
  40. locust/test/test_web.py +142 -27
  41. locust/test/test_zmqrpc.py +5 -3
  42. locust/test/testcases.py +7 -7
  43. locust/test/util.py +6 -7
  44. locust/user/__init__.py +1 -1
  45. locust/user/inspectuser.py +6 -5
  46. locust/user/sequential_taskset.py +3 -1
  47. locust/user/task.py +22 -27
  48. locust/user/users.py +17 -7
  49. locust/util/deprecation.py +0 -1
  50. locust/util/exception_handler.py +1 -1
  51. locust/util/load_locustfile.py +4 -2
  52. locust/web.py +102 -53
  53. locust/webui/dist/assets/auth-5e21717c.js.map +1 -0
  54. locust/webui/dist/assets/{index-01afe4fa.js → index-a83a5dd9.js} +85 -84
  55. locust/webui/dist/assets/index-a83a5dd9.js.map +1 -0
  56. locust/webui/dist/auth.html +20 -0
  57. locust/webui/dist/index.html +1 -1
  58. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/METADATA +2 -2
  59. locust-2.20.2.dist-info/RECORD +105 -0
  60. locust-2.20.1.dev28.dist-info/RECORD +0 -102
  61. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/LICENSE +0 -0
  62. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/WHEEL +0 -0
  63. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/entry_points.txt +0 -0
  64. {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,8 @@
1
- from collections import defaultdict
1
+ from __future__ import annotations
2
+
2
3
  import inspect
4
+ from collections import defaultdict
3
5
  from json import dumps
4
- from typing import List, Type, Dict
5
6
 
6
7
  from .task import TaskSet
7
8
  from .users import User
@@ -49,11 +50,11 @@ def _print_task_ratio(x, level=0):
49
50
  _print_task_ratio(v["tasks"], level + 1)
50
51
 
51
52
 
52
- def get_ratio(user_classes: List[Type[User]], user_spawned: Dict[str, int], total: bool) -> Dict[str, Dict[str, float]]:
53
+ def get_ratio(user_classes: list[type[User]], user_spawned: dict[str, int], total: bool) -> dict[str, dict[str, float]]:
53
54
  user_count = sum(user_spawned.values()) or 1
54
- ratio_percent: Dict[Type[User], float] = {u: user_spawned.get(u.__name__, 0) / user_count for u in user_classes}
55
+ ratio_percent: dict[type[User], float] = {u: user_spawned.get(u.__name__, 0) / user_count for u in user_classes}
55
56
 
56
- task_dict: Dict[str, Dict[str, float]] = {}
57
+ task_dict: dict[str, dict[str, float]] = {}
57
58
  for u, r in ratio_percent.items():
58
59
  d = {"ratio": r}
59
60
  d["tasks"] = _get_task_ratio(u.tasks, total, r)
@@ -1,5 +1,7 @@
1
- import logging
2
1
  from locust.exception import LocustError
2
+
3
+ import logging
4
+
3
5
  from .task import TaskSet, TaskSetMeta
4
6
 
5
7
 
locust/user/task.py CHANGED
@@ -1,4 +1,7 @@
1
1
  from __future__ import annotations
2
+
3
+ from locust.exception import InterruptTaskSet, MissingWaitTimeError, RescheduleTask, RescheduleTaskImmediately, StopUser
4
+
2
5
  import logging
3
6
  import random
4
7
  import traceback
@@ -6,22 +9,17 @@ from time import time
6
9
  from typing import (
7
10
  TYPE_CHECKING,
8
11
  Callable,
9
- List,
10
- TypeVar,
11
- Optional,
12
- Type,
13
- overload,
14
- Dict,
15
- Set,
16
12
  Protocol,
13
+ Type,
14
+ TypeVar,
17
15
  final,
16
+ overload,
18
17
  runtime_checkable,
19
18
  )
19
+
20
20
  import gevent
21
21
  from gevent import GreenletExit
22
22
 
23
- from locust.exception import InterruptTaskSet, RescheduleTask, RescheduleTaskImmediately, StopUser, MissingWaitTimeError
24
-
25
23
  if TYPE_CHECKING:
26
24
  from locust import User
27
25
 
@@ -34,7 +32,7 @@ LOCUST_STATE_RUNNING, LOCUST_STATE_WAITING, LOCUST_STATE_STOPPING = ["running",
34
32
 
35
33
  @runtime_checkable
36
34
  class TaskHolder(Protocol[TaskT]):
37
- tasks: List[TaskT]
35
+ tasks: list[TaskT]
38
36
 
39
37
 
40
38
  @overload
@@ -170,10 +168,10 @@ def get_tasks_from_base_classes(bases, class_dict):
170
168
 
171
169
 
172
170
  def filter_tasks_by_tags(
173
- task_holder: Type[TaskHolder],
174
- tags: Optional[Set[str]] = None,
175
- exclude_tags: Optional[Set[str]] = None,
176
- checked: Optional[Dict[TaskT, bool]] = None,
171
+ task_holder: type[TaskHolder],
172
+ tags: set[str] | None = None,
173
+ exclude_tags: set[str] | None = None,
174
+ checked: dict[TaskT, bool] | None = None,
177
175
  ):
178
176
  """
179
177
  Function used by Environment to recursively remove any tasks/TaskSets from a TaskSet/User that
@@ -238,7 +236,7 @@ class TaskSet(metaclass=TaskSetMeta):
238
236
  will then continue in the first TaskSet).
239
237
  """
240
238
 
241
- tasks: List[TaskSet | Callable] = []
239
+ tasks: list[TaskSet | Callable] = []
242
240
  """
243
241
  Collection of python callables and/or TaskSet classes that the User(s) will run.
244
242
 
@@ -253,7 +251,7 @@ class TaskSet(metaclass=TaskSetMeta):
253
251
  tasks = {ThreadPage:15, write_post:1}
254
252
  """
255
253
 
256
- min_wait: Optional[float] = None
254
+ min_wait: float | None = None
257
255
  """
258
256
  Deprecated: Use wait_time instead.
259
257
  Minimum waiting time between the execution of user tasks. Can be used to override
@@ -261,7 +259,7 @@ class TaskSet(metaclass=TaskSetMeta):
261
259
  TaskSet.
262
260
  """
263
261
 
264
- max_wait: Optional[float] = None
262
+ max_wait: float | None = None
265
263
  """
266
264
  Deprecated: Use wait_time instead.
267
265
  Maximum waiting time between the execution of user tasks. Can be used to override
@@ -277,11 +275,11 @@ class TaskSet(metaclass=TaskSetMeta):
277
275
  if not set on the TaskSet.
278
276
  """
279
277
 
280
- _user: "User"
281
- _parent: "User"
278
+ _user: User
279
+ _parent: User
282
280
 
283
- def __init__(self, parent: "User") -> None:
284
- self._task_queue: List[Callable] = []
281
+ def __init__(self, parent: User) -> None:
282
+ self._task_queue: list[Callable] = []
285
283
  self._time_start = time()
286
284
 
287
285
  if isinstance(parent, TaskSet):
@@ -298,9 +296,10 @@ class TaskSet(metaclass=TaskSetMeta):
298
296
  self.max_wait = self.user.max_wait
299
297
  if not self.wait_function:
300
298
  self.wait_function = self.user.wait_function
299
+ self._cp_last_run = time() # used by constant_pacing wait_time
301
300
 
302
301
  @property
303
- def user(self) -> "User":
302
+ def user(self) -> User:
304
303
  """:py:class:`User <locust.User>` instance that this TaskSet was created by"""
305
304
  return self._user
306
305
 
@@ -426,11 +425,7 @@ class TaskSet(metaclass=TaskSetMeta):
426
425
  return random.randint(self.min_wait, self.max_wait) / 1000.0
427
426
  else:
428
427
  raise MissingWaitTimeError(
429
- "You must define a wait_time method on either the %s or %s class"
430
- % (
431
- type(self.user).__name__,
432
- type(self).__name__,
433
- )
428
+ "You must define a wait_time method on either the {type(self.user).__name__} or {type(self).__name__} class"
434
429
  )
435
430
 
436
431
  def wait(self):
locust/user/users.py CHANGED
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
- from typing import Callable, Dict, List, Optional, final
3
2
 
3
+ import logging
4
4
  import time
5
+ import traceback
6
+ from typing import Callable, final
7
+
5
8
  from gevent import GreenletExit, greenlet
6
9
  from gevent.pool import Group
7
10
  from urllib3 import PoolManager
8
- import logging
9
- import traceback
10
11
 
11
12
  logger = logging.getLogger(__name__)
12
13
  from locust.clients import HttpSession
@@ -59,7 +60,7 @@ class User(metaclass=UserMeta):
59
60
  :py:class:`HttpUser <locust.HttpUser>` class.
60
61
  """
61
62
 
62
- host: Optional[str] = None
63
+ host: str | None = None
63
64
  """Base hostname to swarm. i.e: http://127.0.0.1:1234"""
64
65
 
65
66
  min_wait = None
@@ -89,7 +90,7 @@ class User(metaclass=UserMeta):
89
90
  Method that returns the time between the execution of locust tasks in milliseconds
90
91
  """
91
92
 
92
- tasks: List[TaskSet | Callable] = []
93
+ tasks: list[TaskSet | Callable] = []
93
94
  """
94
95
  Collection of python callables and/or TaskSet classes that the Locust user(s) will run.
95
96
 
@@ -216,13 +217,22 @@ class User(metaclass=UserMeta):
216
217
  def greenlet(self):
217
218
  return self._greenlet
218
219
 
219
- def context(self) -> Dict:
220
+ def context(self) -> dict:
220
221
  """
221
222
  Adds the returned value (a dict) to the context for :ref:`request event <request_context>`.
222
223
  Override this in your User class to customize the context.
223
224
  """
224
225
  return {}
225
226
 
227
+ @classmethod
228
+ def json(cls):
229
+ return {
230
+ "host": cls.host,
231
+ "weight": cls.weight,
232
+ "fixed_count": cls.fixed_count,
233
+ "tasks": [task.__name__ for task in cls.tasks],
234
+ }
235
+
226
236
  @classmethod
227
237
  def fullname(cls) -> str:
228
238
  """Fully qualified name of the user class, e.g. my_package.my_module.MyUserClass"""
@@ -244,7 +254,7 @@ class HttpUser(User):
244
254
  abstract = True
245
255
  """If abstract is True, the class is meant to be subclassed, and users will not choose this locust during a test"""
246
256
 
247
- pool_manager: Optional[PoolManager] = None
257
+ pool_manager: PoolManager | None = None
248
258
  """Connection pool manager to use. If not given, a new manager is created per single user."""
249
259
 
250
260
  def __init__(self, *args, **kwargs):
@@ -1,6 +1,5 @@
1
1
  import warnings
2
2
 
3
-
4
3
  # Show deprecation warnings
5
4
  warnings.filterwarnings("always", category=DeprecationWarning, module="locust")
6
5
 
@@ -1,5 +1,5 @@
1
- import time
2
1
  import logging
2
+ import time
3
3
 
4
4
  logger = logging.getLogger(__name__)
5
5
 
@@ -1,8 +1,10 @@
1
+ from __future__ import annotations
2
+
1
3
  import importlib
2
4
  import inspect
3
5
  import os
4
6
  import sys
5
- from typing import Dict, List, Optional, Tuple
7
+
6
8
  from ..shape import LoadTestShape
7
9
  from ..user import User
8
10
 
@@ -21,7 +23,7 @@ def is_shape_class(item):
21
23
  return bool(inspect.isclass(item) and issubclass(item, LoadTestShape) and not getattr(item, "abstract", True))
22
24
 
23
25
 
24
- def load_locustfile(path) -> Tuple[Optional[str], Dict[str, User], List[LoadTestShape]]:
26
+ def load_locustfile(path) -> tuple[str | None, dict[str, User], list[LoadTestShape]]:
25
27
  """
26
28
  Import given locustfile path and return (docstring, callables).
27
29
 
locust/web.py CHANGED
@@ -1,32 +1,44 @@
1
1
  from __future__ import annotations
2
+
2
3
  import csv
4
+ import json
3
5
  import logging
4
6
  import os.path
5
7
  from functools import wraps
6
-
7
8
  from html import escape
8
9
  from io import StringIO
9
- from json import dumps
10
10
  from itertools import chain
11
+ from json import dumps
11
12
  from time import time
12
- from typing import TYPE_CHECKING, Optional, Any, Dict, List
13
+ from typing import TYPE_CHECKING, Any
13
14
 
14
15
  import gevent
15
- from flask import Flask, make_response, jsonify, render_template, request, send_file, Response, send_from_directory
16
- from flask_basicauth import BasicAuth
16
+ from flask import (
17
+ Flask,
18
+ Response,
19
+ jsonify,
20
+ make_response,
21
+ redirect,
22
+ render_template,
23
+ request,
24
+ send_file,
25
+ send_from_directory,
26
+ url_for,
27
+ )
28
+ from flask_cors import CORS
29
+ from flask_login import LoginManager, login_required
17
30
  from gevent import pywsgi
18
31
 
19
- from .exception import AuthCredentialsError
20
- from .runners import MasterRunner, STATE_RUNNING, STATE_MISSING
32
+ from . import __version__ as version
33
+ from . import argument_parser
34
+ from . import stats as stats_module
35
+ from .html import get_html_report
21
36
  from .log import greenlet_exception_logger
22
- from .stats import StatsCSVFileWriter, StatsErrorDict, sort_stats
23
- from . import stats as stats_module, __version__ as version, argument_parser
24
- from .stats import StatsCSV
37
+ from .runners import STATE_MISSING, STATE_RUNNING, MasterRunner
38
+ from .stats import StatsCSV, StatsCSVFileWriter, StatsErrorDict, sort_stats
25
39
  from .user.inspectuser import get_ratio
26
40
  from .util.cache import memoize
27
41
  from .util.timespan import parse_timespan
28
- from .html import get_html_report
29
- from flask_cors import CORS
30
42
 
31
43
  if TYPE_CHECKING:
32
44
  from .env import Environment
@@ -45,7 +57,7 @@ class WebUI:
45
57
  in :attr:`environment.stats <locust.env.Environment.stats>`
46
58
  """
47
59
 
48
- app: Optional[Flask] = None
60
+ app: Flask | None = None
49
61
  """
50
62
  Reference to the :class:`flask.Flask` app. Can be used to add additional web routes and customize
51
63
  the Flask app in other various ways. Example::
@@ -57,27 +69,30 @@ class WebUI:
57
69
  return "your IP is: %s" % request.remote_addr
58
70
  """
59
71
 
60
- greenlet: Optional[gevent.Greenlet] = None
72
+ greenlet: gevent.Greenlet | None = None
61
73
  """
62
74
  Greenlet of the running web server
63
75
  """
64
76
 
65
- server: Optional[pywsgi.WSGIServer] = None
77
+ server: pywsgi.WSGIServer | None = None
66
78
  """Reference to the :class:`pyqsgi.WSGIServer` instance"""
67
79
 
68
- template_args: Dict[str, Any]
80
+ template_args: dict[str, Any]
69
81
  """Arguments used to render index.html for the web UI. Must be used with custom templates
70
82
  extending index.html."""
71
83
 
84
+ auth_args: dict[str, Any]
85
+ """Arguments used to render auth.html for the web UI auth page. Must be used when configuring auth"""
86
+
72
87
  def __init__(
73
88
  self,
74
- environment: "Environment",
89
+ environment: Environment,
75
90
  host: str,
76
91
  port: int,
77
- auth_credentials: Optional[str] = None,
78
- tls_cert: Optional[str] = None,
79
- tls_key: Optional[str] = None,
80
- stats_csv_writer: Optional[StatsCSV] = None,
92
+ web_login: bool = False,
93
+ tls_cert: str | None = None,
94
+ tls_key: str | None = None,
95
+ stats_csv_writer: StatsCSV | None = None,
81
96
  delayed_start=False,
82
97
  userclass_picker_is_active=False,
83
98
  modern_ui=False,
@@ -89,8 +104,7 @@ class WebUI:
89
104
  environment: Reference to the current Locust Environment
90
105
  host: Host/interface that the web server should accept connections to
91
106
  port: Port that the web server should listen to
92
- auth_credentials: If provided, it will enable basic auth with all the routes protected by default.
93
- Should be supplied in the format: "user:pass".
107
+ web_login: Enables a login page for the modern UI
94
108
  tls_cert: A path to a TLS certificate
95
109
  tls_key: A path to a TLS private key
96
110
  delayed_start: Whether or not to delay starting web UI until `start()` is called. Delaying web UI start
@@ -105,6 +119,7 @@ class WebUI:
105
119
  self.tls_key = tls_key
106
120
  self.userclass_picker_is_active = userclass_picker_is_active
107
121
  self.modern_ui = modern_ui
122
+ self.web_login = web_login
108
123
  app = Flask(__name__)
109
124
  CORS(app)
110
125
  self.app = app
@@ -113,24 +128,16 @@ class WebUI:
113
128
  root_path = os.path.dirname(os.path.abspath(__file__))
114
129
  app.root_path = root_path
115
130
  self.webui_build_path = os.path.join(root_path, "webui", "dist")
116
- self.app.config["BASIC_AUTH_ENABLED"] = False
117
- self.auth: Optional[BasicAuth] = None
118
- self.greenlet: Optional[gevent.Greenlet] = None
119
- self._swarm_greenlet: Optional[gevent.Greenlet] = None
131
+ self.greenlet: gevent.Greenlet | None = None
132
+ self._swarm_greenlet: gevent.Greenlet | None = None
120
133
  self.template_args = {}
134
+ self.auth_args = {}
135
+
136
+ if self.web_login:
137
+ self.login_manager = LoginManager()
138
+ self.login_manager.init_app(app)
139
+ self.login_manager.login_view = "login"
121
140
 
122
- if auth_credentials is not None:
123
- credentials = auth_credentials.split(":")
124
- if len(credentials) == 2:
125
- self.app.config["BASIC_AUTH_USERNAME"] = credentials[0]
126
- self.app.config["BASIC_AUTH_PASSWORD"] = credentials[1]
127
- self.app.config["BASIC_AUTH_ENABLED"] = True
128
- self.auth = BasicAuth()
129
- self.auth.init_app(self.app)
130
- else:
131
- raise AuthCredentialsError(
132
- "Invalid auth_credentials. It should be a string in the following format: 'user:pass'"
133
- )
134
141
  if environment.runner:
135
142
  self.update_template_args()
136
143
  if not delayed_start:
@@ -365,8 +372,8 @@ class WebUI:
365
372
  @self.auth_required_if_enabled
366
373
  @memoize(timeout=DEFAULT_CACHE_TIME, dynamic_timeout=True)
367
374
  def request_stats() -> Response:
368
- stats: List[Dict[str, Any]] = []
369
- errors: List[StatsErrorDict] = []
375
+ stats: list[dict[str, Any]] = []
376
+ errors: list[StatsErrorDict] = []
370
377
 
371
378
  if environment.runner is None:
372
379
  report = {
@@ -476,9 +483,9 @@ class WebUI:
476
483
 
477
484
  @app.route("/tasks")
478
485
  @self.auth_required_if_enabled
479
- def tasks() -> Dict[str, Dict[str, Dict[str, float]]]:
486
+ def tasks() -> dict[str, dict[str, dict[str, float]]]:
480
487
  runner = self.environment.runner
481
- user_spawned: Dict[str, int]
488
+ user_spawned: dict[str, int]
482
489
  if runner is None:
483
490
  user_spawned = {}
484
491
  else:
@@ -508,6 +515,30 @@ class WebUI:
508
515
 
509
516
  return jsonify({"logs": logs})
510
517
 
518
+ @app.route("/login")
519
+ def login():
520
+ if not self.web_login:
521
+ return redirect(url_for("index"))
522
+
523
+ if self.modern_ui:
524
+ self.set_static_modern_ui()
525
+
526
+ return render_template(
527
+ "auth.html",
528
+ auth_args=self.auth_args,
529
+ )
530
+ else:
531
+ return "Web Auth is only available on the modern web ui. Enable it with the --modern-ui flag"
532
+
533
+ @app.route("/user", methods=["POST"])
534
+ def update_user():
535
+ assert request.method == "POST"
536
+
537
+ user_settings = json.loads(request.data)
538
+ self.environment.update_user_class(user_settings)
539
+
540
+ return {}, 201
541
+
511
542
  def start(self):
512
543
  self.greenlet = gevent.spawn(self.start_server)
513
544
  self.greenlet.link_exception(greenlet_exception_handler)
@@ -529,8 +560,8 @@ class WebUI:
529
560
 
530
561
  def auth_required_if_enabled(self, view_func):
531
562
  """
532
- Decorator that can be used on custom route methods that will turn on Basic Auth
533
- authentication if the ``--web-auth`` flag is used. Example::
563
+ Decorator that can be used on custom route methods that will turn on Flask Login
564
+ authentication if the ``--web-login`` flag is used. Example::
534
565
 
535
566
  @web_ui.app.route("/my_custom_route")
536
567
  @web_ui.auth_required_if_enabled
@@ -540,11 +571,11 @@ class WebUI:
540
571
 
541
572
  @wraps(view_func)
542
573
  def wrapper(*args, **kwargs):
543
- if self.app.config["BASIC_AUTH_ENABLED"]:
544
- if self.auth.authenticate():
545
- return view_func(*args, **kwargs)
546
- else:
547
- return self.auth.challenge()
574
+ if self.web_login:
575
+ try:
576
+ return login_required(view_func)(*args, **kwargs)
577
+ except Exception as e:
578
+ return f"Locust auth exception: {e} See https://docs.locust.io/en/stable/extending-locust.html#authentication for configuring authentication."
548
579
  else:
549
580
  return view_func(*args, **kwargs)
550
581
 
@@ -582,17 +613,32 @@ class WebUI:
582
613
  stats = self.environment.runner.stats
583
614
  extra_options = argument_parser.ui_extra_args_dict()
584
615
 
585
- available_user_classes = (
586
- None if not self.environment.available_user_classes else sorted(self.environment.available_user_classes)
587
- )
616
+ available_user_classes = None
617
+ users = None
618
+ if self.environment.available_user_classes:
619
+ available_user_classes = sorted(self.environment.available_user_classes)
620
+ users = {
621
+ user_class_name: user_class.json()
622
+ for (user_class_name, user_class) in self.environment.available_user_classes.items()
623
+ }
588
624
 
589
625
  available_shape_classes = ["Default"]
590
626
  if self.environment.available_shape_classes:
591
627
  available_shape_classes += sorted(self.environment.available_shape_classes.keys())
592
628
 
629
+ available_user_tasks = (
630
+ {
631
+ user_class_name: [task.__name__ for task in user_class]
632
+ for (user_class_name, user_class) in self.environment.available_user_tasks.items()
633
+ }
634
+ if self.environment.available_user_tasks
635
+ else None
636
+ )
637
+
593
638
  if self.modern_ui:
594
639
  percentiles = {
595
640
  "percentiles_to_chart": stats_module.MODERN_UI_PERCENTILES_TO_CHART,
641
+ "percentiles_to_statistics": stats_module.PERCENTILES_TO_STATISTICS,
596
642
  }
597
643
  else:
598
644
  percentiles = {
@@ -623,12 +669,15 @@ class WebUI:
623
669
  "show_userclass_picker": self.userclass_picker_is_active,
624
670
  "available_user_classes": available_user_classes,
625
671
  "available_shape_classes": available_shape_classes,
672
+ "available_user_tasks": available_user_tasks,
673
+ "users": users,
626
674
  **percentiles,
627
675
  }
628
676
 
629
677
  def _update_shape_class(self, shape_class_name):
630
678
  if shape_class_name:
631
679
  shape_class = self.environment.available_shape_classes[shape_class_name]
680
+ shape_class.runner = self.environment.runner
632
681
  else:
633
682
  shape_class = None
634
683
 
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-5e21717c.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}