prefect-client 3.0.0rc1__py3-none-any.whl → 3.0.0rc3__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.
- prefect/_internal/compatibility/migration.py +124 -0
- prefect/_internal/concurrency/__init__.py +2 -2
- prefect/_internal/concurrency/primitives.py +1 -0
- prefect/_internal/pydantic/annotations/pendulum.py +2 -2
- prefect/_internal/pytz.py +1 -1
- prefect/blocks/core.py +1 -1
- prefect/blocks/redis.py +168 -0
- prefect/client/orchestration.py +113 -23
- prefect/client/schemas/actions.py +1 -1
- prefect/client/schemas/filters.py +6 -0
- prefect/client/schemas/objects.py +22 -11
- prefect/client/subscriptions.py +3 -2
- prefect/concurrency/asyncio.py +1 -1
- prefect/concurrency/services.py +1 -1
- prefect/context.py +1 -27
- prefect/deployments/__init__.py +3 -0
- prefect/deployments/base.py +11 -3
- prefect/deployments/deployments.py +3 -0
- prefect/deployments/steps/pull.py +1 -0
- prefect/deployments/steps/utility.py +2 -1
- prefect/engine.py +3 -0
- prefect/events/cli/automations.py +1 -1
- prefect/events/clients.py +7 -1
- prefect/events/schemas/events.py +2 -0
- prefect/exceptions.py +9 -0
- prefect/filesystems.py +22 -11
- prefect/flow_engine.py +118 -156
- prefect/flow_runs.py +2 -2
- prefect/flows.py +91 -35
- prefect/futures.py +44 -43
- prefect/infrastructure/provisioners/container_instance.py +1 -0
- prefect/infrastructure/provisioners/ecs.py +2 -2
- prefect/input/__init__.py +4 -0
- prefect/input/run_input.py +4 -2
- prefect/logging/formatters.py +2 -2
- prefect/logging/handlers.py +2 -2
- prefect/logging/loggers.py +1 -1
- prefect/plugins.py +1 -0
- prefect/records/cache_policies.py +179 -0
- prefect/records/result_store.py +10 -3
- prefect/results.py +27 -55
- prefect/runner/runner.py +1 -1
- prefect/runner/server.py +1 -1
- prefect/runtime/__init__.py +1 -0
- prefect/runtime/deployment.py +1 -0
- prefect/runtime/flow_run.py +1 -0
- prefect/runtime/task_run.py +1 -0
- prefect/settings.py +21 -5
- prefect/states.py +17 -4
- prefect/task_engine.py +337 -209
- prefect/task_runners.py +15 -5
- prefect/task_runs.py +203 -0
- prefect/{task_server.py → task_worker.py} +66 -36
- prefect/tasks.py +180 -77
- prefect/transactions.py +92 -16
- prefect/types/__init__.py +1 -1
- prefect/utilities/asyncutils.py +3 -3
- prefect/utilities/callables.py +90 -7
- prefect/utilities/dockerutils.py +5 -3
- prefect/utilities/engine.py +11 -0
- prefect/utilities/filesystem.py +4 -5
- prefect/utilities/importtools.py +34 -5
- prefect/utilities/services.py +2 -2
- prefect/utilities/urls.py +195 -0
- prefect/utilities/visualization.py +1 -0
- prefect/variables.py +19 -10
- prefect/workers/base.py +46 -1
- {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/METADATA +3 -2
- {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/RECORD +72 -66
- {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/LICENSE +0 -0
- {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/WHEEL +0 -0
- {prefect_client-3.0.0rc1.dist-info → prefect_client-3.0.0rc3.dist-info}/top_level.txt +0 -0
prefect/flow_engine.py
CHANGED
@@ -9,6 +9,7 @@ from typing import (
|
|
9
9
|
Callable,
|
10
10
|
Coroutine,
|
11
11
|
Dict,
|
12
|
+
Generator,
|
12
13
|
Generic,
|
13
14
|
Iterable,
|
14
15
|
Literal,
|
@@ -20,22 +21,17 @@ from typing import (
|
|
20
21
|
)
|
21
22
|
from uuid import UUID
|
22
23
|
|
23
|
-
import anyio
|
24
|
-
import anyio._backends._asyncio
|
25
|
-
from sniffio import AsyncLibraryNotFoundError
|
26
24
|
from typing_extensions import ParamSpec
|
27
25
|
|
28
|
-
from prefect import Task
|
29
|
-
from prefect.
|
30
|
-
from prefect.client.orchestration import SyncPrefectClient
|
26
|
+
from prefect import Task
|
27
|
+
from prefect.client.orchestration import SyncPrefectClient, get_client
|
31
28
|
from prefect.client.schemas import FlowRun, TaskRun
|
32
29
|
from prefect.client.schemas.filters import FlowRunFilter
|
33
30
|
from prefect.client.schemas.sorting import FlowRunSort
|
34
|
-
from prefect.context import ClientContext, FlowRunContext, TagsContext
|
31
|
+
from prefect.context import ClientContext, FlowRunContext, TagsContext
|
35
32
|
from prefect.exceptions import Abort, Pause, PrefectException, UpstreamTaskError
|
36
33
|
from prefect.flows import Flow, load_flow_from_entrypoint, load_flow_from_flow_run
|
37
34
|
from prefect.futures import PrefectFuture, resolve_futures_to_states
|
38
|
-
from prefect.logging.handlers import APILogHandler
|
39
35
|
from prefect.logging.loggers import (
|
40
36
|
flow_run_logger,
|
41
37
|
get_logger,
|
@@ -43,7 +39,7 @@ from prefect.logging.loggers import (
|
|
43
39
|
patch_print,
|
44
40
|
)
|
45
41
|
from prefect.results import ResultFactory
|
46
|
-
from prefect.settings import PREFECT_DEBUG_MODE
|
42
|
+
from prefect.settings import PREFECT_DEBUG_MODE
|
47
43
|
from prefect.states import (
|
48
44
|
Failed,
|
49
45
|
Pending,
|
@@ -54,7 +50,7 @@ from prefect.states import (
|
|
54
50
|
return_value_to_state,
|
55
51
|
)
|
56
52
|
from prefect.utilities.asyncutils import run_coro_as_sync
|
57
|
-
from prefect.utilities.callables import
|
53
|
+
from prefect.utilities.callables import call_with_parameters
|
58
54
|
from prefect.utilities.collections import visit_collection
|
59
55
|
from prefect.utilities.engine import (
|
60
56
|
_get_hook_name,
|
@@ -64,6 +60,7 @@ from prefect.utilities.engine import (
|
|
64
60
|
resolve_to_final_result,
|
65
61
|
)
|
66
62
|
from prefect.utilities.timeout import timeout, timeout_async
|
63
|
+
from prefect.utilities.urls import url_for
|
67
64
|
|
68
65
|
P = ParamSpec("P")
|
69
66
|
R = TypeVar("R")
|
@@ -174,9 +171,6 @@ class FlowRunEngine(Generic[P, R]):
|
|
174
171
|
while state.is_pending():
|
175
172
|
time.sleep(0.2)
|
176
173
|
state = self.set_state(new_state)
|
177
|
-
if state.is_running():
|
178
|
-
for hook in self.get_hooks(state):
|
179
|
-
hook()
|
180
174
|
return state
|
181
175
|
|
182
176
|
def set_state(self, state: State, force: bool = False) -> State:
|
@@ -349,12 +343,14 @@ class FlowRunEngine(Generic[P, R]):
|
|
349
343
|
|
350
344
|
return flow_run
|
351
345
|
|
352
|
-
def
|
346
|
+
def call_hooks(self, state: State = None) -> Iterable[Callable]:
|
347
|
+
if state is None:
|
348
|
+
state = self.state
|
353
349
|
flow = self.flow
|
354
350
|
flow_run = self.flow_run
|
355
351
|
|
356
352
|
if not flow_run:
|
357
|
-
raise ValueError("
|
353
|
+
raise ValueError("Flow run is not set")
|
358
354
|
|
359
355
|
enable_cancellation_and_crashed_hooks = (
|
360
356
|
os.environ.get(
|
@@ -363,7 +359,6 @@ class FlowRunEngine(Generic[P, R]):
|
|
363
359
|
== "true"
|
364
360
|
)
|
365
361
|
|
366
|
-
hooks = None
|
367
362
|
if state.is_failed() and flow.on_failure_hooks:
|
368
363
|
hooks = flow.on_failure_hooks
|
369
364
|
elif state.is_completed() and flow.on_completion_hooks:
|
@@ -382,48 +377,30 @@ class FlowRunEngine(Generic[P, R]):
|
|
382
377
|
hooks = flow.on_crashed_hooks
|
383
378
|
elif state.is_running() and flow.on_running_hooks:
|
384
379
|
hooks = flow.on_running_hooks
|
380
|
+
else:
|
381
|
+
hooks = None
|
385
382
|
|
386
383
|
for hook in hooks or []:
|
387
384
|
hook_name = _get_hook_name(hook)
|
388
385
|
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
else:
|
403
|
-
self.logger.info(
|
404
|
-
f"Hook {hook_name!r} finished running successfully"
|
405
|
-
)
|
406
|
-
|
407
|
-
if as_async:
|
408
|
-
|
409
|
-
async def _hook_fn():
|
410
|
-
with hook_context():
|
411
|
-
result = hook(flow, flow_run, state)
|
412
|
-
if inspect.isawaitable(result):
|
413
|
-
await result
|
414
|
-
|
386
|
+
try:
|
387
|
+
self.logger.info(
|
388
|
+
f"Running hook {hook_name!r} in response to entering state"
|
389
|
+
f" {state.name!r}"
|
390
|
+
)
|
391
|
+
result = hook(flow, flow_run, state)
|
392
|
+
if inspect.isawaitable(result):
|
393
|
+
run_coro_as_sync(result)
|
394
|
+
except Exception:
|
395
|
+
self.logger.error(
|
396
|
+
f"An error was encountered while running hook {hook_name!r}",
|
397
|
+
exc_info=True,
|
398
|
+
)
|
415
399
|
else:
|
416
|
-
|
417
|
-
def _hook_fn():
|
418
|
-
with hook_context():
|
419
|
-
result = hook(flow, flow_run, state)
|
420
|
-
if inspect.isawaitable(result):
|
421
|
-
run_coro_as_sync(result)
|
422
|
-
|
423
|
-
yield _hook_fn
|
400
|
+
self.logger.info(f"Hook {hook_name!r} finished running successfully")
|
424
401
|
|
425
402
|
@contextmanager
|
426
|
-
def
|
403
|
+
def setup_run_context(self, client: Optional[SyncPrefectClient] = None):
|
427
404
|
from prefect.utilities.engine import (
|
428
405
|
should_log_prints,
|
429
406
|
)
|
@@ -436,13 +413,6 @@ class FlowRunEngine(Generic[P, R]):
|
|
436
413
|
self.flow_run = client.read_flow_run(self.flow_run.id)
|
437
414
|
log_prints = should_log_prints(self.flow)
|
438
415
|
|
439
|
-
# if running in a completely synchronous frame, anyio will not detect the
|
440
|
-
# backend to use for the task group
|
441
|
-
try:
|
442
|
-
task_group = anyio.create_task_group()
|
443
|
-
except AsyncLibraryNotFoundError:
|
444
|
-
task_group = anyio._backends._asyncio.TaskGroup()
|
445
|
-
|
446
416
|
with ExitStack() as stack:
|
447
417
|
# TODO: Explore closing task runner before completing the flow to
|
448
418
|
# wait for futures to complete
|
@@ -457,7 +427,6 @@ class FlowRunEngine(Generic[P, R]):
|
|
457
427
|
flow_run=self.flow_run,
|
458
428
|
parameters=self.parameters,
|
459
429
|
client=client,
|
460
|
-
background_tasks=task_group,
|
461
430
|
result_factory=run_coro_as_sync(ResultFactory.from_flow(self.flow)),
|
462
431
|
task_runner=task_runner,
|
463
432
|
)
|
@@ -482,7 +451,7 @@ class FlowRunEngine(Generic[P, R]):
|
|
482
451
|
yield
|
483
452
|
|
484
453
|
@contextmanager
|
485
|
-
def
|
454
|
+
def initialize_run(self):
|
486
455
|
"""
|
487
456
|
Enters a client context and creates a flow run if needed.
|
488
457
|
"""
|
@@ -490,27 +459,29 @@ class FlowRunEngine(Generic[P, R]):
|
|
490
459
|
self._client = client_ctx.sync_client
|
491
460
|
self._is_started = True
|
492
461
|
|
493
|
-
# this conditional is engaged whenever a run is triggered via deployment
|
494
|
-
if self.flow_run_id and not self.flow:
|
495
|
-
self.flow_run = self.client.read_flow_run(self.flow_run_id)
|
496
|
-
try:
|
497
|
-
self.flow = self.load_flow(self.client)
|
498
|
-
except Exception as exc:
|
499
|
-
self.handle_exception(
|
500
|
-
exc,
|
501
|
-
msg="Failed to load flow from entrypoint.",
|
502
|
-
)
|
503
|
-
self.short_circuit = True
|
504
|
-
|
505
462
|
if not self.flow_run:
|
506
463
|
self.flow_run = self.create_flow_run(self.client)
|
464
|
+
flow_run_url = url_for(self.flow_run)
|
507
465
|
|
508
|
-
|
509
|
-
if ui_url:
|
466
|
+
if flow_run_url:
|
510
467
|
self.logger.info(
|
511
|
-
f"View at {
|
512
|
-
extra={"send_to_api": False},
|
468
|
+
f"View at {flow_run_url}", extra={"send_to_api": False}
|
513
469
|
)
|
470
|
+
else:
|
471
|
+
# Update the empirical policy to match the flow if it is not set
|
472
|
+
if self.flow_run.empirical_policy.retry_delay is None:
|
473
|
+
self.flow_run.empirical_policy.retry_delay = (
|
474
|
+
self.flow.retry_delay_seconds
|
475
|
+
)
|
476
|
+
|
477
|
+
if self.flow_run.empirical_policy.retries is None:
|
478
|
+
self.flow_run.empirical_policy.retries = self.flow.retries
|
479
|
+
|
480
|
+
self.client.update_flow_run(
|
481
|
+
flow_run_id=self.flow_run.id,
|
482
|
+
flow_version=self.flow.version,
|
483
|
+
empirical_policy=self.flow_run.empirical_policy,
|
484
|
+
)
|
514
485
|
|
515
486
|
# validate prior to context so that context receives validated params
|
516
487
|
if self.flow.should_validate_parameters:
|
@@ -536,6 +507,9 @@ class FlowRunEngine(Generic[P, R]):
|
|
536
507
|
raise
|
537
508
|
except (Abort, Pause):
|
538
509
|
raise
|
510
|
+
except GeneratorExit:
|
511
|
+
# Do not capture generator exits as crashes
|
512
|
+
raise
|
539
513
|
except BaseException as exc:
|
540
514
|
# BaseExceptions are caught and handled as crashes
|
541
515
|
self.handle_crash(exc)
|
@@ -550,12 +524,6 @@ class FlowRunEngine(Generic[P, R]):
|
|
550
524
|
msg=f"Finished in state {display_state}",
|
551
525
|
)
|
552
526
|
|
553
|
-
# flush any logs in the background if this is a "top" level run
|
554
|
-
if not (FlowRunContext.get() or TaskRunContext.get()):
|
555
|
-
from_sync.call_soon_in_loop_thread(
|
556
|
-
create_call(APILogHandler.aflush)
|
557
|
-
)
|
558
|
-
|
559
527
|
self._is_started = False
|
560
528
|
self._client = None
|
561
529
|
|
@@ -569,61 +537,81 @@ class FlowRunEngine(Generic[P, R]):
|
|
569
537
|
return False # TODO: handle this differently?
|
570
538
|
return getattr(self, "flow_run").state.is_pending()
|
571
539
|
|
540
|
+
# --------------------------
|
541
|
+
#
|
542
|
+
# The following methods compose the main task run loop
|
543
|
+
#
|
544
|
+
# --------------------------
|
572
545
|
|
573
|
-
|
574
|
-
|
546
|
+
@contextmanager
|
547
|
+
def start(self) -> Generator[None, None, None]:
|
548
|
+
with self.initialize_run():
|
549
|
+
self.begin_run()
|
550
|
+
|
551
|
+
if self.state.is_running():
|
552
|
+
self.call_hooks()
|
553
|
+
try:
|
554
|
+
yield
|
555
|
+
finally:
|
556
|
+
if self.state.is_final() or self.state.is_cancelling():
|
557
|
+
self.call_hooks()
|
558
|
+
|
559
|
+
@contextmanager
|
560
|
+
def run_context(self):
|
561
|
+
timeout_context = timeout_async if self.flow.isasync else timeout
|
562
|
+
# reenter the run context to ensure it is up to date for every run
|
563
|
+
with self.setup_run_context():
|
564
|
+
try:
|
565
|
+
with timeout_context(seconds=self.flow.timeout_seconds):
|
566
|
+
self.logger.debug(
|
567
|
+
f"Executing flow {self.flow.name!r} for flow run {self.flow_run.name!r}..."
|
568
|
+
)
|
569
|
+
yield self
|
570
|
+
except TimeoutError as exc:
|
571
|
+
self.handle_timeout(exc)
|
572
|
+
except Exception as exc:
|
573
|
+
self.logger.exception(f"Encountered exception during execution: {exc}")
|
574
|
+
self.handle_exception(exc)
|
575
|
+
|
576
|
+
def call_flow_fn(self) -> Union[R, Coroutine[Any, Any, R]]:
|
577
|
+
"""
|
578
|
+
Convenience method to call the flow function. Returns a coroutine if the
|
579
|
+
flow is async.
|
580
|
+
"""
|
581
|
+
if self.flow.isasync:
|
582
|
+
|
583
|
+
async def _call_flow_fn():
|
584
|
+
result = await call_with_parameters(self.flow.fn, self.parameters)
|
585
|
+
self.handle_success(result)
|
586
|
+
|
587
|
+
return _call_flow_fn()
|
588
|
+
else:
|
589
|
+
result = call_with_parameters(self.flow.fn, self.parameters)
|
590
|
+
self.handle_success(result)
|
591
|
+
|
592
|
+
|
593
|
+
def run_flow_sync(
|
594
|
+
flow: Flow[P, R],
|
575
595
|
flow_run: Optional[FlowRun] = None,
|
576
596
|
parameters: Optional[Dict[str, Any]] = None,
|
577
597
|
wait_for: Optional[Iterable[PrefectFuture]] = None,
|
578
598
|
return_type: Literal["state", "result"] = "result",
|
579
|
-
) -> Union[R, None]:
|
580
|
-
|
581
|
-
Runs a flow against the API.
|
599
|
+
) -> Union[R, State, None]:
|
600
|
+
parameters = flow_run.parameters if flow_run else parameters
|
582
601
|
|
583
|
-
We will most likely want to use this logic as a wrapper and return a coroutine for type inference.
|
584
|
-
"""
|
585
602
|
engine = FlowRunEngine[P, R](
|
586
|
-
flow=flow,
|
587
|
-
parameters=flow_run.parameters if flow_run else parameters,
|
588
|
-
flow_run=flow_run,
|
589
|
-
wait_for=wait_for,
|
603
|
+
flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
|
590
604
|
)
|
591
605
|
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
while run.is_running():
|
597
|
-
with run.enter_run_context():
|
598
|
-
try:
|
599
|
-
# This is where the flow is actually run.
|
600
|
-
with timeout_async(seconds=run.flow.timeout_seconds):
|
601
|
-
call_args, call_kwargs = parameters_to_args_kwargs(
|
602
|
-
flow.fn, run.parameters or {}
|
603
|
-
)
|
604
|
-
run.logger.debug(
|
605
|
-
f"Executing flow {flow.name!r} for flow run {run.flow_run.name!r}..."
|
606
|
-
)
|
607
|
-
result = cast(R, await flow.fn(*call_args, **call_kwargs)) # type: ignore
|
608
|
-
# If the flow run is successful, finalize it.
|
609
|
-
run.handle_success(result)
|
610
|
-
|
611
|
-
except TimeoutError as exc:
|
612
|
-
run.handle_timeout(exc)
|
613
|
-
except Exception as exc:
|
614
|
-
# If the flow fails, and we have retries left, set the flow to retrying.
|
615
|
-
run.logger.exception("Encountered exception during execution:")
|
616
|
-
run.handle_exception(exc)
|
606
|
+
with engine.start():
|
607
|
+
while engine.is_running():
|
608
|
+
with engine.run_context():
|
609
|
+
engine.call_flow_fn()
|
617
610
|
|
618
|
-
|
619
|
-
for hook in run.get_hooks(run.state, as_async=True):
|
620
|
-
await hook()
|
621
|
-
if return_type == "state":
|
622
|
-
return run.state
|
623
|
-
return run.result()
|
611
|
+
return engine.state if return_type == "state" else engine.result()
|
624
612
|
|
625
613
|
|
626
|
-
def
|
614
|
+
async def run_flow_async(
|
627
615
|
flow: Flow[P, R],
|
628
616
|
flow_run: Optional[FlowRun] = None,
|
629
617
|
parameters: Optional[Dict[str, Any]] = None,
|
@@ -636,38 +624,12 @@ def run_flow_sync(
|
|
636
624
|
flow=flow, parameters=parameters, flow_run=flow_run, wait_for=wait_for
|
637
625
|
)
|
638
626
|
|
639
|
-
|
640
|
-
|
641
|
-
|
627
|
+
with engine.start():
|
628
|
+
while engine.is_running():
|
629
|
+
with engine.run_context():
|
630
|
+
await engine.call_flow_fn()
|
642
631
|
|
643
|
-
|
644
|
-
with run.enter_run_context():
|
645
|
-
try:
|
646
|
-
# This is where the flow is actually run.
|
647
|
-
with timeout(seconds=run.flow.timeout_seconds):
|
648
|
-
call_args, call_kwargs = parameters_to_args_kwargs(
|
649
|
-
flow.fn, run.parameters or {}
|
650
|
-
)
|
651
|
-
run.logger.debug(
|
652
|
-
f"Executing flow {flow.name!r} for flow run {run.flow_run.name!r}..."
|
653
|
-
)
|
654
|
-
result = cast(R, flow.fn(*call_args, **call_kwargs)) # type: ignore
|
655
|
-
# If the flow run is successful, finalize it.
|
656
|
-
run.handle_success(result)
|
657
|
-
|
658
|
-
except TimeoutError as exc:
|
659
|
-
run.handle_timeout(exc)
|
660
|
-
except Exception as exc:
|
661
|
-
# If the flow fails, and we have retries left, set the flow to retrying.
|
662
|
-
run.logger.exception("Encountered exception during execution:")
|
663
|
-
run.handle_exception(exc)
|
664
|
-
|
665
|
-
if run.state.is_final() or run.state.is_cancelling():
|
666
|
-
for hook in run.get_hooks(run.state):
|
667
|
-
hook()
|
668
|
-
if return_type == "state":
|
669
|
-
return run.state
|
670
|
-
return run.result()
|
632
|
+
return engine.state if return_type == "state" else engine.result()
|
671
633
|
|
672
634
|
|
673
635
|
def run_flow(
|
prefect/flow_runs.py
CHANGED
@@ -76,7 +76,7 @@ async def wait_for_flow_run(
|
|
76
76
|
```python
|
77
77
|
import asyncio
|
78
78
|
|
79
|
-
from prefect import get_client
|
79
|
+
from prefect.client.orchestration import get_client
|
80
80
|
from prefect.flow_runs import wait_for_flow_run
|
81
81
|
|
82
82
|
async def main():
|
@@ -94,7 +94,7 @@ async def wait_for_flow_run(
|
|
94
94
|
```python
|
95
95
|
import asyncio
|
96
96
|
|
97
|
-
from prefect import get_client
|
97
|
+
from prefect.client.orchestration import get_client
|
98
98
|
from prefect.flow_runs import wait_for_flow_run
|
99
99
|
|
100
100
|
async def main(num_runs: int):
|