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.
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/PKG-INFO +1 -1
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/pyproject.toml +1 -1
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/__init__.py +1 -1
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/app.py +1 -1
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/cli.py +1 -1
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/config.py +12 -2
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/filtering.py +50 -13
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/local.py +20 -2
- labtasker_server-2.0.1/src/labtasker_server/py.typed +0 -0
- labtasker_server-2.0.0/src/labtasker_server/py.typed +0 -1
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/.gitignore +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/LICENSE +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/__main__.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/database.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/errors.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/logging.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/middleware.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/migrations/__init__.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/migrations/env.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/migrations/versions/0001_initial.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/migrations/versions/__init__.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/models.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/pagination.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/schemas.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/services/__init__.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/services/queues.py +0 -0
- {labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/services/tasks.py +0 -0
- {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.
|
|
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
|
|
@@ -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)]
|
|
@@ -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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labtasker_server-2.0.0 → labtasker_server-2.0.1}/src/labtasker_server/migrations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|