pi-ai-python 1.3.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.
Files changed (189) hide show
  1. pi_agent/__init__.py +127 -0
  2. pi_agent/agent.py +617 -0
  3. pi_agent/agent_loop.py +820 -0
  4. pi_agent/proxy.py +269 -0
  5. pi_agent/py.typed +0 -0
  6. pi_agent/stream_fn.py +27 -0
  7. pi_agent/tools.py +327 -0
  8. pi_agent/types.py +394 -0
  9. pi_ai/__init__.py +263 -0
  10. pi_ai/abort.py +315 -0
  11. pi_ai/api/__init__.py +1 -0
  12. pi_ai/api/anthropic_messages.py +1782 -0
  13. pi_ai/api/azure_openai_config.py +138 -0
  14. pi_ai/api/azure_openai_responses.py +245 -0
  15. pi_ai/api/bedrock_converse_stream.py +1598 -0
  16. pi_ai/api/classifier_shared.py +211 -0
  17. pi_ai/api/cloudflare.py +36 -0
  18. pi_ai/api/cloudflare_workers_ai_system_one.py +80 -0
  19. pi_ai/api/constrained_sampling.py +279 -0
  20. pi_ai/api/github_copilot_headers.py +30 -0
  21. pi_ai/api/google_generative_ai.py +138 -0
  22. pi_ai/api/google_shared.py +927 -0
  23. pi_ai/api/google_vertex.py +234 -0
  24. pi_ai/api/lazy.py +156 -0
  25. pi_ai/api/llama_cpp_classify.py +449 -0
  26. pi_ai/api/mistral_conversations.py +756 -0
  27. pi_ai/api/openai_codex_responses.py +1545 -0
  28. pi_ai/api/openai_completions.py +1594 -0
  29. pi_ai/api/openai_decisions.py +210 -0
  30. pi_ai/api/openai_prompt_cache.py +19 -0
  31. pi_ai/api/openai_responses.py +418 -0
  32. pi_ai/api/openai_responses_shared.py +840 -0
  33. pi_ai/api/openai_sdk.py +204 -0
  34. pi_ai/api/openrouter_images.py +153 -0
  35. pi_ai/api/pi_messages.py +449 -0
  36. pi_ai/api/registry.py +103 -0
  37. pi_ai/api/simple_options.py +119 -0
  38. pi_ai/api/system_one_shared.py +148 -0
  39. pi_ai/api/transform_messages.py +211 -0
  40. pi_ai/api/typesafe_system_one.py +43 -0
  41. pi_ai/auth/__init__.py +1 -0
  42. pi_ai/auth/context.py +47 -0
  43. pi_ai/auth/credential_store.py +208 -0
  44. pi_ai/auth/helpers.py +123 -0
  45. pi_ai/auth/oauth/__init__.py +5 -0
  46. pi_ai/auth/oauth/_common.py +454 -0
  47. pi_ai/auth/oauth/anthropic.py +283 -0
  48. pi_ai/auth/oauth/callback_server.py +339 -0
  49. pi_ai/auth/oauth/device_code.py +127 -0
  50. pi_ai/auth/oauth/github_copilot.py +508 -0
  51. pi_ai/auth/oauth/kimi_coding.py +283 -0
  52. pi_ai/auth/oauth/load.py +64 -0
  53. pi_ai/auth/oauth/meta.py +238 -0
  54. pi_ai/auth/oauth/openai_chatgpt.py +321 -0
  55. pi_ai/auth/oauth/openai_codex.py +391 -0
  56. pi_ai/auth/oauth/openrouter.py +173 -0
  57. pi_ai/auth/oauth/pkce.py +23 -0
  58. pi_ai/auth/oauth/radius.py +311 -0
  59. pi_ai/auth/oauth/xai.py +220 -0
  60. pi_ai/auth/resolve.py +190 -0
  61. pi_ai/auth/types.py +292 -0
  62. pi_ai/cli.py +265 -0
  63. pi_ai/compat.py +651 -0
  64. pi_ai/env_api_keys.py +137 -0
  65. pi_ai/event_stream.py +329 -0
  66. pi_ai/models.py +1191 -0
  67. pi_ai/models_store.py +121 -0
  68. pi_ai/oauth.py +90 -0
  69. pi_ai/providers/__init__.py +1 -0
  70. pi_ai/providers/all.py +154 -0
  71. pi_ai/providers/amazon_bedrock.py +116 -0
  72. pi_ai/providers/ant_ling.py +23 -0
  73. pi_ai/providers/anthropic.py +105 -0
  74. pi_ai/providers/azure.py +103 -0
  75. pi_ai/providers/baseten.py +23 -0
  76. pi_ai/providers/catalog.py +167 -0
  77. pi_ai/providers/cerebras.py +23 -0
  78. pi_ai/providers/cloudflare_ai_gateway.py +30 -0
  79. pi_ai/providers/cloudflare_auth.py +112 -0
  80. pi_ai/providers/cloudflare_stream.py +84 -0
  81. pi_ai/providers/cloudflare_workers_ai.py +26 -0
  82. pi_ai/providers/data/amazon-bedrock.json +1 -0
  83. pi_ai/providers/data/ant-ling.json +1 -0
  84. pi_ai/providers/data/anthropic.json +1 -0
  85. pi_ai/providers/data/azure.json +1 -0
  86. pi_ai/providers/data/baseten.json +1 -0
  87. pi_ai/providers/data/cerebras.json +1 -0
  88. pi_ai/providers/data/cloudflare-ai-gateway.json +1 -0
  89. pi_ai/providers/data/cloudflare-workers-ai.json +1 -0
  90. pi_ai/providers/data/deepseek.json +1 -0
  91. pi_ai/providers/data/fireworks.json +1 -0
  92. pi_ai/providers/data/github-copilot.json +1 -0
  93. pi_ai/providers/data/google-vertex.json +1 -0
  94. pi_ai/providers/data/google.json +1 -0
  95. pi_ai/providers/data/groq.json +1 -0
  96. pi_ai/providers/data/huggingface.json +1 -0
  97. pi_ai/providers/data/kimi-coding.json +1 -0
  98. pi_ai/providers/data/manifest.json +1 -0
  99. pi_ai/providers/data/meta.json +1 -0
  100. pi_ai/providers/data/minimax-cn.json +1 -0
  101. pi_ai/providers/data/minimax.json +1 -0
  102. pi_ai/providers/data/mistral.json +1 -0
  103. pi_ai/providers/data/moonshotai-cn.json +1 -0
  104. pi_ai/providers/data/moonshotai.json +1 -0
  105. pi_ai/providers/data/nvidia.json +1 -0
  106. pi_ai/providers/data/openai-codex.json +1 -0
  107. pi_ai/providers/data/openai.json +1 -0
  108. pi_ai/providers/data/opencode-go.json +1 -0
  109. pi_ai/providers/data/opencode.json +1 -0
  110. pi_ai/providers/data/openrouter.json +1 -0
  111. pi_ai/providers/data/qwen-token-plan-cn.json +1 -0
  112. pi_ai/providers/data/qwen-token-plan-individual.json +1 -0
  113. pi_ai/providers/data/qwen-token-plan.json +1 -0
  114. pi_ai/providers/data/radius.json +1 -0
  115. pi_ai/providers/data/together.json +1 -0
  116. pi_ai/providers/data/typesafe.json +1 -0
  117. pi_ai/providers/data/vercel-ai-gateway.json +1 -0
  118. pi_ai/providers/data/xai.json +1 -0
  119. pi_ai/providers/data/xiaomi-token-plan-ams.json +1 -0
  120. pi_ai/providers/data/xiaomi-token-plan-cn.json +1 -0
  121. pi_ai/providers/data/xiaomi-token-plan-sgp.json +1 -0
  122. pi_ai/providers/data/xiaomi.json +1 -0
  123. pi_ai/providers/data/zai-coding-cn.json +1 -0
  124. pi_ai/providers/data/zai.json +1 -0
  125. pi_ai/providers/deepseek.py +23 -0
  126. pi_ai/providers/faux.py +680 -0
  127. pi_ai/providers/fireworks.py +26 -0
  128. pi_ai/providers/github_copilot.py +46 -0
  129. pi_ai/providers/google.py +23 -0
  130. pi_ai/providers/google_vertex.py +131 -0
  131. pi_ai/providers/groq.py +23 -0
  132. pi_ai/providers/huggingface.py +23 -0
  133. pi_ai/providers/kimi_coding.py +32 -0
  134. pi_ai/providers/meta.py +32 -0
  135. pi_ai/providers/minimax.py +23 -0
  136. pi_ai/providers/minimax_cn.py +23 -0
  137. pi_ai/providers/mistral.py +23 -0
  138. pi_ai/providers/moonshotai.py +23 -0
  139. pi_ai/providers/moonshotai_cn.py +23 -0
  140. pi_ai/providers/nvidia.py +23 -0
  141. pi_ai/providers/openai.py +44 -0
  142. pi_ai/providers/openai_codex.py +26 -0
  143. pi_ai/providers/opencode.py +36 -0
  144. pi_ai/providers/opencode_go.py +33 -0
  145. pi_ai/providers/opencode_headers.py +60 -0
  146. pi_ai/providers/openrouter.py +145 -0
  147. pi_ai/providers/openrouter_api.py +122 -0
  148. pi_ai/providers/qwen_token_plan.py +23 -0
  149. pi_ai/providers/qwen_token_plan_cn.py +23 -0
  150. pi_ai/providers/qwen_token_plan_individual.py +23 -0
  151. pi_ai/providers/radius.py +146 -0
  152. pi_ai/providers/radius_config.py +136 -0
  153. pi_ai/providers/together.py +23 -0
  154. pi_ai/providers/typesafe.py +22 -0
  155. pi_ai/providers/vercel_ai_gateway.py +25 -0
  156. pi_ai/providers/xai.py +32 -0
  157. pi_ai/providers/xiaomi.py +23 -0
  158. pi_ai/providers/xiaomi_token_plan_ams.py +23 -0
  159. pi_ai/providers/xiaomi_token_plan_cn.py +23 -0
  160. pi_ai/providers/xiaomi_token_plan_sgp.py +23 -0
  161. pi_ai/providers/zai.py +23 -0
  162. pi_ai/providers/zai_coding_cn.py +23 -0
  163. pi_ai/py.typed +0 -0
  164. pi_ai/serialization.py +477 -0
  165. pi_ai/session_resources.py +33 -0
  166. pi_ai/types.py +1003 -0
  167. pi_ai/utils/__init__.py +1 -0
  168. pi_ai/utils/callbacks.py +15 -0
  169. pi_ai/utils/diagnostics.py +70 -0
  170. pi_ai/utils/error_body.py +89 -0
  171. pi_ai/utils/estimate.py +125 -0
  172. pi_ai/utils/fastjson.py +63 -0
  173. pi_ai/utils/http.py +409 -0
  174. pi_ai/utils/json_parse.py +424 -0
  175. pi_ai/utils/misc.py +193 -0
  176. pi_ai/utils/model_operations.py +73 -0
  177. pi_ai/utils/models_error.py +35 -0
  178. pi_ai/utils/oauth_page.py +121 -0
  179. pi_ai/utils/overflow.py +87 -0
  180. pi_ai/utils/retry.py +193 -0
  181. pi_ai/utils/schema.py +283 -0
  182. pi_ai/utils/text.py +65 -0
  183. pi_ai/utils/transcript.py +271 -0
  184. pi_ai/utils/validation.py +813 -0
  185. pi_ai_python-1.3.0.dist-info/METADATA +167 -0
  186. pi_ai_python-1.3.0.dist-info/RECORD +189 -0
  187. pi_ai_python-1.3.0.dist-info/WHEEL +4 -0
  188. pi_ai_python-1.3.0.dist-info/entry_points.txt +2 -0
  189. pi_ai_python-1.3.0.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1 @@
1
+ """Shared utilities."""
@@ -0,0 +1,15 @@
1
+ """Helpers for user callbacks that may be sync or async."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from typing import Any
7
+
8
+ __all__ = ["maybe_await"]
9
+
10
+
11
+ async def maybe_await(value: Any) -> Any:
12
+ """Await ``value`` if it is awaitable, otherwise return it unchanged."""
13
+ if inspect.isawaitable(value):
14
+ return await value
15
+ return value
@@ -0,0 +1,70 @@
1
+ """Redacted provider/runtime diagnostics attached to assistant messages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import traceback
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from ..types import now_ms
10
+
11
+ __all__ = [
12
+ "DiagnosticErrorInfo",
13
+ "AssistantMessageDiagnostic",
14
+ "format_thrown_value",
15
+ "extract_diagnostic_error",
16
+ "create_assistant_message_diagnostic",
17
+ "append_assistant_message_diagnostic",
18
+ ]
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class DiagnosticErrorInfo:
23
+ message: str
24
+ name: str | None = None
25
+ stack: str | None = None
26
+ code: str | int | None = None
27
+
28
+
29
+ @dataclass(slots=True)
30
+ class AssistantMessageDiagnostic:
31
+ type: str
32
+ timestamp: int = field(default_factory=now_ms)
33
+ error: DiagnosticErrorInfo | None = None
34
+ details: dict[str, Any] | None = None
35
+
36
+
37
+ def format_thrown_value(value: Any) -> str:
38
+ """Human-readable text for an exception or thrown value."""
39
+ if isinstance(value, BaseException):
40
+ return str(value) or type(value).__name__
41
+ if isinstance(value, str):
42
+ return value
43
+ return str(value)
44
+
45
+
46
+ def extract_diagnostic_error(error: Any) -> DiagnosticErrorInfo:
47
+ if not isinstance(error, BaseException):
48
+ return DiagnosticErrorInfo(name="ThrownValue", message=format_thrown_value(error))
49
+ code = getattr(error, "code", None)
50
+ stack = (
51
+ "".join(traceback.format_exception(type(error), error, error.__traceback__)) if error.__traceback__ else None
52
+ )
53
+ return DiagnosticErrorInfo(
54
+ name=type(error).__name__,
55
+ message=str(error) or type(error).__name__,
56
+ stack=stack,
57
+ code=code if isinstance(code, (str, int)) and not isinstance(code, bool) else None,
58
+ )
59
+
60
+
61
+ def create_assistant_message_diagnostic(
62
+ type: str, error: Any, details: dict[str, Any] | None = None
63
+ ) -> AssistantMessageDiagnostic:
64
+ return AssistantMessageDiagnostic(
65
+ type=type, timestamp=now_ms(), error=extract_diagnostic_error(error), details=details
66
+ )
67
+
68
+
69
+ def append_assistant_message_diagnostic(message: Any, diagnostic: AssistantMessageDiagnostic) -> None:
70
+ message.diagnostics = [*(message.diagnostics or []), diagnostic]
@@ -0,0 +1,89 @@
1
+ """Normalization of provider HTTP error objects into display strings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ __all__ = [
10
+ "MAX_PROVIDER_ERROR_BODY_CHARS",
11
+ "NormalizedProviderError",
12
+ "normalize_provider_error",
13
+ "format_provider_error",
14
+ "truncate_error_text",
15
+ "safe_json_stringify",
16
+ ]
17
+
18
+ MAX_PROVIDER_ERROR_BODY_CHARS = 4000
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class NormalizedProviderError:
23
+ message: str
24
+ #: True when ``message`` already contains the body.
25
+ message_carries_body: bool
26
+ status: int | None = None
27
+ body: str | None = None
28
+
29
+
30
+ def safe_json_stringify(value: Any) -> str:
31
+ try:
32
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
33
+ except (TypeError, ValueError):
34
+ return str(value)
35
+
36
+
37
+ def truncate_error_text(text: str, max_chars: int) -> str:
38
+ if len(text) <= max_chars:
39
+ return text
40
+ return f"{text[:max_chars]}... [truncated {len(text) - max_chars} chars]"
41
+
42
+
43
+ def _extract_status(error: Any) -> int | None:
44
+ for name in ("status_code", "status"):
45
+ value = getattr(error, name, None)
46
+ if isinstance(value, int) and not isinstance(value, bool):
47
+ return value
48
+ return None
49
+
50
+
51
+ def _pick_body_text(error: Any) -> str | None:
52
+ body = getattr(error, "body", None)
53
+ if isinstance(body, str):
54
+ return body
55
+ parsed = getattr(error, "error", None)
56
+ if isinstance(parsed, dict) and parsed:
57
+ return safe_json_stringify(parsed)
58
+ return None
59
+
60
+
61
+ def normalize_provider_error(error: Any) -> NormalizedProviderError:
62
+ """Probe an exception for HTTP status and body so the body reason is not lost."""
63
+ if not isinstance(error, BaseException):
64
+ return NormalizedProviderError(message=safe_json_stringify(error), message_carries_body=False)
65
+ message = str(error) or type(error).__name__
66
+ status = _extract_status(error)
67
+ body_text = _pick_body_text(error)
68
+ body: str | None = None
69
+ if body_text is not None:
70
+ trimmed = body_text.strip()
71
+ if trimmed:
72
+ body = truncate_error_text(trimmed, MAX_PROVIDER_ERROR_BODY_CHARS)
73
+ return NormalizedProviderError(
74
+ message=message,
75
+ message_carries_body=body is None or body in message,
76
+ status=status,
77
+ body=body,
78
+ )
79
+
80
+
81
+ def format_provider_error(norm: NormalizedProviderError, prefix: str | None = None) -> str:
82
+ """Compose a display string: ``"<status>: <body>"`` or ``"<prefix> (<status>): <body>"``."""
83
+ if norm.message_carries_body or norm.status is None or norm.body is None:
84
+ if prefix is not None and norm.status is not None:
85
+ return f"{prefix} ({norm.status}): {norm.message}"
86
+ return norm.message
87
+ if prefix is not None:
88
+ return f"{prefix} ({norm.status}): {norm.body}"
89
+ return f"{norm.status}: {norm.body}"
@@ -0,0 +1,125 @@
1
+ """Rough token estimates for context accounting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ from collections.abc import Sequence
8
+ from dataclasses import dataclass
9
+ from typing import Any
10
+
11
+ from ..types import TranscriptContext, Usage
12
+ from .text import get_system_message_text
13
+
14
+ __all__ = [
15
+ "ContextUsageEstimate",
16
+ "calculate_context_tokens",
17
+ "estimate_text_tokens",
18
+ "estimate_text_and_image_content_tokens",
19
+ "estimate_message_tokens",
20
+ "estimate_context_tokens",
21
+ ]
22
+
23
+ _CHARS_PER_TOKEN = 3.5
24
+ _ESTIMATED_IMAGE_CHARS = 4800
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class ContextUsageEstimate:
29
+ #: Estimated total context tokens.
30
+ tokens: int
31
+ #: Tokens reported by the most recent applicable assistant usage block.
32
+ usage_tokens: int
33
+ #: Estimated tokens after that usage block.
34
+ trailing_tokens: int
35
+ #: Index of the message that provided usage, or ``None``.
36
+ last_usage_index: int | None
37
+
38
+
39
+ def calculate_context_tokens(usage: Usage) -> int:
40
+ return usage.total_tokens or usage.input + usage.output + usage.cache_read + usage.cache_write
41
+
42
+
43
+ def _safe_json(value: Any) -> str:
44
+ try:
45
+ from ..serialization import to_json_value
46
+
47
+ return json.dumps(to_json_value(value), separators=(",", ":"))
48
+ except Exception:
49
+ return "[unserializable]"
50
+
51
+
52
+ def _content_chars(content: Any) -> int:
53
+ if isinstance(content, str):
54
+ return len(content)
55
+ chars = 0
56
+ for block in content:
57
+ chars += len(block.text) if block.type == "text" else _ESTIMATED_IMAGE_CHARS
58
+ return chars
59
+
60
+
61
+ def estimate_text_tokens(text: str) -> int:
62
+ return math.ceil(len(text) / _CHARS_PER_TOKEN)
63
+
64
+
65
+ def estimate_text_and_image_content_tokens(content: Any) -> int:
66
+ return math.ceil(_content_chars(content) / _CHARS_PER_TOKEN)
67
+
68
+
69
+ def _estimate_tools_tokens(tools: Sequence[Any] | None) -> int:
70
+ if not tools:
71
+ return 0
72
+ return estimate_text_tokens(_safe_json(list(tools)))
73
+
74
+
75
+ def estimate_message_tokens(message: Any) -> int:
76
+ role = message.role
77
+ if role == "system":
78
+ return (
79
+ estimate_text_tokens(get_system_message_text(message))
80
+ + _estimate_tools_tokens(message.tools_added)
81
+ + _estimate_tools_tokens(message.tools_removed)
82
+ )
83
+ if role in ("user", "toolResult"):
84
+ return estimate_text_and_image_content_tokens(message.content)
85
+ if role != "assistant":
86
+ return 0
87
+ chars = 0
88
+ for block in message.content:
89
+ kind = block.type
90
+ if kind == "text":
91
+ chars += len(block.text)
92
+ elif kind == "thinking":
93
+ chars += len(block.thinking)
94
+ else:
95
+ chars += len(block.name) + len(_safe_json(block.arguments))
96
+ return math.ceil(chars / _CHARS_PER_TOKEN)
97
+
98
+
99
+ def _last_assistant_usage(messages: Sequence[Any]) -> tuple[Usage, int] | None:
100
+ latest_prefix_timestamp = -math.inf
101
+ info: tuple[Usage, int] | None = None
102
+ for index, message in enumerate(messages):
103
+ if message.role == "assistant":
104
+ applies = message.timestamp >= latest_prefix_timestamp
105
+ if (
106
+ applies
107
+ and message.stop_reason not in ("aborted", "error")
108
+ and calculate_context_tokens(message.usage) > 0
109
+ ):
110
+ info = (message.usage, index)
111
+ latest_prefix_timestamp = max(latest_prefix_timestamp, getattr(message, "timestamp", 0))
112
+ return info
113
+
114
+
115
+ def estimate_context_tokens(context: TranscriptContext | Sequence[Any]) -> ContextUsageEstimate:
116
+ """Estimate context tokens, anchored on the most recent assistant usage report."""
117
+ messages = context.messages if isinstance(context, TranscriptContext) else context
118
+ usage_info = _last_assistant_usage(messages)
119
+ if usage_info is not None:
120
+ usage, index = usage_info
121
+ usage_tokens = calculate_context_tokens(usage)
122
+ trailing = sum(estimate_message_tokens(message) for message in messages[index + 1 :])
123
+ return ContextUsageEstimate(usage_tokens + trailing, usage_tokens, trailing, index)
124
+ tokens = sum(estimate_message_tokens(message) for message in messages)
125
+ return ContextUsageEstimate(tokens, 0, tokens, None)
@@ -0,0 +1,63 @@
1
+ """JSON encode/decode with an optional ``orjson`` fast path.
2
+
3
+ Install ``pi-ai-python[fast]`` to use orjson for SSE chunk decoding and request bodies.
4
+ Output is compact and UTF-8 in both cases.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any
11
+
12
+ __all__ = ["loads", "dumps", "dumps_bytes", "HAS_ORJSON"]
13
+
14
+ try: # pragma: no cover - exercised when orjson is installed
15
+ import orjson as _orjson
16
+
17
+ HAS_ORJSON = True
18
+ except ImportError: # pragma: no cover
19
+ _orjson = None
20
+ HAS_ORJSON = False
21
+
22
+ _decode = json.JSONDecoder(strict=True).decode
23
+
24
+
25
+ def _default(value: Any) -> Any:
26
+ from ..serialization import to_json_value
27
+
28
+ return to_json_value(value)
29
+
30
+
31
+ if _orjson is not None:
32
+ # Dataclasses go through ``_default`` so they serialize in pi's camelCase format.
33
+ _ORJSON_OPTS = _orjson.OPT_NON_STR_KEYS | _orjson.OPT_PASSTHROUGH_DATACLASS
34
+
35
+ loads = _orjson.loads # pyright: ignore[reportAssignmentType]
36
+ _orjson_dumps = _orjson.dumps
37
+
38
+ def dumps_bytes(value: Any) -> bytes:
39
+ try:
40
+ return _orjson_dumps(value, default=_default, option=_ORJSON_OPTS)
41
+ except TypeError:
42
+ # orjson rejects lone surrogates and integers above 64 bits; the stdlib escapes them.
43
+ return json.dumps(value, ensure_ascii=True, separators=(",", ":"), default=_default).encode()
44
+
45
+ def dumps(value: Any) -> str:
46
+ return dumps_bytes(value).decode()
47
+
48
+ else:
49
+
50
+ def loads(data: str | bytes | bytearray | memoryview) -> Any:
51
+ if not isinstance(data, str):
52
+ data = bytes(data).decode()
53
+ return _decode(data)
54
+
55
+ def dumps(value: Any) -> str:
56
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"), default=_default)
57
+
58
+ def dumps_bytes(value: Any) -> bytes:
59
+ text = dumps(value)
60
+ try:
61
+ return text.encode()
62
+ except UnicodeEncodeError: # lone surrogates: emit them as \u escapes
63
+ return json.dumps(value, ensure_ascii=True, separators=(",", ":"), default=_default).encode()