labtasker-server 2.0.0__tar.gz → 2.0.1__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 (28) hide show
  1. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/PKG-INFO +1 -1
  2. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/pyproject.toml +1 -1
  3. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/__init__.py +1 -1
  4. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/app.py +1 -1
  5. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/cli.py +1 -1
  6. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/config.py +12 -2
  7. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/filtering.py +50 -13
  8. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/local.py +20 -2
  9. labtasker_server-2.0.1/src/labtasker_server/py.typed +0 -0
  10. labtasker_server-2.0.0/src/labtasker_server/py.typed +0 -1
  11. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/.gitignore +0 -0
  12. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/LICENSE +0 -0
  13. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/__main__.py +0 -0
  14. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/database.py +0 -0
  15. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/errors.py +0 -0
  16. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/logging.py +0 -0
  17. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/middleware.py +0 -0
  18. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/migrations/__init__.py +0 -0
  19. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/migrations/env.py +0 -0
  20. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/migrations/versions/0001_initial.py +0 -0
  21. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/migrations/versions/__init__.py +0 -0
  22. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/models.py +0 -0
  23. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/pagination.py +0 -0
  24. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/schemas.py +0 -0
  25. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/services/__init__.py +0 -0
  26. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/services/queues.py +0 -0
  27. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/services/tasks.py +0 -0
  28. {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labtasker-server
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: SQLite/FastAPI server 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-server"
7
- version = "2.0.0"
7
+ version = "2.0.1"
8
8
  description = "SQLite/FastAPI server for parallel model inference and evaluation"
9
9
  requires-python = ">=3.11"
10
10
  license = "Apache-2.0"
@@ -1,3 +1,3 @@
1
1
  """Labtasker v2 HTTP server package."""
2
2
 
3
- __version__ = "2.0.0"
3
+ __version__ = "2.0.1"
@@ -142,7 +142,7 @@ def create_app(
142
142
  return
143
143
  if credentials is None or credentials.scheme.lower() != "bearer":
144
144
  raise _unauthorized()
145
- if not hmac.compare_digest(credentials.credentials, token):
145
+ if not hmac.compare_digest(credentials.credentials.encode(), token.encode("ascii")):
146
146
  raise _unauthorized()
147
147
 
148
148
  authenticated = [Depends(require_auth)]
@@ -75,7 +75,7 @@ def serve(
75
75
 
76
76
  Examples:
77
77
 
78
- 
78
+ \b
79
79
  labtasker-server serve
80
80
  LABTASKER_SERVER_TOKEN=secret labtasker-server serve \\
81
81
  --host 0.0.0.0 --database /data/labtasker.db
@@ -14,6 +14,9 @@ class ServerSettings:
14
14
  token: str | None = None
15
15
  database_fd: int | None = None
16
16
 
17
+ def __post_init__(self) -> None:
18
+ _validate_token(self.token)
19
+
17
20
  @classmethod
18
21
  def from_values(
19
22
  cls,
@@ -24,8 +27,6 @@ class ServerSettings:
24
27
  token: str | None = None,
25
28
  ) -> ServerSettings:
26
29
  effective_token = os.environ.get("LABTASKER_SERVER_TOKEN") if token is None else token
27
- if effective_token == "":
28
- raise ValueError("LABTASKER_SERVER_TOKEN must not be empty.")
29
30
  if not 1 <= port <= 65535:
30
31
  raise ValueError("port must be between 1 and 65535.")
31
32
  if effective_token is None and not _is_tokenless_host_allowed(host):
@@ -33,6 +34,15 @@ class ServerSettings:
33
34
  return cls(host=host, port=port, database=Path(database), token=effective_token)
34
35
 
35
36
 
37
+ def _validate_token(token: str | None) -> None:
38
+ if token is not None and not isinstance(token, str):
39
+ raise ValueError("LABTASKER_SERVER_TOKEN must be a string.")
40
+ if token == "":
41
+ raise ValueError("LABTASKER_SERVER_TOKEN must not be empty.")
42
+ if token is not None and any(not 0x21 <= ord(character) <= 0x7E for character in token):
43
+ raise ValueError("LABTASKER_SERVER_TOKEN must contain only visible ASCII characters.")
44
+
45
+
36
46
  def _is_tokenless_host_allowed(host: str) -> bool:
37
47
  if host.lower() == "localhost":
38
48
  return True
@@ -445,17 +445,21 @@ def _compile_membership(node: Membership) -> ColumnElement[bool]:
445
445
  raise _invalid_filter("Use a scalar literal on the left to test route membership.")
446
446
  if runtime.kind == "fixed" or runtime.declared_type is not None:
447
447
  normalized = [_normalize_declared_literal(runtime, value) for value in node.values]
448
- matches = (
449
- or_(*[_declared_comparison(runtime, "==", value) for value in normalized])
450
- if normalized
451
- else false()
452
- )
448
+ if runtime.kind == "fixed":
449
+ candidates = _unique_scalars([value for value in normalized if value is not None])
450
+ fixed_matches: list[ColumnElement[bool]] = []
451
+ if any(value is None for value in normalized):
452
+ fixed_matches.append(runtime.value.is_(None))
453
+ if candidates:
454
+ fixed_matches.append(
455
+ and_(runtime.value.is_not(None), runtime.value.in_(candidates))
456
+ )
457
+ matches = or_(*fixed_matches) if fixed_matches else false()
458
+ else:
459
+ matches = _json_in(runtime.value, runtime.json_type, normalized)
453
460
  present = true() if runtime.kind == "fixed" else runtime.json_type.is_not(None)
454
461
  else:
455
- comparisons = [
456
- _json_equal(runtime.value, runtime.json_type, value) for value in node.values
457
- ]
458
- matches = or_(*comparisons) if comparisons else false()
462
+ matches = _json_in(runtime.value, runtime.json_type, list(node.values))
459
463
  present = runtime.json_type.in_(["null", "true", "false", "integer", "real", "text"])
460
464
  return and_(present, not_(matches) if node.operator == "not in" else matches)
461
465
 
@@ -499,17 +503,50 @@ def _json_equal(value_expression: Any, type_expression: Any, value: Scalar) -> C
499
503
  return and_(type_expression == "text", value_expression == value)
500
504
 
501
505
 
506
+ def _json_in(
507
+ value_expression: Any,
508
+ type_expression: Any,
509
+ values: list[Scalar | int],
510
+ ) -> ColumnElement[bool]:
511
+ matches: list[ColumnElement[bool]] = []
512
+ if any(value is None for value in values):
513
+ matches.append(type_expression == "null")
514
+ if any(value is True for value in values):
515
+ matches.append(type_expression == "true")
516
+ if any(value is False for value in values):
517
+ matches.append(type_expression == "false")
518
+ numbers = _unique_scalars(
519
+ [
520
+ value
521
+ for value in values
522
+ if isinstance(value, (int, float)) and not isinstance(value, bool)
523
+ ]
524
+ )
525
+ if numbers:
526
+ matches.append(
527
+ and_(type_expression.in_(["integer", "real"]), value_expression.in_(numbers))
528
+ )
529
+ strings = _unique_scalars([value for value in values if isinstance(value, str)])
530
+ if strings:
531
+ matches.append(and_(type_expression == "text", value_expression.in_(strings)))
532
+ return or_(*matches) if matches else false()
533
+
534
+
535
+ def _unique_scalars(values: list[Scalar | int]) -> list[Scalar | int]:
536
+ return list(dict.fromkeys(values))
537
+
538
+
502
539
  def _timestamp_us(value: str) -> int:
503
540
  if not RFC3339_RE.fullmatch(value):
504
541
  raise _invalid_filter("Timestamp literal must be a strict RFC 3339 string.")
505
542
  normalized = value[:-1] + "+00:00" if value.endswith("Z") else value
506
543
  try:
507
544
  parsed = datetime.fromisoformat(normalized)
508
- except ValueError as error:
545
+ if parsed.utcoffset() is None:
546
+ raise _invalid_filter("Timestamp literal must include a UTC offset.")
547
+ delta = parsed.astimezone(UTC) - datetime(1970, 1, 1, tzinfo=UTC)
548
+ except (ValueError, OverflowError) as error:
509
549
  raise _invalid_filter("Timestamp literal must be a valid RFC 3339 time.") from error
510
- if parsed.utcoffset() is None:
511
- raise _invalid_filter("Timestamp literal must include a UTC offset.")
512
- delta = parsed.astimezone(UTC) - datetime(1970, 1, 1, tzinfo=UTC)
513
550
  return (delta.days * 86_400 + delta.seconds) * 1_000_000 + delta.microseconds
514
551
 
515
552
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import json
5
+ import math
5
6
  import os
6
7
  import secrets
7
8
  import socket
@@ -209,13 +210,30 @@ def read_metadata(paths: LocalPaths) -> RuntimeMetadata | None:
209
210
  except (OSError, ValueError, TypeError, json.JSONDecodeError):
210
211
  return None
211
212
  if (
212
- metadata.metadata_version != METADATA_VERSION
213
+ not isinstance(metadata.metadata_version, int)
214
+ or isinstance(metadata.metadata_version, bool)
215
+ or metadata.metadata_version != METADATA_VERSION
216
+ or not isinstance(metadata.generation, str)
217
+ or not metadata.generation
218
+ or not isinstance(metadata.role, str)
213
219
  or metadata.role not in {"coordinator", "daemon"}
220
+ or not isinstance(metadata.pid, int)
221
+ or isinstance(metadata.pid, bool)
214
222
  or metadata.pid <= 0
215
- or not metadata.generation
223
+ or not isinstance(metadata.process_start_marker, str)
224
+ or not metadata.process_start_marker
225
+ or not isinstance(metadata.directory, str)
216
226
  or metadata.directory != str(paths.directory)
227
+ or not isinstance(metadata.database, str)
217
228
  or metadata.database != str(paths.database)
229
+ or not isinstance(metadata.database_device, int)
230
+ or isinstance(metadata.database_device, bool)
231
+ or not isinstance(metadata.database_inode, int)
232
+ or isinstance(metadata.database_inode, bool)
218
233
  or not isinstance(metadata.automatic_attempt_at, (int, float))
234
+ or isinstance(metadata.automatic_attempt_at, bool)
235
+ or not math.isfinite(metadata.automatic_attempt_at)
236
+ or not (metadata.server_version is None or isinstance(metadata.server_version, str))
219
237
  ):
220
238
  return None
221
239
  return metadata
File without changes