typesafe-sdk 0.7.2__tar.gz → 0.7.4__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.
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/PKG-INFO +3 -1
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/pyproject.toml +4 -2
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/pyproject.toml.orig +5 -2
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/client/aio/client.py +34 -4
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/client/sync/client.py +6 -3
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/config.py +4 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/endpoints.py +3 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/json.py +2 -1
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/question_types.py +27 -9
- typesafe_sdk-0.7.4/src/typesafe_sdk/_core/questions.py +49 -0
- typesafe_sdk-0.7.2/src/typesafe_sdk/_core/questions.py +0 -29
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/LICENSE +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/README.md +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/__init__.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/__init__.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/client/__init__.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/client/aio/__init__.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/client/aio/models.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/client/sync/__init__.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/client/sync/models.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/constants.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/errors.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/json_types.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/logging.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/response_types.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/retry.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/schemas/__init__.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/schemas/base.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_core/transport.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_schemas/__init__.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_schemas/models.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/_version.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/constants.py +0 -0
- {typesafe_sdk-0.7.2 → typesafe_sdk-0.7.4}/src/typesafe_sdk/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: typesafe-sdk
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
4
4
|
Summary: Python SDK for TypeSafe AI API.
|
|
5
5
|
Keywords: typesafe,sdk,api,ai
|
|
6
6
|
Author: TypeSafe AI
|
|
@@ -25,9 +25,11 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
25
25
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
26
|
Classifier: License :: OSI Approved :: MIT License
|
|
27
27
|
Requires-Dist: httpx2>=2.0.0
|
|
28
|
+
Requires-Dist: packaging>=24.0
|
|
28
29
|
Requires-Dist: pydantic>=2.12.0
|
|
29
30
|
Requires-Dist: pydantic-core>=2.41.1
|
|
30
31
|
Requires-Dist: tenacity>=9.0.0
|
|
32
|
+
Requires-Dist: truststore>=0.10.0 ; sys_platform != 'emscripten'
|
|
31
33
|
Requires-Dist: typing-extensions>=4.13.0
|
|
32
34
|
Requires-Dist: httpx2[http2]>=2.0.0 ; extra == 'http2'
|
|
33
35
|
Maintainer: Daniel Gafni
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "typesafe-sdk"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.4"
|
|
4
4
|
description = "Python SDK for TypeSafe AI API."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -33,9 +33,11 @@ classifiers = [
|
|
|
33
33
|
]
|
|
34
34
|
dependencies = [
|
|
35
35
|
"httpx2>=2.0.0",
|
|
36
|
+
"packaging>=24.0",
|
|
36
37
|
"pydantic>=2.12.0",
|
|
37
38
|
"pydantic-core>=2.41.1",
|
|
38
39
|
"tenacity>=9.0.0",
|
|
40
|
+
"truststore>=0.10.0 ; sys_platform != 'emscripten'",
|
|
39
41
|
"typing-extensions>=4.13.0",
|
|
40
42
|
]
|
|
41
43
|
|
|
@@ -78,7 +80,7 @@ docs = [
|
|
|
78
80
|
"ruff>=0.16.6",
|
|
79
81
|
"zensical>=0.0.60",
|
|
80
82
|
]
|
|
81
|
-
codegen = ["datamodel-code-generator>=0.
|
|
83
|
+
codegen = ["datamodel-code-generator>=0.81.0"]
|
|
82
84
|
|
|
83
85
|
[tool.pytest.ini_options]
|
|
84
86
|
addopts = "-vv -ra --capture=tee-sys -p no:tach"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "typesafe-sdk"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.4"
|
|
4
4
|
description = "Python SDK for TypeSafe AI API."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -34,9 +34,12 @@ classifiers = [
|
|
|
34
34
|
]
|
|
35
35
|
dependencies = [
|
|
36
36
|
"httpx2>=2.0.0",
|
|
37
|
+
"packaging>=24.0",
|
|
37
38
|
"pydantic>=2.12.0",
|
|
38
39
|
"pydantic-core>=2.41.1",
|
|
39
40
|
"tenacity>=9.0.0",
|
|
41
|
+
# TODO: bump constraint once https://github.com/sethmlarson/truststore/pull/219 is released
|
|
42
|
+
"truststore>=0.10.0 ; sys_platform != 'emscripten'",
|
|
40
43
|
"typing-extensions>=4.13.0",
|
|
41
44
|
]
|
|
42
45
|
|
|
@@ -74,7 +77,7 @@ docs = [
|
|
|
74
77
|
"zensical>=0.0.60",
|
|
75
78
|
]
|
|
76
79
|
codegen = [
|
|
77
|
-
"datamodel-code-generator>=0.
|
|
80
|
+
"datamodel-code-generator>=0.81.0",
|
|
78
81
|
]
|
|
79
82
|
|
|
80
83
|
[tool.pytest.ini_options]
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
"""Asynchronous API client."""
|
|
2
2
|
|
|
3
|
+
import ssl
|
|
4
|
+
import threading
|
|
3
5
|
from collections.abc import Mapping
|
|
4
6
|
from functools import cached_property
|
|
5
7
|
from types import TracebackType
|
|
6
|
-
from typing import overload
|
|
8
|
+
from typing import Any, overload
|
|
7
9
|
|
|
8
10
|
import httpx2
|
|
11
|
+
from packaging.version import Version
|
|
9
12
|
from typing_extensions import Self
|
|
10
13
|
|
|
11
14
|
from typesafe_sdk._core.client.aio.models import AsyncModels
|
|
12
|
-
from typesafe_sdk._core.config import Config
|
|
15
|
+
from typesafe_sdk._core.config import DEFAULT_LIMITS, Config
|
|
13
16
|
from typesafe_sdk._core.endpoints import prepare_system_one
|
|
14
17
|
from typesafe_sdk._core.json_types import JSONContent, JSONValue
|
|
15
18
|
from typesafe_sdk._core.question_types import Question
|
|
@@ -18,6 +21,28 @@ from typesafe_sdk._core.retry import RetryPolicy, build_tenacity_async
|
|
|
18
21
|
from typesafe_sdk._core.transport import Request, ResponseT, send_async
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
def _ssl_context() -> ssl.SSLContext:
|
|
25
|
+
"""httpx2's default SSL context, with `wrap_bio()` serialized on affected truststore versions.
|
|
26
|
+
|
|
27
|
+
AnyIO calls `wrap_bio()` from worker threads, and truststore<=0.10.4 reconfigures the shared
|
|
28
|
+
context there without a lock, which can abort the interpreter (sethmlarson/truststore#221).
|
|
29
|
+
"""
|
|
30
|
+
ctx = httpx2.create_ssl_context()
|
|
31
|
+
import truststore # noqa: PLC0415 - Unavailable on emscripten; httpx2 imports it lazily too.
|
|
32
|
+
|
|
33
|
+
if not isinstance(ctx, truststore.SSLContext) or Version(truststore.__version__) > Version("0.10.4"):
|
|
34
|
+
return ctx
|
|
35
|
+
wrap_bio = ctx.wrap_bio
|
|
36
|
+
lock = threading.Lock()
|
|
37
|
+
|
|
38
|
+
def locked_wrap_bio(*args: Any, **kwargs: Any) -> ssl.SSLObject:
|
|
39
|
+
with lock:
|
|
40
|
+
return wrap_bio(*args, **kwargs)
|
|
41
|
+
|
|
42
|
+
ctx.wrap_bio = locked_wrap_bio
|
|
43
|
+
return ctx
|
|
44
|
+
|
|
45
|
+
|
|
21
46
|
class AsyncTypeSafeClient:
|
|
22
47
|
def __init__(
|
|
23
48
|
self,
|
|
@@ -91,7 +116,11 @@ class AsyncTypeSafeClient:
|
|
|
91
116
|
timeout = http_client.timeout
|
|
92
117
|
self._config = Config.resolve(api_key, base_url, model, timeout, headers)
|
|
93
118
|
self._retry = build_tenacity_async(retry)
|
|
94
|
-
self._http_client =
|
|
119
|
+
self._http_client = (
|
|
120
|
+
httpx2.AsyncClient(timeout=self._config.timeout, limits=DEFAULT_LIMITS, transport=transport, verify=_ssl_context())
|
|
121
|
+
if http_client is None
|
|
122
|
+
else http_client
|
|
123
|
+
)
|
|
95
124
|
|
|
96
125
|
@cached_property
|
|
97
126
|
def models(self) -> AsyncModels:
|
|
@@ -170,7 +199,8 @@ class AsyncTypeSafeClient:
|
|
|
170
199
|
name and model and token usage details when no custom model is supplied.
|
|
171
200
|
|
|
172
201
|
Raises:
|
|
173
|
-
TypeSafeError:
|
|
202
|
+
TypeSafeError: State is `None`, questions are empty, a score or choice question has no criteria,
|
|
203
|
+
or a noul question has neither instructions nor criteria.
|
|
174
204
|
TypeSafeAPIError: The server returns an unsuccessful HTTP response after any retries.
|
|
175
205
|
TypeSafeAPIConnectionError: The request cannot connect or times out after any retries.
|
|
176
206
|
TypeSafeAPIResponseValidationError: The response body does not match the response model.
|
|
@@ -9,7 +9,7 @@ import httpx2
|
|
|
9
9
|
from typing_extensions import Self
|
|
10
10
|
|
|
11
11
|
from typesafe_sdk._core.client.sync.models import Models
|
|
12
|
-
from typesafe_sdk._core.config import Config
|
|
12
|
+
from typesafe_sdk._core.config import DEFAULT_LIMITS, Config
|
|
13
13
|
from typesafe_sdk._core.endpoints import prepare_system_one
|
|
14
14
|
from typesafe_sdk._core.json_types import JSONContent, JSONValue
|
|
15
15
|
from typesafe_sdk._core.question_types import Question
|
|
@@ -84,7 +84,9 @@ class TypeSafeClient:
|
|
|
84
84
|
timeout = http_client.timeout
|
|
85
85
|
self._config = Config.resolve(api_key, base_url, model, timeout, headers)
|
|
86
86
|
self._retry = build_tenacity(retry)
|
|
87
|
-
self._http_client =
|
|
87
|
+
self._http_client = (
|
|
88
|
+
httpx2.Client(timeout=self._config.timeout, limits=DEFAULT_LIMITS, transport=transport) if http_client is None else http_client
|
|
89
|
+
)
|
|
88
90
|
|
|
89
91
|
@cached_property
|
|
90
92
|
def models(self) -> Models:
|
|
@@ -162,7 +164,8 @@ class TypeSafeClient:
|
|
|
162
164
|
name and model and token usage details when no custom model is supplied.
|
|
163
165
|
|
|
164
166
|
Raises:
|
|
165
|
-
TypeSafeError:
|
|
167
|
+
TypeSafeError: State is `None`, questions are empty, a score or choice question has no criteria,
|
|
168
|
+
or a noul question has neither instructions nor criteria.
|
|
166
169
|
TypeSafeAPIError: The server returns an unsuccessful HTTP response after any retries.
|
|
167
170
|
TypeSafeAPIConnectionError: The request cannot connect or times out after any retries.
|
|
168
171
|
TypeSafeAPIResponseValidationError: The response body does not match the response model.
|
|
@@ -17,6 +17,10 @@ from typesafe_sdk.constants import (
|
|
|
17
17
|
DEFAULT_TIMEOUT,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
+
# httpx2 keeps only 20 idle connections for 5 s by default, so bursts beyond that reopen connections with new TLS
|
|
21
|
+
# handshakes. Keep every pooled connection; the API reuses idle connections for far longer than 30 s.
|
|
22
|
+
DEFAULT_LIMITS = httpx2.Limits(max_connections=100, max_keepalive_connections=100, keepalive_expiry=30.0)
|
|
23
|
+
|
|
20
24
|
|
|
21
25
|
def _resolve_string(value: str | None, env: str, default: str = "") -> str:
|
|
22
26
|
"""Resolve an explicit string or a stripped environment value, falling back to a default."""
|
|
@@ -7,6 +7,7 @@ import httpx2
|
|
|
7
7
|
|
|
8
8
|
from typesafe_sdk._core.config import Config
|
|
9
9
|
from typesafe_sdk._core.constants import MODELS_PATH, SYSTEM_ONE_PATH
|
|
10
|
+
from typesafe_sdk._core.errors import TypeSafeError
|
|
10
11
|
from typesafe_sdk._core.json_types import JSONContent, JSONValue
|
|
11
12
|
from typesafe_sdk._core.question_types import Question
|
|
12
13
|
from typesafe_sdk._core.questions import normalize_questions
|
|
@@ -24,6 +25,8 @@ def prepare_system_one(
|
|
|
24
25
|
headers: Mapping[str, str] | None,
|
|
25
26
|
response_type: type[ResponseT],
|
|
26
27
|
) -> Request[ResponseT]:
|
|
28
|
+
if state is None:
|
|
29
|
+
raise TypeSafeError("State is required.")
|
|
27
30
|
body: dict[str, Any] = {
|
|
28
31
|
"state": state,
|
|
29
32
|
"model": config.default_model if model is None else model,
|
|
@@ -23,7 +23,8 @@ def _fallback(value: object) -> object:
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def serialize(value: object) -> bytes:
|
|
26
|
-
|
|
26
|
+
# JSON has no NaN/Infinity tokens; emit them as the strings "NaN"/"Infinity"/"-Infinity" instead.
|
|
27
|
+
return to_json(value, fallback=_fallback, inf_nan_mode="strings")
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
def deserialize(content: bytes) -> Any:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Question objects and raw input models."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping, Sequence
|
|
4
|
-
from typing import Any, Literal, TypeAlias
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, overload
|
|
5
5
|
|
|
6
|
-
from pydantic import BaseModel, ConfigDict, model_serializer
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, model_serializer
|
|
7
7
|
from pydantic.functional_serializers import SerializerFunctionWrapHandler
|
|
8
8
|
from typing_extensions import NotRequired, TypedDict
|
|
9
9
|
|
|
@@ -23,6 +23,16 @@ class NoulCriteria(TypedDict, total=False, closed=True):
|
|
|
23
23
|
"""Description of the no outcome as text, a JSON object, or an array; `None` leaves it undescribed."""
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
class _DescribedTrue(TypedDict, closed=True):
|
|
27
|
+
true: JSONContent
|
|
28
|
+
false: NotRequired[JSONContent | None]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _DescribedFalse(TypedDict, closed=True):
|
|
32
|
+
true: NotRequired[JSONContent | None]
|
|
33
|
+
false: JSONContent
|
|
34
|
+
|
|
35
|
+
|
|
26
36
|
class NoulModel(TypedDict, closed=True):
|
|
27
37
|
"""A yes/no question dictionary with `type="noul"`.
|
|
28
38
|
|
|
@@ -31,9 +41,9 @@ class NoulModel(TypedDict, closed=True):
|
|
|
31
41
|
|
|
32
42
|
type: Literal["noul"]
|
|
33
43
|
instructions: NotRequired[JSONContent | None]
|
|
34
|
-
"""The question to ask, expressed as text, a JSON object, or an array; optional."""
|
|
44
|
+
"""The question to ask, expressed as text, a JSON object, or an array; optional when `criteria` describes an outcome."""
|
|
35
45
|
criteria: NotRequired[NoulCriteria | None]
|
|
36
|
-
"""Optional descriptions of the yes and no outcomes
|
|
46
|
+
"""Optional descriptions of the yes and no outcomes; required when `instructions` is `None`."""
|
|
37
47
|
|
|
38
48
|
|
|
39
49
|
class ChoiceModel(TypedDict, closed=True):
|
|
@@ -46,7 +56,7 @@ class ChoiceModel(TypedDict, closed=True):
|
|
|
46
56
|
instructions: NotRequired[JSONContent | None]
|
|
47
57
|
"""The question to ask, expressed as text, a JSON object, or an array; optional."""
|
|
48
58
|
criteria: Mapping[str, JSONContent | None]
|
|
49
|
-
"""
|
|
59
|
+
"""A nonempty mapping of labels to text, object, or array descriptions, or `None` for undescribed labels."""
|
|
50
60
|
|
|
51
61
|
|
|
52
62
|
class ScoreModel(TypedDict, closed=True):
|
|
@@ -82,9 +92,17 @@ class Noul(_Question, wire.NoulQuestion):
|
|
|
82
92
|
|
|
83
93
|
type: Literal["noul"] = "noul"
|
|
84
94
|
instructions: JSONContent | None = None # pyrefly: ignore[bad-override-mutable-attribute]
|
|
85
|
-
"""The question to ask, expressed as text, a JSON object, or an array; optional."""
|
|
95
|
+
"""The question to ask, expressed as text, a JSON object, or an array; optional when `criteria` describes an outcome."""
|
|
86
96
|
criteria: NoulCriteria | None = None # pyrefly: ignore[bad-override-mutable-attribute]
|
|
87
|
-
"""Optional descriptions of the yes and no outcomes
|
|
97
|
+
"""Optional descriptions of the yes and no outcomes; required when `instructions` is `None`."""
|
|
98
|
+
|
|
99
|
+
if TYPE_CHECKING:
|
|
100
|
+
# Require instructions or at least one described outcome.
|
|
101
|
+
@overload
|
|
102
|
+
def __init__(self, *, type: Literal["noul"] = ..., instructions: JSONContent, criteria: NoulCriteria | None = ...) -> None: ...
|
|
103
|
+
@overload
|
|
104
|
+
def __init__(self, *, type: Literal["noul"] = ..., instructions: None = ..., criteria: _DescribedTrue | _DescribedFalse) -> None: ...
|
|
105
|
+
def __init__(self, **data: Any) -> None: ...
|
|
88
106
|
|
|
89
107
|
|
|
90
108
|
class Choice(_Question, wire.ChoiceQuestion):
|
|
@@ -95,7 +113,7 @@ class Choice(_Question, wire.ChoiceQuestion):
|
|
|
95
113
|
|
|
96
114
|
type: Literal["choice"] = "choice"
|
|
97
115
|
criteria: Mapping[str, JSONContent | None] # pyrefly: ignore[bad-override-mutable-attribute]
|
|
98
|
-
"""
|
|
116
|
+
"""A nonempty mapping of labels to text, object, or array descriptions, or `None` for undescribed labels."""
|
|
99
117
|
instructions: JSONContent | None = None # pyrefly: ignore[bad-override-mutable-attribute]
|
|
100
118
|
"""The question to ask, expressed as text, a JSON object, or an array; optional."""
|
|
101
119
|
|
|
@@ -107,7 +125,7 @@ class Score(_Question, wire.ScoreQuestion):
|
|
|
107
125
|
"""
|
|
108
126
|
|
|
109
127
|
type: Literal["score"] = "score"
|
|
110
|
-
criteria: Sequence[JSONContent] # pyrefly: ignore[bad-override-mutable-attribute]
|
|
128
|
+
criteria: Sequence[JSONContent] = Field(min_length=1) # pyrefly: ignore[bad-override-mutable-attribute]
|
|
111
129
|
"""A nonempty, ordered list of text, object, or array descriptions, one per score from zero."""
|
|
112
130
|
instructions: JSONContent | None = None # pyrefly: ignore[bad-override-mutable-attribute]
|
|
113
131
|
"""The question to ask, expressed as text, a JSON object, or an array; optional."""
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Question validation before JSON encoding."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
|
|
5
|
+
from typesafe_sdk._core.errors import TypeSafeError
|
|
6
|
+
from typesafe_sdk._core.json_types import JSONContent
|
|
7
|
+
from typesafe_sdk._core.question_types import Choice, Noul, Question, Score
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize_questions(questions: Mapping[str, Question]) -> dict[str, Question]:
|
|
11
|
+
if not questions:
|
|
12
|
+
raise TypeSafeError("At least one question is required.")
|
|
13
|
+
for name, question in questions.items():
|
|
14
|
+
if isinstance(question, Choice):
|
|
15
|
+
_validate_choice_criteria(name, question.criteria)
|
|
16
|
+
elif isinstance(question, Noul):
|
|
17
|
+
_validate_noul(name, question.instructions, question.criteria)
|
|
18
|
+
elif not isinstance(question, Score):
|
|
19
|
+
if not isinstance(question, dict) or not isinstance(question.get("type"), str) or not question["type"]:
|
|
20
|
+
raise TypeSafeError(f'Question "{name}" must be a question object or a dictionary with a nonempty string "type".')
|
|
21
|
+
if question["type"] in ("choice", "score") and "criteria" not in question:
|
|
22
|
+
raise TypeSafeError(f'Question "{name}" requires "criteria".')
|
|
23
|
+
if question["type"] == "score":
|
|
24
|
+
_validate_score_criteria(name, question["criteria"])
|
|
25
|
+
elif question["type"] == "choice":
|
|
26
|
+
_validate_choice_criteria(name, question["criteria"])
|
|
27
|
+
elif question["type"] == "noul":
|
|
28
|
+
_validate_noul(name, question.get("instructions"), question.get("criteria"))
|
|
29
|
+
return dict(questions)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _validate_score_criteria(name: str, criteria: Sequence[JSONContent]) -> None:
|
|
33
|
+
"""Reject a score question with no criteria; at least one score is required."""
|
|
34
|
+
if not criteria:
|
|
35
|
+
raise TypeSafeError(f'Score question "{name}" has no criteria; at least one score is required.')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _validate_choice_criteria(name: str, criteria: Mapping[str, JSONContent | None]) -> None:
|
|
39
|
+
"""Reject a choice question with no criteria; at least one choice is required."""
|
|
40
|
+
if not criteria:
|
|
41
|
+
raise TypeSafeError(f'Choice question "{name}" has no criteria; at least one choice is required.')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _validate_noul(name: str, instructions: object, criteria: object) -> None:
|
|
45
|
+
"""Reject a noul question with non-object criteria or with neither instructions nor a described outcome."""
|
|
46
|
+
if criteria is not None and not isinstance(criteria, Mapping):
|
|
47
|
+
raise TypeSafeError(f'Noul question "{name}" criteria must be an object.')
|
|
48
|
+
if instructions is None and not (isinstance(criteria, Mapping) and any(value is not None for value in criteria.values())):
|
|
49
|
+
raise TypeSafeError(f'Noul question "{name}" has neither instructions nor criteria; at least one is required.')
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"""Question validation before JSON encoding."""
|
|
2
|
-
|
|
3
|
-
from collections.abc import Mapping, Sequence
|
|
4
|
-
|
|
5
|
-
from typesafe_sdk._core.errors import TypeSafeError
|
|
6
|
-
from typesafe_sdk._core.json_types import JSONContent
|
|
7
|
-
from typesafe_sdk._core.question_types import Choice, Noul, Question, Score
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def normalize_questions(questions: Mapping[str, Question]) -> dict[str, Question]:
|
|
11
|
-
if not questions:
|
|
12
|
-
raise TypeSafeError("At least one question is required.")
|
|
13
|
-
for name, question in questions.items():
|
|
14
|
-
if isinstance(question, Score):
|
|
15
|
-
_validate_score_criteria(name, question.criteria)
|
|
16
|
-
elif not isinstance(question, (Noul, Choice)):
|
|
17
|
-
if not isinstance(question, dict) or not isinstance(question.get("type"), str) or not question["type"]:
|
|
18
|
-
raise TypeSafeError(f'Question "{name}" must be a question object or a dictionary with a nonempty string "type".')
|
|
19
|
-
if question["type"] in ("choice", "score") and "criteria" not in question:
|
|
20
|
-
raise TypeSafeError(f'Question "{name}" requires "criteria".')
|
|
21
|
-
if question["type"] == "score":
|
|
22
|
-
_validate_score_criteria(name, question["criteria"])
|
|
23
|
-
return dict(questions)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _validate_score_criteria(name: str, criteria: Sequence[JSONContent]) -> None:
|
|
27
|
-
"""Reject a score question with no criteria; at least one score is required."""
|
|
28
|
-
if not criteria:
|
|
29
|
-
raise TypeSafeError(f'Score question "{name}" has no criteria; at least one score is required.')
|
|
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
|
|
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
|
|
File without changes
|