screamingface 0.1.0__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.
- screamingface/__init__.py +109 -0
- screamingface/_candidate_policy.py +80 -0
- screamingface/_client_connections.py +123 -0
- screamingface/_core/__init__.py +3 -0
- screamingface/_core/ports.py +59 -0
- screamingface/_core/wire.py +36 -0
- screamingface/_default_client.py +181 -0
- screamingface/_engine/__init__.py +3 -0
- screamingface/_engine/access_contract.py +206 -0
- screamingface/_engine/auth.py +465 -0
- screamingface/_engine/auth_base.py +32 -0
- screamingface/_engine/benchmark.py +158 -0
- screamingface/_engine/catalog.py +288 -0
- screamingface/_engine/catalog_contract.py +145 -0
- screamingface/_engine/connections.py +396 -0
- screamingface/_engine/contract.py +536 -0
- screamingface/_engine/model_parameters.py +233 -0
- screamingface/_engine/run_lifecycle.py +70 -0
- screamingface/_engine/transport.py +662 -0
- screamingface/_evaluation/__init__.py +3 -0
- screamingface/_evaluation/benchmark.py +76 -0
- screamingface/_evaluation/candidate.py +411 -0
- screamingface/_evaluation/compilation.py +71 -0
- screamingface/_evaluation/linking.py +100 -0
- screamingface/_evaluation/model.py +330 -0
- screamingface/_evaluation/model_parameters.py +88 -0
- screamingface/_evaluation/policy.py +32 -0
- screamingface/_evaluation/progress.py +142 -0
- screamingface/_evaluation/results.py +482 -0
- screamingface/_evaluation/runner.py +352 -0
- screamingface/_evaluation/topology.py +188 -0
- screamingface/_evaluation/url4.py +322 -0
- screamingface/_immutable_json.py +63 -0
- screamingface/_named_values.py +88 -0
- screamingface/_operation_projection.py +54 -0
- screamingface/_report_primitives.py +198 -0
- screamingface/_scoreboard/__init__.py +3 -0
- screamingface/_scoreboard/leaderboards.py +531 -0
- screamingface/_ui/__init__.py +3 -0
- screamingface/_ui/assets/provider_icons/NOTICE.md +36 -0
- screamingface/_ui/assets/provider_icons/anthropic/icon-dark.svg +1 -0
- screamingface/_ui/assets/provider_icons/anthropic/icon.svg +1 -0
- screamingface/_ui/assets/provider_icons/gemini-cli/icon.svg +1 -0
- screamingface/_ui/assets/provider_icons/huggingface/icon.svg +8 -0
- screamingface/_ui/assets/provider_icons/ollama/icon-dark.svg +7 -0
- screamingface/_ui/assets/provider_icons/ollama/icon.svg +7 -0
- screamingface/_ui/assets/provider_icons/openrouter/icon-dark.svg +21 -0
- screamingface/_ui/assets/provider_icons/openrouter/icon.svg +21 -0
- screamingface/_ui/card_style.py +95 -0
- screamingface/_ui/cards.py +198 -0
- screamingface/_ui/catalog.py +133 -0
- screamingface/_ui/connection_state.py +57 -0
- screamingface/_ui/connection_view.py +468 -0
- screamingface/_ui/connections.py +446 -0
- screamingface/_ui/evaluation_state.py +258 -0
- screamingface/_ui/evaluation_view.py +312 -0
- screamingface/_ui/leaderboard_style.py +142 -0
- screamingface/_ui/leaderboard_view.py +251 -0
- screamingface/_ui/provider_icons.py +48 -0
- screamingface/_ui/report_view.py +805 -0
- screamingface/_ui/style.py +76 -0
- screamingface/benchmarks.py +21 -0
- screamingface/case_result.py +492 -0
- screamingface/client.py +589 -0
- screamingface/connections.py +243 -0
- screamingface/discovery.py +417 -0
- screamingface/errors.py +187 -0
- screamingface/events.py +269 -0
- screamingface/fusion.py +67 -0
- screamingface/leaderboard.py +276 -0
- screamingface/leaderboards.py +35 -0
- screamingface/model.py +68 -0
- screamingface/models.py +21 -0
- screamingface/operation.py +104 -0
- screamingface/pipeline.py +72 -0
- screamingface/py.typed +1 -0
- screamingface/recipe.py +78 -0
- screamingface/report.py +554 -0
- screamingface/url4.py +328 -0
- screamingface/warnings.py +12 -0
- screamingface-0.1.0.dist-info/METADATA +512 -0
- screamingface-0.1.0.dist-info/RECORD +84 -0
- screamingface-0.1.0.dist-info/WHEEL +4 -0
- screamingface-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""ScreamingFace — evaluate composable Candidate Recipes on research Benchmarks."""
|
|
2
|
+
|
|
3
|
+
from screamingface import benchmarks, connections, events, leaderboards, models
|
|
4
|
+
from screamingface._default_client import close, configure, connect, disconnect, evaluate
|
|
5
|
+
from screamingface._ui.connections import ConnectionPanel
|
|
6
|
+
from screamingface.client import AsyncClient, Client
|
|
7
|
+
from screamingface.connections import AsyncOAuthFlow, Connection, OAuthFlow
|
|
8
|
+
from screamingface.discovery import (
|
|
9
|
+
Benchmark,
|
|
10
|
+
BenchmarkInfo,
|
|
11
|
+
ModelCapability,
|
|
12
|
+
ModelDetails,
|
|
13
|
+
ModelInfo,
|
|
14
|
+
ModelParameter,
|
|
15
|
+
ModelParameterSchema,
|
|
16
|
+
)
|
|
17
|
+
from screamingface.errors import (
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
EngineUnavailableError,
|
|
20
|
+
ExecutionError,
|
|
21
|
+
LeaderboardError,
|
|
22
|
+
PlanningError,
|
|
23
|
+
ProviderConnectionError,
|
|
24
|
+
ScreamingFaceError,
|
|
25
|
+
)
|
|
26
|
+
from screamingface.events import Event
|
|
27
|
+
from screamingface.fusion import Fusion
|
|
28
|
+
from screamingface.leaderboard import (
|
|
29
|
+
Leaderboard,
|
|
30
|
+
LeaderboardBaseline,
|
|
31
|
+
LeaderboardEntry,
|
|
32
|
+
LeaderboardInfo,
|
|
33
|
+
LeaderboardScore,
|
|
34
|
+
)
|
|
35
|
+
from screamingface.model import Model
|
|
36
|
+
from screamingface.operation import OperationInfo
|
|
37
|
+
from screamingface.pipeline import Pipeline
|
|
38
|
+
from screamingface.recipe import Recipe
|
|
39
|
+
from screamingface.report import (
|
|
40
|
+
CandidateResult,
|
|
41
|
+
CaseGrade,
|
|
42
|
+
CaseResult,
|
|
43
|
+
Check,
|
|
44
|
+
Evidence,
|
|
45
|
+
EvidenceProducer,
|
|
46
|
+
Failure,
|
|
47
|
+
MemberResult,
|
|
48
|
+
Report,
|
|
49
|
+
Usage,
|
|
50
|
+
)
|
|
51
|
+
from screamingface.url4 import Url4
|
|
52
|
+
from screamingface.warnings import CoverageWarning, EvaluationWarning
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"AsyncClient",
|
|
56
|
+
"AuthenticationError",
|
|
57
|
+
"Benchmark",
|
|
58
|
+
"BenchmarkInfo",
|
|
59
|
+
"CaseGrade",
|
|
60
|
+
"CaseResult",
|
|
61
|
+
"CandidateResult",
|
|
62
|
+
"Check",
|
|
63
|
+
"Client",
|
|
64
|
+
"Connection",
|
|
65
|
+
"ConnectionPanel",
|
|
66
|
+
"AsyncOAuthFlow",
|
|
67
|
+
"CoverageWarning",
|
|
68
|
+
"close",
|
|
69
|
+
"configure",
|
|
70
|
+
"connect",
|
|
71
|
+
"connections",
|
|
72
|
+
"disconnect",
|
|
73
|
+
"Evidence",
|
|
74
|
+
"EvidenceProducer",
|
|
75
|
+
"Event",
|
|
76
|
+
"EngineUnavailableError",
|
|
77
|
+
"ExecutionError",
|
|
78
|
+
"EvaluationWarning",
|
|
79
|
+
"evaluate",
|
|
80
|
+
"Failure",
|
|
81
|
+
"Fusion",
|
|
82
|
+
"Leaderboard",
|
|
83
|
+
"LeaderboardBaseline",
|
|
84
|
+
"LeaderboardEntry",
|
|
85
|
+
"LeaderboardError",
|
|
86
|
+
"LeaderboardInfo",
|
|
87
|
+
"LeaderboardScore",
|
|
88
|
+
"MemberResult",
|
|
89
|
+
"Model",
|
|
90
|
+
"ModelCapability",
|
|
91
|
+
"ModelDetails",
|
|
92
|
+
"ModelInfo",
|
|
93
|
+
"ModelParameter",
|
|
94
|
+
"ModelParameterSchema",
|
|
95
|
+
"OperationInfo",
|
|
96
|
+
"OAuthFlow",
|
|
97
|
+
"PlanningError",
|
|
98
|
+
"Pipeline",
|
|
99
|
+
"ProviderConnectionError",
|
|
100
|
+
"Recipe",
|
|
101
|
+
"Report",
|
|
102
|
+
"ScreamingFaceError",
|
|
103
|
+
"Usage",
|
|
104
|
+
"Url4",
|
|
105
|
+
"benchmarks",
|
|
106
|
+
"events",
|
|
107
|
+
"leaderboards",
|
|
108
|
+
"models",
|
|
109
|
+
]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Validated immutable Candidate generation overrides."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
|
|
9
|
+
type GenerationScalar = str | int | float | bool
|
|
10
|
+
type GenerationParams = Mapping[str, GenerationScalar]
|
|
11
|
+
|
|
12
|
+
_EXECUTION_OWNED_PARAMS = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"input",
|
|
15
|
+
"messages",
|
|
16
|
+
"model",
|
|
17
|
+
"plugins",
|
|
18
|
+
"provider",
|
|
19
|
+
"q",
|
|
20
|
+
"stream",
|
|
21
|
+
"tool_choice",
|
|
22
|
+
"tools",
|
|
23
|
+
"web_search",
|
|
24
|
+
"web_search_exclude",
|
|
25
|
+
"web_search_excluded_domains",
|
|
26
|
+
"web_search_policy",
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
"""Transport, routing, tool, and Benchmark protocol fields users cannot override."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def prompt(value: object | None, label: str) -> str | None:
|
|
33
|
+
if value is None:
|
|
34
|
+
return None
|
|
35
|
+
if not isinstance(value, str):
|
|
36
|
+
raise TypeError(f"{label} must be a string or None")
|
|
37
|
+
if not value.strip():
|
|
38
|
+
raise ValueError(f"{label} must not be empty")
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def params(value: object | None, label: str) -> GenerationParams:
|
|
43
|
+
if value is None:
|
|
44
|
+
return MappingProxyType({})
|
|
45
|
+
if not isinstance(value, Mapping):
|
|
46
|
+
raise TypeError(f"{label} must be a mapping or None")
|
|
47
|
+
selected: dict[str, GenerationScalar] = {}
|
|
48
|
+
for name, item in value.items():
|
|
49
|
+
selected_name = _parameter_name(name, label)
|
|
50
|
+
selected[selected_name] = _parameter_value(selected_name, item, label)
|
|
51
|
+
return MappingProxyType(selected)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parameter_name(value: object, label: str) -> str:
|
|
55
|
+
if not isinstance(value, str) or not value.strip():
|
|
56
|
+
raise ValueError(f"{label} names must be non-empty strings")
|
|
57
|
+
if not value.isascii() or not all(
|
|
58
|
+
character.isalnum() or character in "._" for character in value
|
|
59
|
+
):
|
|
60
|
+
raise ValueError(f"{label} parameter {value!r} cannot be encoded")
|
|
61
|
+
if value in _EXECUTION_OWNED_PARAMS:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"{label} parameter {value!r} is reserved for Benchmark and Engine execution"
|
|
64
|
+
)
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parameter_value(name: str, value: object, label: str) -> GenerationScalar:
|
|
69
|
+
if not isinstance(value, str | int | float | bool):
|
|
70
|
+
raise TypeError(f"{label} value {name!r} must be a scalar")
|
|
71
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
72
|
+
raise ValueError(f"{label} value {name!r} must be finite")
|
|
73
|
+
if isinstance(value, str) and any(
|
|
74
|
+
character in "';(),&" or ord(character) < 32 or ord(character) == 127 for character in value
|
|
75
|
+
):
|
|
76
|
+
raise ValueError(f"{label} value {name!r} cannot be encoded")
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Shared Client origin, auth-listener, and provider-connection support."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
8
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
9
|
+
|
|
10
|
+
from screamingface.errors import ProviderConnectionError
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from screamingface._engine.connections import AsyncConnections, Connections
|
|
14
|
+
from screamingface.connections import AsyncOAuthFlow, Connection, OAuthFlow
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _AuthListeners:
|
|
18
|
+
"""Thread-safe observer set shared by the sync and async Client facades."""
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self._lock = threading.Lock()
|
|
22
|
+
self._callbacks: set[Callable[[], None]] = set()
|
|
23
|
+
|
|
24
|
+
def subscribe(self, callback: Callable[[], None]) -> Callable[[], None]:
|
|
25
|
+
with self._lock:
|
|
26
|
+
self._callbacks.add(callback)
|
|
27
|
+
|
|
28
|
+
def unsubscribe() -> None:
|
|
29
|
+
with self._lock:
|
|
30
|
+
self._callbacks.discard(callback)
|
|
31
|
+
|
|
32
|
+
return unsubscribe
|
|
33
|
+
|
|
34
|
+
def notify(self) -> None:
|
|
35
|
+
with self._lock:
|
|
36
|
+
callbacks = tuple(self._callbacks)
|
|
37
|
+
for callback in callbacks:
|
|
38
|
+
callback()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _engine_origin(value: Any) -> str:
|
|
42
|
+
return _http_origin(value, "engine_url")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _scoreboard_origin(value: Any) -> str:
|
|
46
|
+
return _http_origin(value, "scoreboard_url")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _http_origin(value: Any, name: str) -> str:
|
|
50
|
+
if not isinstance(value, str) or not value.strip():
|
|
51
|
+
raise ValueError(f"{name} must be an HTTP(S) origin")
|
|
52
|
+
parts = urlsplit(value.strip())
|
|
53
|
+
if (
|
|
54
|
+
parts.scheme not in {"http", "https"}
|
|
55
|
+
or not parts.netloc
|
|
56
|
+
or parts.path not in {"", "/"}
|
|
57
|
+
or parts.query
|
|
58
|
+
or parts.fragment
|
|
59
|
+
or parts.username is not None
|
|
60
|
+
or parts.password is not None
|
|
61
|
+
):
|
|
62
|
+
raise ValueError(f"{name} must be an HTTP(S) origin without credentials or a path")
|
|
63
|
+
return urlunsplit((parts.scheme, parts.netloc, "", "", ""))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _require_secure_connection_origin(engine_url: str) -> None:
|
|
67
|
+
parts = urlsplit(engine_url)
|
|
68
|
+
if parts.scheme == "https" or (
|
|
69
|
+
parts.scheme == "http" and parts.hostname in {"localhost", "127.0.0.1", "::1"}
|
|
70
|
+
):
|
|
71
|
+
return
|
|
72
|
+
raise ProviderConnectionError(
|
|
73
|
+
"Provider connection operations require HTTPS outside a loopback SF Engine",
|
|
74
|
+
code="secure_transport_required",
|
|
75
|
+
permanent=True,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _connect_sync(
|
|
80
|
+
connections: Connections,
|
|
81
|
+
provider: str,
|
|
82
|
+
api_key: str | None,
|
|
83
|
+
method: Literal["api_key", "oauth"] | None,
|
|
84
|
+
) -> Connection | OAuthFlow:
|
|
85
|
+
_validate_auth_method(method)
|
|
86
|
+
if api_key is not None:
|
|
87
|
+
_require_api_key_action(method)
|
|
88
|
+
return connections.connect(provider, api_key)
|
|
89
|
+
if method == "api_key":
|
|
90
|
+
raise ValueError("api_key is required for API-key authentication")
|
|
91
|
+
if method == "oauth":
|
|
92
|
+
return connections.start_oauth(provider)
|
|
93
|
+
raise ValueError("api_key is required unless method='oauth' is selected")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def _connect_async(
|
|
97
|
+
connections: AsyncConnections,
|
|
98
|
+
provider: str,
|
|
99
|
+
api_key: str | None,
|
|
100
|
+
method: Literal["api_key", "oauth"] | None,
|
|
101
|
+
) -> Connection | AsyncOAuthFlow:
|
|
102
|
+
_validate_auth_method(method)
|
|
103
|
+
if api_key is not None:
|
|
104
|
+
_require_api_key_action(method)
|
|
105
|
+
return await connections.connect(provider, api_key)
|
|
106
|
+
if method == "api_key":
|
|
107
|
+
raise ValueError("api_key is required for API-key authentication")
|
|
108
|
+
if method == "oauth":
|
|
109
|
+
return await connections.start_oauth(provider)
|
|
110
|
+
raise ValueError("api_key is required unless method='oauth' is selected")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _validate_auth_method(method: object) -> None:
|
|
114
|
+
if method not in {None, "api_key", "oauth"}:
|
|
115
|
+
raise ValueError("method must be 'api_key', 'oauth', or None")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _require_api_key_action(method: object) -> None:
|
|
119
|
+
if method == "oauth":
|
|
120
|
+
raise ValueError("api_key cannot be combined with OAuth")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Client-owned ports for SF Engine integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import TYPE_CHECKING, Protocol
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from screamingface._evaluation.model import Candidate
|
|
12
|
+
from screamingface.events import Event
|
|
13
|
+
from screamingface.report import Usage
|
|
14
|
+
|
|
15
|
+
type SyncEventObserver = Callable[[Event], None]
|
|
16
|
+
type AsyncEventObserver = Callable[[Event], None | Awaitable[None]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class _RunOutcome:
|
|
21
|
+
"""Transport-neutral root result retained for strict Report decoding."""
|
|
22
|
+
|
|
23
|
+
run_id: str
|
|
24
|
+
started_at: datetime
|
|
25
|
+
completed_at: datetime
|
|
26
|
+
result_body: str
|
|
27
|
+
media_type: str | None
|
|
28
|
+
root_usage: Usage | None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SyncRunTransport(Protocol):
|
|
32
|
+
"""Execute one inspected Candidate through an SF Engine."""
|
|
33
|
+
|
|
34
|
+
def run(
|
|
35
|
+
self,
|
|
36
|
+
candidate: Candidate,
|
|
37
|
+
on_event: SyncEventObserver | None,
|
|
38
|
+
) -> _RunOutcome: ...
|
|
39
|
+
|
|
40
|
+
def cancel_active(self) -> None: ...
|
|
41
|
+
|
|
42
|
+
def close(self) -> None: ...
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AsyncRunTransport(Protocol):
|
|
46
|
+
"""Asynchronous counterpart of :class:`SyncRunTransport`."""
|
|
47
|
+
|
|
48
|
+
async def run(
|
|
49
|
+
self,
|
|
50
|
+
candidate: Candidate,
|
|
51
|
+
on_event: AsyncEventObserver | None,
|
|
52
|
+
) -> _RunOutcome: ...
|
|
53
|
+
|
|
54
|
+
async def cancel_active(self) -> None: ...
|
|
55
|
+
|
|
56
|
+
async def close(self) -> None: ...
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Small, policy-free primitives for decoding external wire values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from typing import NoReturn, cast
|
|
7
|
+
|
|
8
|
+
type _Invalid = Callable[[str], NoReturn]
|
|
9
|
+
|
|
10
|
+
# httpx request-extension key marking a request as safe to re-send after an interactive
|
|
11
|
+
# Cloudflare Access login.
|
|
12
|
+
# INVARIANT: default-deny. Replay safety is a property of the REQUEST, not of the response
|
|
13
|
+
# status, so a call site that forgets the marker pays one extra round trip — never an extra
|
|
14
|
+
# paid Run. "GET /?q=" starts billable work despite being a GET and is never marked.
|
|
15
|
+
# WHY here: this module has no dependencies, so marking a call site never drags the Access
|
|
16
|
+
# stack (and its native crypto) into an import path that must stay cheap.
|
|
17
|
+
_REPLAY_SAFE = "screamingface_replay_safe"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def mapping(value: object, label: str, invalid: _Invalid) -> Mapping[str, object]:
|
|
21
|
+
"""Return a JSON-like mapping or delegate the domain-specific failure."""
|
|
22
|
+
|
|
23
|
+
if not isinstance(value, Mapping):
|
|
24
|
+
invalid(f"{label} must be an object")
|
|
25
|
+
return cast(Mapping[str, object], value)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def text(value: object, label: str, invalid: _Invalid) -> str:
|
|
29
|
+
"""Return stripped non-blank wire text or delegate failure policy."""
|
|
30
|
+
|
|
31
|
+
if not isinstance(value, str) or not value.strip():
|
|
32
|
+
invalid(f"{label} must be non-blank text")
|
|
33
|
+
return cast(str, value).strip()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""One lazily constructed synchronous module-level Client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from threading import Lock
|
|
7
|
+
from typing import TYPE_CHECKING, Literal, overload
|
|
8
|
+
|
|
9
|
+
from screamingface.client import DEFAULT_ENGINE_URL, DEFAULT_SCOREBOARD_URL, Client
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Callable, Sequence
|
|
13
|
+
|
|
14
|
+
from screamingface._ui.connections import ConnectionPanel
|
|
15
|
+
from screamingface.connections import Connection, OAuthFlow
|
|
16
|
+
from screamingface.events import Event
|
|
17
|
+
from screamingface.recipe import Recipe
|
|
18
|
+
from screamingface.report import Report
|
|
19
|
+
|
|
20
|
+
_client: Client | None = None
|
|
21
|
+
_lock = Lock()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def default_client() -> Client:
|
|
25
|
+
"""Return the process-wide Client, constructing it on first use."""
|
|
26
|
+
|
|
27
|
+
global _client
|
|
28
|
+
if _client is not None:
|
|
29
|
+
return _client
|
|
30
|
+
with _lock:
|
|
31
|
+
if _client is None:
|
|
32
|
+
_client = Client(
|
|
33
|
+
engine_url=os.environ.get("SCREAMINGFACE_ENGINE_URL", DEFAULT_ENGINE_URL),
|
|
34
|
+
scoreboard_url=os.environ.get(
|
|
35
|
+
"SCREAMINGFACE_SCOREBOARD_URL",
|
|
36
|
+
DEFAULT_SCOREBOARD_URL,
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
return _client
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def configure(
|
|
43
|
+
*,
|
|
44
|
+
engine_url: str = DEFAULT_ENGINE_URL,
|
|
45
|
+
scoreboard_url: str = DEFAULT_SCOREBOARD_URL,
|
|
46
|
+
) -> Client:
|
|
47
|
+
"""Replace the process-wide Client used by module-level convenience functions."""
|
|
48
|
+
|
|
49
|
+
global _client
|
|
50
|
+
replacement = Client(engine_url=engine_url, scoreboard_url=scoreboard_url)
|
|
51
|
+
with _lock:
|
|
52
|
+
previous = _client
|
|
53
|
+
_client = replacement
|
|
54
|
+
if previous is not None:
|
|
55
|
+
previous.close()
|
|
56
|
+
return replacement
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def close() -> None:
|
|
60
|
+
"""Close and forget the process-wide Client, if it has been created."""
|
|
61
|
+
|
|
62
|
+
global _client
|
|
63
|
+
with _lock:
|
|
64
|
+
previous = _client
|
|
65
|
+
_client = None
|
|
66
|
+
if previous is not None:
|
|
67
|
+
previous.close()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@overload
|
|
71
|
+
def evaluate(
|
|
72
|
+
candidates: str,
|
|
73
|
+
*,
|
|
74
|
+
benchmark: None = None,
|
|
75
|
+
limit: None = None,
|
|
76
|
+
on_event: Callable[[Event], None] | None = None,
|
|
77
|
+
progress: bool | None = None,
|
|
78
|
+
) -> Report: ...
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
def evaluate(
|
|
83
|
+
candidates: Recipe | Sequence[Recipe],
|
|
84
|
+
*,
|
|
85
|
+
benchmark: str,
|
|
86
|
+
limit: int | None = None,
|
|
87
|
+
on_event: Callable[[Event], None] | None = None,
|
|
88
|
+
progress: bool | None = None,
|
|
89
|
+
) -> Report: ...
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def evaluate(
|
|
93
|
+
candidates: Recipe | Sequence[Recipe] | str,
|
|
94
|
+
*,
|
|
95
|
+
benchmark: str | None = None,
|
|
96
|
+
limit: int | None = None,
|
|
97
|
+
on_event: Callable[[Event], None] | None = None,
|
|
98
|
+
progress: bool | None = None,
|
|
99
|
+
) -> Report:
|
|
100
|
+
"""Evaluate Recipes or a complete URL4 through the lazy default Client."""
|
|
101
|
+
|
|
102
|
+
client = default_client()
|
|
103
|
+
if isinstance(candidates, str):
|
|
104
|
+
if benchmark is not None:
|
|
105
|
+
raise TypeError("benchmark must not be passed when evaluating a complete URL4")
|
|
106
|
+
if limit is not None:
|
|
107
|
+
raise TypeError("limit must not be passed when evaluating a complete URL4")
|
|
108
|
+
return client.evaluate(candidates, on_event=on_event, progress=progress)
|
|
109
|
+
if benchmark is None:
|
|
110
|
+
raise TypeError("benchmark is required when evaluating Recipes")
|
|
111
|
+
return client.evaluate(
|
|
112
|
+
candidates,
|
|
113
|
+
benchmark=benchmark,
|
|
114
|
+
limit=limit,
|
|
115
|
+
on_event=on_event,
|
|
116
|
+
progress=progress,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@overload
|
|
121
|
+
def connect(
|
|
122
|
+
provider: None = None,
|
|
123
|
+
*,
|
|
124
|
+
api_key: None = None,
|
|
125
|
+
method: None = None,
|
|
126
|
+
) -> ConnectionPanel: ...
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@overload
|
|
130
|
+
def connect(
|
|
131
|
+
provider: str,
|
|
132
|
+
*,
|
|
133
|
+
api_key: str,
|
|
134
|
+
method: Literal["api_key"] | None = None,
|
|
135
|
+
) -> Connection: ...
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@overload
|
|
139
|
+
def connect(
|
|
140
|
+
provider: str,
|
|
141
|
+
*,
|
|
142
|
+
api_key: None = None,
|
|
143
|
+
method: Literal["oauth"],
|
|
144
|
+
) -> OAuthFlow: ...
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def connect(
|
|
148
|
+
provider: str | None = None,
|
|
149
|
+
*,
|
|
150
|
+
api_key: str | None = None,
|
|
151
|
+
method: Literal["api_key", "oauth"] | None = None,
|
|
152
|
+
) -> Connection | ConnectionPanel | OAuthFlow:
|
|
153
|
+
"""Open the provider panel or connect through the lazy default Client."""
|
|
154
|
+
|
|
155
|
+
if provider is None:
|
|
156
|
+
if api_key is not None or method is not None:
|
|
157
|
+
raise TypeError("provider is required when api_key or method is supplied")
|
|
158
|
+
from screamingface._ui.connections import ConnectionPanel
|
|
159
|
+
|
|
160
|
+
return ConnectionPanel(default_client())
|
|
161
|
+
client = default_client()
|
|
162
|
+
if api_key is not None:
|
|
163
|
+
if method == "oauth":
|
|
164
|
+
raise ValueError("api_key cannot be combined with OAuth")
|
|
165
|
+
result = client.connect(provider, api_key=api_key, method=method)
|
|
166
|
+
elif method == "api_key":
|
|
167
|
+
raise ValueError("api_key is required for API-key authentication")
|
|
168
|
+
elif method == "oauth":
|
|
169
|
+
result = client.connect(provider, method="oauth")
|
|
170
|
+
else:
|
|
171
|
+
raise ValueError("api_key is required unless method='oauth' is selected")
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def disconnect(provider: str) -> Connection:
|
|
176
|
+
"""Disconnect a provider through the lazy default Client."""
|
|
177
|
+
|
|
178
|
+
return default_client().disconnect(provider)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
__all__ = ["close", "configure", "connect", "disconnect", "evaluate"]
|