labtasker-client 2.5.0__tar.gz → 2.6.0__tar.gz

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 (26) hide show
  1. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/PKG-INFO +1 -1
  2. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/pyproject.toml +1 -1
  3. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/__init__.py +1 -1
  4. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/cli.py +48 -50
  5. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/client.py +87 -49
  6. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/command_worker.py +24 -24
  7. labtasker_client-2.6.0/src/labtasker/config.py +294 -0
  8. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/execution.py +16 -21
  9. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/journal.py +33 -18
  10. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/local.py +40 -48
  11. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/observations.py +4 -2
  12. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/worker.py +150 -15
  13. labtasker_client-2.5.0/src/labtasker/config.py +0 -213
  14. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/.gitignore +0 -0
  15. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/LICENSE +0 -0
  16. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/__main__.py +0 -0
  17. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/api.py +0 -0
  18. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/binding.py +0 -0
  19. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/command_template.py +0 -0
  20. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/errors.py +0 -0
  21. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/models.py +0 -0
  22. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/paths.py +0 -0
  23. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/py.typed +0 -0
  24. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/tee.py +0 -0
  25. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/types.py +0 -0
  26. {labtasker_client-2.5.0 → labtasker_client-2.6.0}/src/labtasker/validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labtasker-client
3
- Version: 2.5.0
3
+ Version: 2.6.0
4
4
  Summary: A small task queue for parallel model inference and evaluation
5
5
  Project-URL: Homepage, https://github.com/luocfprime/labtasker
6
6
  Project-URL: Repository, https://github.com/luocfprime/labtasker.git
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "labtasker-client"
7
- version = "2.5.0"
7
+ version = "2.6.0"
8
8
  description = "A small task queue for parallel model inference and evaluation"
9
9
  requires-python = ">=3.10"
10
10
  license = "Apache-2.0"
@@ -50,7 +50,7 @@ from labtasker.models import (
50
50
  from labtasker.types import JSONValue, TaskOrderField, TaskStatus, TaskUpdate
51
51
  from labtasker.worker import loop
52
52
 
53
- __version__ = "2.5.0"
53
+ __version__ = "2.6.0"
54
54
 
55
55
  __all__ = [
56
56
  "APIError",
@@ -3,6 +3,9 @@ from __future__ import annotations
3
3
  import json
4
4
  import logging
5
5
  from collections.abc import Callable
6
+ from contextvars import ContextVar
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
6
9
  from typing import Annotated, Any, TypeVar, cast
7
10
 
8
11
  import typer
@@ -15,8 +18,6 @@ from labtasker.command_template import TemplateSyntaxError
15
18
  from labtasker.command_worker import run_command_worker
16
19
  from labtasker.config import resolve_config
17
20
  from labtasker.errors import LabtaskerError
18
- from labtasker.execution import report_progress as report_current_progress
19
- from labtasker.execution import report_worker_telemetry as report_current_worker_telemetry
20
21
  from labtasker.types import TaskOrderField, TaskStatus, TaskUpdate
21
22
  from labtasker.validation import RequestValidationError, validate_grouping, validate_json_object
22
23
 
@@ -59,6 +60,15 @@ app.add_typer(config_app, name="config")
59
60
  logger = logging.getLogger("labtasker.cli")
60
61
 
61
62
 
63
+ @dataclass(frozen=True, slots=True)
64
+ class CLIState:
65
+ labtasker_root: Path | None
66
+ auto_start_local_server: bool
67
+
68
+
69
+ _CLI_STATE: ContextVar[CLIState | None] = ContextVar("labtasker_cli_state", default=None)
70
+
71
+
62
72
  def _version_callback(value: bool) -> None:
63
73
  if value:
64
74
  typer.echo(f"labtasker-client {__version__}")
@@ -76,8 +86,20 @@ def main(
76
86
  help="Show the Client package version and exit.",
77
87
  ),
78
88
  ] = False,
89
+ labtasker_root: Annotated[
90
+ Path | None,
91
+ typer.Option(help="Exact configuration, journal, and managed-local root."),
92
+ ] = None,
93
+ auto_start_local_server: Annotated[
94
+ bool,
95
+ typer.Option(
96
+ "--auto-start-local-server",
97
+ help="Allow this invocation to start a managed-local daemon.",
98
+ ),
99
+ ] = False,
79
100
  ) -> None:
80
101
  """Submit, inspect, and execute Labtasker v2 Tasks."""
102
+ _CLI_STATE.set(CLIState(labtasker_root, auto_start_local_server))
81
103
 
82
104
 
83
105
  class _SeparatedCommand(TyperCommand):
@@ -161,6 +183,8 @@ def worker_loop(
161
183
  max_consecutive_failures=max_consecutive_failures,
162
184
  force_stop_timeout=force_stop_timeout,
163
185
  metadata=worker_metadata,
186
+ labtasker_root=_cli_state().labtasker_root,
187
+ auto_start_local_server=_cli_state().auto_start_local_server,
164
188
  )
165
189
  except (TemplateSyntaxError, RequestValidationError) as error:
166
190
  raise typer.BadParameter(str(error)) from error
@@ -177,29 +201,6 @@ def worker_loop(
177
201
  raise typer.Exit(1) from error
178
202
 
179
203
 
180
- @app.command("progress")
181
- def progress_report(
182
- data: Annotated[
183
- str,
184
- typer.Option(help="Latest progress as one strict JSON object."),
185
- ],
186
- ) -> None:
187
- """Replace the current Task run's progress snapshot.
188
-
189
- This command is available inside a command launched by ``labtasker loop``.
190
- It prints whether the best-effort report was accepted; transport failures
191
- and confirmed revocation return ``reported: false`` without failing the
192
- command workload.
193
- """
194
- try:
195
- progress = _json_object(data, option="--data")
196
- reported = _invoke(lambda: report_current_progress(progress))
197
- except RuntimeError as error:
198
- typer.echo(str(error), err=True)
199
- raise typer.Exit(1) from error
200
- _write_json({"reported": reported})
201
-
202
-
203
204
  @task_app.command("submit")
204
205
  def task_submit(
205
206
  args: Annotated[
@@ -472,28 +473,6 @@ def worker_count(
472
473
  _write_json({"count": result} if isinstance(result, int) else result)
473
474
 
474
475
 
475
- @worker_app.command("telemetry")
476
- def worker_telemetry_report(
477
- data: Annotated[
478
- str,
479
- typer.Option(help="Latest Worker telemetry as one strict JSON object."),
480
- ],
481
- ) -> None:
482
- """Replace telemetry for the current Worker invocation.
483
-
484
- This command is available inside a command launched by ``labtasker loop``.
485
- It performs one best-effort synchronous report and prints whether the Server
486
- accepted it.
487
- """
488
- try:
489
- telemetry = _json_object(data, option="--data")
490
- reported = _invoke(lambda: report_current_worker_telemetry(telemetry))
491
- except RuntimeError as error:
492
- typer.echo(str(error), err=True)
493
- raise typer.Exit(1) from error
494
- _write_json({"reported": reported})
495
-
496
-
497
476
  def _count_options(
498
477
  group_by: list[str] | None, allowed: set[str], limit: int | None, cursor: str | None
499
478
  ) -> dict[str, Any]:
@@ -628,7 +607,10 @@ def queue_delete(
628
607
  name: Annotated[str, typer.Argument(help="Queue name to delete.")],
629
608
  cascade: Annotated[
630
609
  bool,
631
- typer.Option(help="Also permanently delete every non-running Task in the Queue."),
610
+ typer.Option(
611
+ "--cascade",
612
+ help="Also permanently delete every non-running Task in the Queue.",
613
+ ),
632
614
  ] = False,
633
615
  ) -> None:
634
616
  """Permanently delete one Queue.
@@ -647,14 +629,30 @@ def config_show() -> None:
647
629
  .labtasker/config.toml, then built-in defaults. The token value is never
648
630
  printed.
649
631
  """
650
- _write_json(_invoke(lambda: resolve_config().public_dict()))
632
+ state = _cli_state()
633
+ _write_json(
634
+ _invoke(
635
+ lambda: resolve_config(
636
+ labtasker_root=state.labtasker_root,
637
+ auto_start_local_server=state.auto_start_local_server,
638
+ ).public_dict()
639
+ )
640
+ )
651
641
 
652
642
 
653
643
  def _with_client(operation: Callable[[Client], T]) -> T:
654
- with Client() as client:
644
+ state = _cli_state()
645
+ with Client(
646
+ labtasker_root=state.labtasker_root,
647
+ auto_start_local_server=state.auto_start_local_server,
648
+ ) as client:
655
649
  return operation(client)
656
650
 
657
651
 
652
+ def _cli_state() -> CLIState:
653
+ return _CLI_STATE.get() or CLIState(None, False)
654
+
655
+
658
656
  def _invoke(operation: Callable[[], T]) -> T:
659
657
  try:
660
658
  return operation()
@@ -13,12 +13,7 @@ from pydantic import TypeAdapter, ValidationError
13
13
 
14
14
  from labtasker.config import ResolvedConfig, resolve_config
15
15
  from labtasker.errors import APIError, TransportError
16
- from labtasker.local import (
17
- ensure_local_server,
18
- local_paths,
19
- require_local_capabilities,
20
- socket_transport,
21
- )
16
+ from labtasker.local import ensure_local_server, socket_transport
22
17
  from labtasker.models import (
23
18
  BulkUpdateResult,
24
19
  ClaimResponse,
@@ -53,7 +48,7 @@ from labtasker.validation import (
53
48
 
54
49
  T = TypeVar("T")
55
50
  ModelT = TypeVar("ModelT", bound=ResponseModel)
56
- REQUEST_TIMEOUT_SECONDS = 10.0
51
+ REQUEST_TIMEOUT_SECONDS = 15.0
57
52
  MAX_RETRY_ATTEMPTS = 3
58
53
  RETRY_BACKOFF_SECONDS = (0.05, 0.1)
59
54
  QUEUE_LIST_ADAPTER = TypeAdapter(list[Queue])
@@ -63,21 +58,36 @@ class Client:
63
58
  def __init__(
64
59
  self,
65
60
  url: str | None = None,
61
+ socket: str | Path | None = None,
62
+ labtasker_root: str | Path | None = None,
63
+ auto_start_local_server: bool = False,
66
64
  token: str | None = None,
67
65
  queue: str | None = None,
68
66
  ) -> None:
69
- self._initialize(resolve_config(url=url, token=token, queue=queue))
67
+ self._initialize(
68
+ resolve_config(
69
+ url=url,
70
+ socket=socket,
71
+ labtasker_root=labtasker_root,
72
+ auto_start_local_server=auto_start_local_server,
73
+ token=token,
74
+ queue=queue,
75
+ )
76
+ )
70
77
 
71
78
  @classmethod
72
- def _from_local_directory(cls, directory: Path, *, queue: str) -> Client:
73
- require_local_capabilities()
79
+ def _from_socket(cls, socket: Path, *, queue: str) -> Client:
74
80
  client = cls.__new__(cls)
75
81
  client._initialize(
76
82
  ResolvedConfig(
77
83
  url=None,
84
+ socket=socket,
85
+ managed_local=False,
86
+ labtasker_root=Path("/"),
78
87
  queue=validate_identifier(queue, field="queue"),
79
88
  token=None,
80
- local=local_paths(directory),
89
+ auto_start_local_server=False,
90
+ local=None,
81
91
  )
82
92
  )
83
93
  return client
@@ -87,22 +97,22 @@ class Client:
87
97
  headers = {}
88
98
  if self._config.token is not None:
89
99
  headers["Authorization"] = f"Bearer {self._config.token}"
90
- if self._config.local is None:
91
- assert self._config.url is not None
100
+ if self._config.url is not None:
92
101
  self._http = httpx.Client(
93
102
  base_url=f"{self._config.url}/api/v2/",
94
103
  headers=headers,
95
104
  timeout=REQUEST_TIMEOUT_SECONDS,
96
105
  )
97
106
  else:
107
+ assert self._config.socket is not None
98
108
  self._http = httpx.Client(
99
109
  base_url="http://labtasker/api/v2/",
100
- transport=socket_transport(self._config.local),
110
+ transport=socket_transport(self._config.socket),
101
111
  timeout=REQUEST_TIMEOUT_SECONDS,
102
112
  )
103
113
  self._closed = False
104
114
  self._endpoint_announced = False
105
- self._local_ready = False
115
+ self._local_ensure_result: tuple[int | None, str | None] | None = None
106
116
  self._server_version: str | None = None
107
117
  self._warned_server_versions: set[Version] = set()
108
118
 
@@ -148,7 +158,7 @@ class Client:
148
158
  )
149
159
 
150
160
  @property
151
- def configuration(self) -> ResolvedConfig:
161
+ def _configuration(self) -> ResolvedConfig:
152
162
  return self._config
153
163
 
154
164
  def submit_task(
@@ -538,7 +548,7 @@ class Client:
538
548
  path=f"queues/{queue_name}/tasks/claim",
539
549
  json={"route": normalized_route, "run_id": normalized_run_id},
540
550
  parser=_parse_claim,
541
- retry=True,
551
+ recover_local_connect=False,
542
552
  )
543
553
 
544
554
  def _health(self) -> HealthResponse:
@@ -561,6 +571,7 @@ class Client:
561
571
  task_id: str,
562
572
  run_id: str,
563
573
  queue: str | None = None,
574
+ recover_local_connect: bool = True,
564
575
  ) -> HeartbeatResponse:
565
576
  return self._run_action(
566
577
  "heartbeat",
@@ -569,6 +580,7 @@ class Client:
569
580
  queue=queue,
570
581
  body={},
571
582
  parser=lambda response: _parse_model(response, HeartbeatResponse, {200}),
583
+ recover_local_connect=recover_local_connect,
572
584
  )
573
585
 
574
586
  def _complete(
@@ -707,6 +719,7 @@ class Client:
707
719
  queue: str | None,
708
720
  body: dict[str, object],
709
721
  parser: Callable[[httpx.Response], T],
722
+ recover_local_connect: bool = True,
710
723
  ) -> T:
711
724
  self._ensure_open()
712
725
  queue_name = self._queue(queue)
@@ -718,6 +731,7 @@ class Client:
718
731
  path=f"queues/{queue_name}/tasks/{normalized_task_id}/{action}",
719
732
  json={"run_id": normalized_run_id, **body},
720
733
  parser=parser,
734
+ recover_local_connect=recover_local_connect,
721
735
  )
722
736
 
723
737
  def _queue(self, queue: str | None) -> str:
@@ -733,9 +747,9 @@ class Client:
733
747
  json: object | None = None,
734
748
  params: dict[str, str | int] | None = None,
735
749
  retry: bool = False,
750
+ recover_local_connect: bool = True,
736
751
  ) -> T:
737
752
  self._ensure_open()
738
- self._prepare_endpoint()
739
753
  attempts = MAX_RETRY_ATTEMPTS if retry else 1
740
754
  last_transport_error: TransportError | None = None
741
755
  local_connect_recovery_used = False
@@ -746,13 +760,14 @@ class Client:
746
760
  except httpx.RequestError as error:
747
761
  last_transport_error = self._connection_error(operation)
748
762
  can_recover_local_connect = (
749
- self._config.local is not None
763
+ recover_local_connect
764
+ and self._config.managed_local
765
+ and self._config.auto_start_local_server
750
766
  and isinstance(error, (httpx.ConnectError, httpx.ConnectTimeout))
751
767
  and not local_connect_recovery_used
752
768
  )
753
769
  if can_recover_local_connect:
754
770
  local_connect_recovery_used = True
755
- self._local_ready = False
756
771
  self._ensure_local_available()
757
772
  if attempt + 1 == attempts:
758
773
  attempts += 1
@@ -760,7 +775,7 @@ class Client:
760
775
  raise last_transport_error from error
761
776
  else:
762
777
  self._observe_server_version(response)
763
- self._announce_http_endpoint()
778
+ self._announce_endpoint()
764
779
  if response.is_error:
765
780
  try:
766
781
  api_error = _parse_api_error(response)
@@ -794,32 +809,55 @@ class Client:
794
809
  raise AssertionError("Request loop ended without a result or error.")
795
810
  raise last_transport_error
796
811
 
812
+ def _repair_local_connection(self, error: TransportError) -> None:
813
+ """Repair an opted-in managed-local connection without replaying a request."""
814
+ cause = error.__cause__
815
+ if (
816
+ self._config.managed_local
817
+ and self._config.auto_start_local_server
818
+ and isinstance(cause, (httpx.ConnectError, httpx.ConnectTimeout))
819
+ ):
820
+ self._ensure_local_available()
821
+
797
822
  def _ensure_open(self) -> None:
798
823
  if self._closed:
799
824
  raise RuntimeError("Client is closed.")
800
825
 
801
826
  @property
802
827
  def _operation_endpoint_details(self) -> dict[str, object]:
803
- if self._config.local is None:
828
+ if self._config.url is not None:
804
829
  return {"url": self._config.url}
805
830
  return {
806
- "directory": str(self._config.local.directory),
807
- "socket": str(self._config.local.socket),
831
+ "labtasker_root": str(self._config.labtasker_root),
832
+ "socket": str(self._config.socket),
808
833
  }
809
834
 
810
- def _prepare_endpoint(self) -> None:
811
- if self._config.local is not None and not self._local_ready:
812
- self._ensure_local_available()
813
-
814
- def _announce_http_endpoint(self) -> None:
815
- if self._endpoint_announced or self._config.local is not None:
835
+ def _announce_endpoint(self) -> None:
836
+ if self._endpoint_announced:
816
837
  return
817
- assert self._config.url is not None
818
- transport = self._config.url.partition(":")[0]
819
- print(
820
- f"[labtasker] connected server=remote transport={transport} url={self._config.url}",
821
- file=sys.stderr,
822
- )
838
+ if self._config.url is not None:
839
+ transport = self._config.url.partition(":")[0]
840
+ message = (
841
+ f"[labtasker] connected server=remote transport={transport} url={self._config.url}"
842
+ )
843
+ elif self._config.managed_local:
844
+ assert self._config.local is not None and self._config.socket is not None
845
+ pid, version = self._local_ensure_result or (None, None)
846
+ message = (
847
+ "[labtasker] connected server=local transport=unix "
848
+ f"labtasker_root={self._config.labtasker_root} "
849
+ f"database={self._config.local.database} socket={self._config.socket}"
850
+ )
851
+ if pid is not None:
852
+ message += f" pid={pid}"
853
+ if version is not None:
854
+ message += f" version={version}"
855
+ else:
856
+ assert self._config.socket is not None
857
+ message = (
858
+ f"[labtasker] connected server=external transport=unix socket={self._config.socket}"
859
+ )
860
+ print(message, file=sys.stderr)
823
861
  self._endpoint_announced = True
824
862
 
825
863
  def _ensure_local_available(self) -> None:
@@ -827,16 +865,7 @@ class Client:
827
865
  if paths is None:
828
866
  return
829
867
  result = ensure_local_server(paths, emit=self._emit_local_transition)
830
- pid = result.pid if result.pid is not None else "unknown"
831
- version = result.server_version if result.server_version is not None else "unknown"
832
- print(
833
- "[labtasker] connected server=local transport=unix "
834
- f"directory={paths.directory} database={paths.database} socket={paths.socket} "
835
- f"pid={pid} version={version}",
836
- file=sys.stderr,
837
- )
838
- self._endpoint_announced = True
839
- self._local_ready = True
868
+ self._local_ensure_result = (result.pid, result.server_version)
840
869
 
841
870
  @staticmethod
842
871
  def _emit_local_transition(message: str) -> None:
@@ -844,16 +873,25 @@ class Client:
844
873
 
845
874
  def _connection_error(self, operation: str) -> TransportError:
846
875
  details: dict[str, object] = {"operation": operation}
847
- if self._config.local is None:
876
+ if self._config.url is not None:
848
877
  details["url"] = self._config.url
878
+ elif not self._config.managed_local:
879
+ details["socket"] = str(self._config.socket)
849
880
  else:
881
+ assert self._config.local is not None
850
882
  details.update(
851
883
  {
852
884
  "state": "unhealthy",
853
- "directory": str(self._config.local.directory),
885
+ "labtasker_root": str(self._config.labtasker_root),
854
886
  "database": str(self._config.local.database),
855
- "socket": str(self._config.local.socket),
887
+ "socket": str(self._config.socket),
856
888
  "log": str(self._config.local.log),
889
+ "remedies": [
890
+ "rerun with --auto-start-local-server",
891
+ "launch labtasker-server serve --connection socket --daemon "
892
+ f"--labtasker-root {self._config.labtasker_root}",
893
+ "configure LABTASKER_URL or LABTASKER_SOCKET",
894
+ ],
857
895
  }
858
896
  )
859
897
  return TransportError("The Labtasker Server could not be reached.", details)
@@ -29,15 +29,14 @@ from labtasker.tee import configure_worker_logger
29
29
  from labtasker.types import JSONValue
30
30
  from labtasker.validation import validate_identifier, validate_json_object
31
31
  from labtasker.worker import (
32
- POLL_INTERVAL_SECONDS,
33
32
  Heartbeat,
34
33
  _best_effort_unclaim,
35
34
  _ExecutionResult,
36
35
  _FailureGuard,
37
36
  _finish_journal,
38
- _generate_run_id,
39
37
  _guard_worker_topology,
40
38
  _journal_best_effort,
39
+ _next_claim,
41
40
  _preflight,
42
41
  _report_until_resolved,
43
42
  _safe_diagnostic_text,
@@ -58,6 +57,8 @@ def run_command_worker(
58
57
  force_stop_timeout: float | None = None,
59
58
  max_consecutive_failures: int = 5,
60
59
  metadata: dict[str, JSONValue] | None = None,
60
+ labtasker_root: Path | None = None,
61
+ auto_start_local_server: bool = False,
61
62
  ) -> None:
62
63
  guard = _FailureGuard(max_consecutive_failures)
63
64
  templates = compile_argv(argv)
@@ -70,29 +71,26 @@ def run_command_worker(
70
71
  _guard_command_worker_platform()
71
72
  _guard_worker_topology()
72
73
  configure_worker_logger()
73
- with Client(queue=queue) as client:
74
- queue_name = client.configuration.queue
74
+ with Client(
75
+ queue=queue,
76
+ labtasker_root=labtasker_root,
77
+ auto_start_local_server=auto_start_local_server,
78
+ ) as client:
79
+ queue_name = client._configuration.queue
75
80
  _preflight(client, queue_name)
76
81
  with ObservationReporter(
77
- client.configuration, normalized_route, normalized_metadata
82
+ client._configuration, normalized_route, normalized_metadata
78
83
  ) as observer:
79
- idle_deadline: float | None = None
80
84
  while True:
81
- claim = client._claim(
85
+ claim = _next_claim(
86
+ client,
82
87
  route=normalized_route,
83
- run_id=_generate_run_id(),
84
88
  queue=queue_name,
89
+ idle_timeout=normalized_idle_timeout,
85
90
  )
86
91
  if claim is None:
87
- now = time.monotonic()
88
- if idle_deadline is None:
89
- idle_deadline = now + normalized_idle_timeout
90
- if now >= idle_deadline:
91
- logger.info("Worker idle timeout reached; stopping normally.")
92
- return
93
- time.sleep(min(POLL_INTERVAL_SECONDS, idle_deadline - now))
94
- continue
95
- idle_deadline = None
92
+ logger.info("Worker idle timeout reached; stopping normally.")
93
+ return
96
94
  observer.activity(claim.task.id)
97
95
  logger.info(
98
96
  "Claimed Task %s as run %s (attempt %d, route %s).",
@@ -136,9 +134,10 @@ def _run_command_claim(
136
134
  try:
137
135
  journal = LocalRunJournal.create(
138
136
  claim=claim,
139
- endpoint=client.configuration.endpoint_dict(),
137
+ endpoint=client._configuration.endpoint_dict(),
140
138
  queue=queue,
141
139
  route=route,
140
+ labtasker_root=client._configuration.labtasker_root,
142
141
  )
143
142
  except Exception:
144
143
  _best_effort_unclaim(client, claim, queue)
@@ -510,18 +509,19 @@ def _command_environment(
510
509
  "LABTASKER_WORKER_ID": worker_id,
511
510
  }
512
511
  )
513
- configuration = client.configuration
514
- if configuration.local is None:
512
+ configuration = client._configuration
513
+ if configuration.url is not None:
515
514
  assert configuration.url is not None
516
515
  environment["LABTASKER_URL"] = configuration.url
517
516
  environment.pop("LABTASKER_SOCKET", None)
518
- environment.pop("LABTASKER_LOCAL_DIRECTORY", None)
519
517
  else:
520
- environment["LABTASKER_SOCKET"] = str(configuration.local.socket)
521
- environment["LABTASKER_LOCAL_DIRECTORY"] = str(configuration.local.directory)
518
+ assert configuration.socket is not None
519
+ environment["LABTASKER_SOCKET"] = str(configuration.socket)
522
520
  environment.pop("LABTASKER_URL", None)
521
+ environment.pop("LABTASKER_ROOT", None)
522
+ environment.pop("LABTASKER_LOCAL_DIRECTORY", None)
523
523
  token = configuration.token
524
- if token is None or configuration.local is not None:
524
+ if token is None or configuration.url is None:
525
525
  environment.pop("LABTASKER_TOKEN", None)
526
526
  else:
527
527
  environment["LABTASKER_TOKEN"] = token