labtasker-client 2.0.0__tar.gz → 2.1.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 (25) hide show
  1. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/PKG-INFO +1 -1
  2. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/pyproject.toml +1 -1
  3. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/__init__.py +1 -1
  4. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/binding.py +6 -2
  5. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/cli.py +29 -8
  6. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/command_worker.py +10 -2
  7. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/config.py +6 -0
  8. labtasker_client-2.1.0/src/labtasker/py.typed +0 -0
  9. labtasker_client-2.0.0/src/labtasker/py.typed +0 -1
  10. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/.gitignore +0 -0
  11. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/LICENSE +0 -0
  12. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/__main__.py +0 -0
  13. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/api.py +0 -0
  14. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/client.py +0 -0
  15. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/command_template.py +0 -0
  16. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/errors.py +0 -0
  17. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/execution.py +0 -0
  18. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/journal.py +0 -0
  19. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/local.py +0 -0
  20. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/models.py +0 -0
  21. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/paths.py +0 -0
  22. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/tee.py +0 -0
  23. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/types.py +0 -0
  24. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/validation.py +0 -0
  25. {labtasker_client-2.0.0 → labtasker_client-2.1.0}/src/labtasker/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labtasker-client
3
- Version: 2.0.0
3
+ Version: 2.1.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.0.0"
7
+ version = "2.1.0"
8
8
  description = "A small task queue for parallel model inference and evaluation"
9
9
  requires-python = ">=3.11"
10
10
  license = "Apache-2.0"
@@ -35,7 +35,7 @@ from labtasker.models import BulkUpdateResult, LastError, Queue, Task, TaskInfo,
35
35
  from labtasker.types import JSONValue, TaskOrderField, TaskStatus, TaskUpdate
36
36
  from labtasker.worker import loop
37
37
 
38
- __version__ = "2.0.0"
38
+ __version__ = "2.1.0"
39
39
 
40
40
  __all__ = [
41
41
  "APIError",
@@ -119,7 +119,7 @@ class CompiledBinding:
119
119
 
120
120
 
121
121
  def compile_binding(function: Callable[..., Any]) -> CompiledBinding:
122
- if inspect.iscoroutinefunction(function):
122
+ if _is_async_callable(function):
123
123
  raise TypeError("Labtasker v2 Worker handlers must be synchronous functions.")
124
124
  signature = inspect.signature(function)
125
125
  try:
@@ -170,7 +170,7 @@ def compile_binding(function: Callable[..., Any]) -> CompiledBinding:
170
170
  def _validate_resolver(resolver: object, name: str) -> None:
171
171
  if not callable(resolver):
172
172
  raise TypeError(f"TaskArg resolver for {name!r} must be callable.")
173
- if inspect.iscoroutinefunction(resolver):
173
+ if _is_async_callable(resolver):
174
174
  raise TypeError(f"TaskArg resolver for {name!r} must be synchronous.")
175
175
  try:
176
176
  signature = inspect.signature(resolver)
@@ -182,3 +182,7 @@ def _validate_resolver(resolver: object, name: str) -> None:
182
182
  inspect.Parameter.POSITIONAL_OR_KEYWORD,
183
183
  }:
184
184
  raise TypeError(f"TaskArg resolver for {name!r} must accept exactly one value.")
185
+
186
+
187
+ def _is_async_callable(value: object) -> bool:
188
+ return inspect.iscoroutinefunction(value) or inspect.iscoroutinefunction(type(value).__call__)
@@ -10,6 +10,7 @@ from pydantic import BaseModel
10
10
  from typer._click.core import Context as ClickContext
11
11
  from typer.core import TyperCommand
12
12
 
13
+ from labtasker import __version__
13
14
  from labtasker.client import Client
14
15
  from labtasker.command_template import TemplateSyntaxError
15
16
  from labtasker.command_worker import run_command_worker
@@ -50,6 +51,27 @@ app.add_typer(config_app, name="config")
50
51
  logger = logging.getLogger("labtasker.cli")
51
52
 
52
53
 
54
+ def _version_callback(value: bool) -> None:
55
+ if value:
56
+ typer.echo(f"labtasker-client {__version__}")
57
+ raise typer.Exit()
58
+
59
+
60
+ @app.callback()
61
+ def main(
62
+ version: Annotated[
63
+ bool,
64
+ typer.Option(
65
+ "--version",
66
+ callback=_version_callback,
67
+ is_eager=True,
68
+ help="Show the Client package version and exit.",
69
+ ),
70
+ ] = False,
71
+ ) -> None:
72
+ """Submit, inspect, and execute Labtasker v2 Tasks."""
73
+
74
+
53
75
  class _SeparatedCommand(TyperCommand):
54
76
  """Require the explicit boundary between Worker options and child argv."""
55
77
 
@@ -98,7 +120,7 @@ def worker_loop(
98
120
 
99
121
  Example:
100
122
 
101
- 
123
+ \b
102
124
  labtasker loop --route train -- \\
103
125
  python train.py --seed '%{seed}' --lr '%{optimizer.lr}'
104
126
  """
@@ -174,7 +196,7 @@ def task_submit(
174
196
 
175
197
  Example:
176
198
 
177
- 
199
+ \b
178
200
  labtasker task submit --name baseline \\
179
201
  --args '{"seed":1,"enabled":true}' \\
180
202
  --metadata '{"group":"paper"}' --route train
@@ -250,7 +272,7 @@ def task_list(
250
272
 
251
273
  Example:
252
274
 
253
- 
275
+ \b
254
276
  labtasker task list --status pending \\
255
277
  --filter 'priority >= 10 and metadata.group == "paper"' \\
256
278
  --order-by priority --descending --limit 100
@@ -295,7 +317,7 @@ def task_count(
295
317
 
296
318
  Example:
297
319
 
298
- 
320
+ \b
299
321
  labtasker task count --status failed \\
300
322
  --filter 'last_error.type == "ValueError"'
301
323
  """
@@ -339,7 +361,7 @@ def task_update(
339
361
 
340
362
  Examples:
341
363
 
342
- 
364
+ \b
343
365
  labtasker task update t_ABCDEFGHIJKL \\
344
366
  --changes '{"priority":20}'
345
367
  labtasker task update --filter 'status == "pending"' \\
@@ -465,7 +487,7 @@ def _invoke(operation: Callable[[], T]) -> T:
465
487
  try:
466
488
  return operation()
467
489
  except LabtaskerError as error:
468
- _write_json(error.as_envelope(), error=True)
490
+ _write_json(error.as_envelope())
469
491
  raise typer.Exit(1) from error
470
492
  except RequestValidationError as error:
471
493
  raise typer.BadParameter(str(error)) from error
@@ -494,13 +516,12 @@ def _json_object(value: str, *, option: str) -> dict[str, Any]:
494
516
  raise typer.BadParameter(f"{option} must be one strict JSON object: {error}") from error
495
517
 
496
518
 
497
- def _write_json(value: object, *, error: bool = False) -> None:
519
+ def _write_json(value: object) -> None:
498
520
  if isinstance(value, BaseModel):
499
521
  value = value.model_dump(mode="json")
500
522
  elif isinstance(value, list) and all(isinstance(item, BaseModel) for item in value):
501
523
  value = [item.model_dump(mode="json") for item in value]
502
524
  typer.echo(
503
525
  json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n",
504
- err=error,
505
526
  nl=False,
506
527
  )
@@ -244,13 +244,21 @@ def _start_drain(
244
244
  name: str,
245
245
  ) -> threading.Thread:
246
246
  def drain() -> None:
247
+ relay = True
247
248
  while True:
248
- chunk = source.read(65536)
249
+ chunk = os.read(source.fileno(), 65536)
249
250
  if not chunk:
250
251
  return
251
252
  with lock:
252
253
  log.write(chunk)
253
- _write_bytes(destination, chunk)
254
+ if relay:
255
+ try:
256
+ _write_bytes(destination, chunk)
257
+ except Exception:
258
+ # The child must never block on a full pipe merely because the
259
+ # caller closed or replaced its output stream. Keep draining and
260
+ # journaling after live relay becomes unavailable.
261
+ relay = False
254
262
 
255
263
  thread = threading.Thread(target=drain, name=f"labtasker-command-{name}", daemon=True)
256
264
  thread.start()
@@ -190,4 +190,10 @@ def _validate_token(value: str | None, *, source: str) -> str | None:
190
190
  return None
191
191
  if not isinstance(value, str) or not value:
192
192
  raise invalid_config("Token must be a non-empty string.", source=source, field="token")
193
+ if any(not 0x21 <= ord(character) <= 0x7E for character in value):
194
+ raise invalid_config(
195
+ "Token must contain only visible ASCII characters.",
196
+ source=source,
197
+ field="token",
198
+ )
193
199
  return value
File without changes
@@ -1 +0,0 @@
1
-