shinychat 0.0.1a0__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.
- shinychat/__init__.py +3 -0
- shinychat/__version.py +21 -0
- shinychat/_chat.py +1878 -0
- shinychat/_chat_bookmark.py +110 -0
- shinychat/_chat_normalize.py +350 -0
- shinychat/_chat_provider_types.py +127 -0
- shinychat/_chat_tokenizer.py +67 -0
- shinychat/_chat_types.py +79 -0
- shinychat/_html_deps_py_shiny.py +41 -0
- shinychat/_markdown_stream.py +374 -0
- shinychat/_typing_extensions.py +63 -0
- shinychat/_utils.py +173 -0
- shinychat/express/__init__.py +3 -0
- shinychat/playwright/__init__.py +3 -0
- shinychat/playwright/_chat.py +154 -0
- shinychat/www/GIT_VERSION +1 -0
- shinychat/www/chat/chat.css +2 -0
- shinychat/www/chat/chat.css.map +7 -0
- shinychat/www/chat/chat.js +87 -0
- shinychat/www/chat/chat.js.map +7 -0
- shinychat/www/markdown-stream/markdown-stream.css +2 -0
- shinychat/www/markdown-stream/markdown-stream.css.map +7 -0
- shinychat/www/markdown-stream/markdown-stream.js +149 -0
- shinychat/www/markdown-stream/markdown-stream.js.map +7 -0
- shinychat-0.0.1a0.dist-info/METADATA +36 -0
- shinychat-0.0.1a0.dist-info/RECORD +28 -0
- shinychat-0.0.1a0.dist-info/WHEEL +4 -0
- shinychat-0.0.1a0.dist-info/licenses/LICENSE +21 -0
shinychat/_chat_types.py
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Literal, TypedDict
|
5
|
+
|
6
|
+
from htmltools import HTML, TagChild
|
7
|
+
from shiny.session import require_active_session
|
8
|
+
|
9
|
+
from ._typing_extensions import NotRequired
|
10
|
+
|
11
|
+
Role = Literal["assistant", "user", "system"]
|
12
|
+
|
13
|
+
|
14
|
+
# TODO: content should probably be [{"type": "text", "content": "..."}, {"type": "image", ...}]
|
15
|
+
# in order to support multiple content types...
|
16
|
+
class ChatMessageDict(TypedDict):
|
17
|
+
content: str
|
18
|
+
role: Role
|
19
|
+
|
20
|
+
|
21
|
+
class ChatMessage:
|
22
|
+
def __init__(
|
23
|
+
self,
|
24
|
+
content: TagChild,
|
25
|
+
role: Role,
|
26
|
+
):
|
27
|
+
self.role: Role = role
|
28
|
+
|
29
|
+
# content _can_ be a TagChild, but it's most likely just a string (of
|
30
|
+
# markdown), so only process it if it's not a string.
|
31
|
+
deps = []
|
32
|
+
if not isinstance(content, str):
|
33
|
+
session = require_active_session(None)
|
34
|
+
res = session._process_ui(content)
|
35
|
+
content = res["html"]
|
36
|
+
deps = res["deps"]
|
37
|
+
|
38
|
+
self.content = content
|
39
|
+
self.html_deps = deps
|
40
|
+
|
41
|
+
|
42
|
+
# A message once transformed have been applied
|
43
|
+
@dataclass
|
44
|
+
class TransformedMessage:
|
45
|
+
content_client: str | HTML
|
46
|
+
content_server: str
|
47
|
+
role: Role
|
48
|
+
transform_key: Literal["content_client", "content_server"]
|
49
|
+
pre_transform_key: Literal["content_client", "content_server"]
|
50
|
+
html_deps: list[dict[str, str]] | None = None
|
51
|
+
|
52
|
+
@classmethod
|
53
|
+
def from_chat_message(cls, message: ChatMessage) -> "TransformedMessage":
|
54
|
+
if message.role == "user":
|
55
|
+
transform_key = "content_server"
|
56
|
+
pre_transform_key = "content_client"
|
57
|
+
else:
|
58
|
+
transform_key = "content_client"
|
59
|
+
pre_transform_key = "content_server"
|
60
|
+
|
61
|
+
return TransformedMessage(
|
62
|
+
content_client=message.content,
|
63
|
+
content_server=message.content,
|
64
|
+
role=message.role,
|
65
|
+
transform_key=transform_key,
|
66
|
+
pre_transform_key=pre_transform_key,
|
67
|
+
html_deps=message.html_deps,
|
68
|
+
)
|
69
|
+
|
70
|
+
|
71
|
+
# A message that can be sent to the client
|
72
|
+
class ClientMessage(TypedDict):
|
73
|
+
content: str
|
74
|
+
role: Literal["assistant", "user"]
|
75
|
+
content_type: Literal["markdown", "html"]
|
76
|
+
chunk_type: Literal["message_start", "message_end"] | None
|
77
|
+
operation: Literal["append", "replace"]
|
78
|
+
icon: NotRequired[str]
|
79
|
+
html_deps: NotRequired[list[dict[str, str]]]
|
@@ -0,0 +1,41 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from htmltools import HTMLDependency
|
4
|
+
|
5
|
+
from .__version import __version__
|
6
|
+
|
7
|
+
"""
|
8
|
+
HTML dependencies for internal dependencies such as dataframe.
|
9
|
+
|
10
|
+
For...
|
11
|
+
* External dependencies (e.g. jQuery, Bootstrap), see `shiny.ui._html_deps_external`
|
12
|
+
* Internal dependencies (e.g. dataframe), see `shiny.ui._html_deps_py_shiny`
|
13
|
+
* shinyverse dependencies (e.g. bslib, htmltools), see `shiny.ui._html_deps_shinyverse`
|
14
|
+
"""
|
15
|
+
|
16
|
+
|
17
|
+
def chat_deps() -> list[HTMLDependency]:
|
18
|
+
dep = HTMLDependency(
|
19
|
+
name="shinychat-chat",
|
20
|
+
version=__version__,
|
21
|
+
source={
|
22
|
+
"package": "shinychat",
|
23
|
+
"subdir": "www/chat",
|
24
|
+
},
|
25
|
+
script={"src": "chat.js", "type": "module"},
|
26
|
+
stylesheet={"href": "chat.css"},
|
27
|
+
)
|
28
|
+
return [dep, markdown_stream_dependency()]
|
29
|
+
|
30
|
+
|
31
|
+
def markdown_stream_dependency() -> HTMLDependency:
|
32
|
+
return HTMLDependency(
|
33
|
+
name="shinychat-markdown",
|
34
|
+
version=__version__,
|
35
|
+
source={
|
36
|
+
"package": "shinychat",
|
37
|
+
"subdir": "www/markdown-stream",
|
38
|
+
},
|
39
|
+
script={"src": "markdown-stream.js", "type": "module"},
|
40
|
+
stylesheet={"href": "markdown-stream.css"},
|
41
|
+
)
|
@@ -0,0 +1,374 @@
|
|
1
|
+
from contextlib import asynccontextmanager
|
2
|
+
from typing import AsyncIterable, Iterable, Literal, Union
|
3
|
+
|
4
|
+
from htmltools import RenderedHTML, Tag, TagChild, TagList, css
|
5
|
+
from shiny import _utils, reactive
|
6
|
+
from shiny._deprecated import warn_deprecated
|
7
|
+
from shiny.module import resolve_id
|
8
|
+
from shiny.session import require_active_session, session_context
|
9
|
+
from shiny.session._utils import RenderedDeps
|
10
|
+
from shiny.types import NotifyException
|
11
|
+
from shiny.ui.css import CssUnit, as_css_unit
|
12
|
+
|
13
|
+
from ._html_deps_py_shiny import markdown_stream_dependency
|
14
|
+
from ._typing_extensions import TypedDict
|
15
|
+
|
16
|
+
__all__ = (
|
17
|
+
"output_markdown_stream",
|
18
|
+
"MarkdownStream",
|
19
|
+
"ExpressMarkdownStream",
|
20
|
+
)
|
21
|
+
|
22
|
+
StreamingContentType = Literal[
|
23
|
+
"markdown",
|
24
|
+
"html",
|
25
|
+
"semi-markdown",
|
26
|
+
"text",
|
27
|
+
]
|
28
|
+
|
29
|
+
|
30
|
+
class ContentMessage(TypedDict):
|
31
|
+
id: str
|
32
|
+
content: str
|
33
|
+
operation: Literal["append", "replace"]
|
34
|
+
html_deps: list[dict[str, str]]
|
35
|
+
|
36
|
+
|
37
|
+
class isStreamingMessage(TypedDict):
|
38
|
+
id: str
|
39
|
+
isStreaming: bool
|
40
|
+
|
41
|
+
|
42
|
+
class MarkdownStream:
|
43
|
+
"""
|
44
|
+
A component for streaming markdown or HTML content.
|
45
|
+
|
46
|
+
Parameters
|
47
|
+
----------
|
48
|
+
id
|
49
|
+
A unique identifier for this `MarkdownStream`. In Shiny Core, make sure this id
|
50
|
+
matches a corresponding :func:`~shiny.ui.output_markdown_stream` call in the app's
|
51
|
+
UI.
|
52
|
+
on_error
|
53
|
+
How to handle errors that occur while streaming. When `"unhandled"`,
|
54
|
+
the app will stop running when an error occurs. Otherwise, a notification
|
55
|
+
is displayed to the user and the app continues to run.
|
56
|
+
|
57
|
+
* `"auto"`: Sanitize the error message if the app is set to sanitize errors,
|
58
|
+
otherwise display the actual error message.
|
59
|
+
* `"actual"`: Display the actual error message to the user.
|
60
|
+
* `"sanitize"`: Sanitize the error message before displaying it to the user.
|
61
|
+
* `"unhandled"`: Do not display any error message to the user.
|
62
|
+
|
63
|
+
Note
|
64
|
+
----
|
65
|
+
Markdown is parsed on the client via `marked.js`. Consider using
|
66
|
+
:func:`~shiny.ui.markdown` for server-side rendering of markdown content.
|
67
|
+
"""
|
68
|
+
|
69
|
+
def __init__(
|
70
|
+
self,
|
71
|
+
id: str,
|
72
|
+
*,
|
73
|
+
on_error: Literal["auto", "actual", "sanitize", "unhandled"] = "auto",
|
74
|
+
):
|
75
|
+
self.id = resolve_id(id)
|
76
|
+
# TODO: remove the `None` when this PR lands:
|
77
|
+
# https://github.com/posit-dev/py-shiny/pull/793/files
|
78
|
+
self._session = require_active_session(None)
|
79
|
+
|
80
|
+
# Default to sanitizing until we know the app isn't sanitizing errors
|
81
|
+
if on_error == "auto":
|
82
|
+
on_error = "sanitize"
|
83
|
+
app = self._session.app
|
84
|
+
if app is not None and not app.sanitize_errors: # type: ignore
|
85
|
+
on_error = "actual"
|
86
|
+
|
87
|
+
self.on_error = on_error
|
88
|
+
|
89
|
+
with session_context(self._session):
|
90
|
+
|
91
|
+
@reactive.extended_task
|
92
|
+
async def _mock_task() -> str:
|
93
|
+
return ""
|
94
|
+
|
95
|
+
self._latest_stream: reactive.Value[reactive.ExtendedTask[[], str]] = (
|
96
|
+
reactive.Value(_mock_task)
|
97
|
+
)
|
98
|
+
|
99
|
+
async def stream(
|
100
|
+
self,
|
101
|
+
content: Union[Iterable[TagChild], AsyncIterable[TagChild]],
|
102
|
+
clear: bool = True,
|
103
|
+
):
|
104
|
+
"""
|
105
|
+
Send a stream of content to the UI.
|
106
|
+
|
107
|
+
Stream content into the relevant UI element.
|
108
|
+
|
109
|
+
Parameters
|
110
|
+
----------
|
111
|
+
content
|
112
|
+
The content to stream. This can be a Iterable or an AsyncIterable of strings.
|
113
|
+
Note that this includes synchronous and asynchronous generators, which is
|
114
|
+
a useful way to stream content in as it arrives (e.g. from a LLM).
|
115
|
+
clear
|
116
|
+
Whether to clear the existing content before streaming the new content.
|
117
|
+
|
118
|
+
Note
|
119
|
+
----
|
120
|
+
If you already have the content available as a string, you can do
|
121
|
+
`.stream([content])` to set the content.
|
122
|
+
|
123
|
+
Returns
|
124
|
+
-------
|
125
|
+
:
|
126
|
+
An extended task that represents the streaming task. The `.result()` method
|
127
|
+
of the task can be called in a reactive context to get the final state of the
|
128
|
+
stream.
|
129
|
+
"""
|
130
|
+
|
131
|
+
content = _utils.wrap_async_iterable(content)
|
132
|
+
|
133
|
+
@reactive.extended_task
|
134
|
+
async def _task():
|
135
|
+
if clear:
|
136
|
+
await self._send_content_message("", "replace", [])
|
137
|
+
|
138
|
+
result = ""
|
139
|
+
async with self._streaming_dot():
|
140
|
+
async for x in content:
|
141
|
+
if isinstance(x, str):
|
142
|
+
# x is most likely a string, so avoid overhead in that case
|
143
|
+
ui: RenderedDeps = {"html": x, "deps": []}
|
144
|
+
else:
|
145
|
+
# process_ui() does *not* render markdown->HTML, but it does:
|
146
|
+
# 1. Extract and register HTMLdependency()s with the session.
|
147
|
+
# 2. Returns a HTML string representation of the TagChild
|
148
|
+
# (i.e., `div()` -> `"<div>"`).
|
149
|
+
ui = self._session._process_ui(x)
|
150
|
+
|
151
|
+
result += ui["html"]
|
152
|
+
await self._send_content_message(ui["html"], "append", ui["deps"])
|
153
|
+
|
154
|
+
return result
|
155
|
+
|
156
|
+
_task()
|
157
|
+
|
158
|
+
self._latest_stream.set(_task)
|
159
|
+
|
160
|
+
# Since the task runs in the background (outside/beyond the current context,
|
161
|
+
# if any), we need to manually raise any exceptions that occur
|
162
|
+
@reactive.effect
|
163
|
+
async def _handle_error():
|
164
|
+
e = _task.error()
|
165
|
+
if e:
|
166
|
+
await self._raise_exception(e)
|
167
|
+
_handle_error.destroy() # type: ignore
|
168
|
+
|
169
|
+
return _task
|
170
|
+
|
171
|
+
@property
|
172
|
+
def latest_stream(self):
|
173
|
+
"""
|
174
|
+
React to changes in the latest stream.
|
175
|
+
|
176
|
+
Reactively reads for the :class:`~shiny.reactive.ExtendedTask` behind the
|
177
|
+
latest stream.
|
178
|
+
|
179
|
+
From the return value (i.e., the extended task), you can then:
|
180
|
+
|
181
|
+
1. Reactively read for the final `.result()`.
|
182
|
+
2. `.cancel()` the stream.
|
183
|
+
3. Check the `.status()` of the stream.
|
184
|
+
|
185
|
+
Returns
|
186
|
+
-------
|
187
|
+
:
|
188
|
+
An extended task that represents the streaming task. The `.result()` method
|
189
|
+
of the task can be called in a reactive context to get the final state of the
|
190
|
+
stream.
|
191
|
+
|
192
|
+
Note
|
193
|
+
----
|
194
|
+
If no stream has yet been started when this method is called, then it returns an
|
195
|
+
extended task with `.status()` of `"initial"` and that it status doesn't change
|
196
|
+
state until a message is streamed.
|
197
|
+
"""
|
198
|
+
return self._latest_stream()
|
199
|
+
|
200
|
+
def get_latest_stream_result(self) -> Union[str, None]:
|
201
|
+
"""
|
202
|
+
Reactively read the latest stream result.
|
203
|
+
|
204
|
+
Deprecated. Use `latest_stream.result()` instead.
|
205
|
+
"""
|
206
|
+
warn_deprecated(
|
207
|
+
"The `.get_latest_stream_result()` method is deprecated and will be removed "
|
208
|
+
"in a future release. Use `.latest_stream.result()` instead. "
|
209
|
+
)
|
210
|
+
return self.latest_stream.result()
|
211
|
+
|
212
|
+
async def clear(self):
|
213
|
+
"""
|
214
|
+
Empty the UI element of the `MarkdownStream`.
|
215
|
+
"""
|
216
|
+
return await self.stream([], clear=True)
|
217
|
+
|
218
|
+
@asynccontextmanager
|
219
|
+
async def _streaming_dot(self):
|
220
|
+
await self._send_stream_message(True)
|
221
|
+
try:
|
222
|
+
yield
|
223
|
+
finally:
|
224
|
+
await self._send_stream_message(False)
|
225
|
+
|
226
|
+
async def _send_content_message(
|
227
|
+
self,
|
228
|
+
content: str,
|
229
|
+
operation: Literal["append", "replace"],
|
230
|
+
html_deps: list[dict[str, str]],
|
231
|
+
):
|
232
|
+
msg: ContentMessage = {
|
233
|
+
"id": self.id,
|
234
|
+
"content": content,
|
235
|
+
"operation": operation,
|
236
|
+
"html_deps": html_deps,
|
237
|
+
}
|
238
|
+
await self._send_custom_message(msg)
|
239
|
+
|
240
|
+
async def _send_stream_message(self, is_streaming: bool):
|
241
|
+
msg: isStreamingMessage = {
|
242
|
+
"id": self.id,
|
243
|
+
"isStreaming": is_streaming,
|
244
|
+
}
|
245
|
+
await self._send_custom_message(msg)
|
246
|
+
|
247
|
+
async def _send_custom_message(
|
248
|
+
self, msg: Union[ContentMessage, isStreamingMessage]
|
249
|
+
):
|
250
|
+
if self._session.is_stub_session():
|
251
|
+
return
|
252
|
+
await self._session.send_custom_message("shinyMarkdownStreamMessage", {**msg})
|
253
|
+
|
254
|
+
async def _raise_exception(self, e: BaseException):
|
255
|
+
if self.on_error == "unhandled":
|
256
|
+
raise e
|
257
|
+
else:
|
258
|
+
sanitize = self.on_error == "sanitize"
|
259
|
+
msg = f"Error in MarkdownStream('{self.id}'): {str(e)}"
|
260
|
+
raise NotifyException(msg, sanitize=sanitize) from e
|
261
|
+
|
262
|
+
|
263
|
+
class ExpressMarkdownStream(MarkdownStream):
|
264
|
+
def ui(
|
265
|
+
self,
|
266
|
+
*,
|
267
|
+
content: TagChild = "",
|
268
|
+
content_type: StreamingContentType = "markdown",
|
269
|
+
auto_scroll: bool = True,
|
270
|
+
width: CssUnit = "min(680px, 100%)",
|
271
|
+
height: CssUnit = "auto",
|
272
|
+
) -> Tag:
|
273
|
+
"""
|
274
|
+
Create a UI element for this `MarkdownStream`.
|
275
|
+
|
276
|
+
Parameters
|
277
|
+
----------
|
278
|
+
content
|
279
|
+
A string of content to display before any streaming occurs. When
|
280
|
+
`content_type` is Markdown or HTML, it may also be UI element(s) such as
|
281
|
+
input and output bindings.
|
282
|
+
content_type
|
283
|
+
The content type. Default is `"markdown"` (specifically, CommonMark).
|
284
|
+
Supported content types include:
|
285
|
+
- `"markdown"`: markdown text, specifically CommonMark
|
286
|
+
- `"html"`: for rendering HTML content.
|
287
|
+
- `"text"`: for plain text.
|
288
|
+
- `"semi-markdown"`: for rendering markdown, but with HTML tags escaped.
|
289
|
+
auto_scroll
|
290
|
+
Whether to automatically scroll to the bottom of a scrollable container
|
291
|
+
when new content is added. Default is `True`.
|
292
|
+
width
|
293
|
+
The width of the UI element.
|
294
|
+
height
|
295
|
+
The height of the UI element.
|
296
|
+
|
297
|
+
Returns
|
298
|
+
-------
|
299
|
+
Tag
|
300
|
+
A UI element for locating the `MarkdownStream` in the app.
|
301
|
+
"""
|
302
|
+
return output_markdown_stream(
|
303
|
+
self.id,
|
304
|
+
content=content,
|
305
|
+
content_type=content_type,
|
306
|
+
auto_scroll=auto_scroll,
|
307
|
+
width=width,
|
308
|
+
height=height,
|
309
|
+
)
|
310
|
+
|
311
|
+
|
312
|
+
def output_markdown_stream(
|
313
|
+
id: str,
|
314
|
+
*,
|
315
|
+
content: TagChild = "",
|
316
|
+
content_type: StreamingContentType = "markdown",
|
317
|
+
auto_scroll: bool = True,
|
318
|
+
width: CssUnit = "min(680px, 100%)",
|
319
|
+
height: CssUnit = "auto",
|
320
|
+
) -> Tag:
|
321
|
+
"""
|
322
|
+
Create a UI element for a :class:`~shiny.ui.MarkdownStream`.
|
323
|
+
|
324
|
+
This function is only relevant for Shiny Core. In Shiny Express, use
|
325
|
+
:meth:`~shiny.express.ui.MarkdownStream.ui` to create the UI element.
|
326
|
+
|
327
|
+
Parameters
|
328
|
+
----------
|
329
|
+
id
|
330
|
+
A unique identifier for the UI element. This id should match the id of the
|
331
|
+
:class:`~shiny.ui.MarkdownStream` instance.
|
332
|
+
content
|
333
|
+
A string of content to display before any streaming occurs. When `content_type`
|
334
|
+
is Markdown or HTML, it may also be UI element(s) such as input and output
|
335
|
+
bindings.
|
336
|
+
content_type
|
337
|
+
The content type. Default is "markdown" (specifically, CommonMark). Supported
|
338
|
+
content types include:
|
339
|
+
- `"markdown"`: markdown text, specifically CommonMark
|
340
|
+
- `"html"`: for rendering HTML content.
|
341
|
+
- `"text"`: for plain text.
|
342
|
+
- `"semi-markdown"`: for rendering markdown, but with HTML tags escaped.
|
343
|
+
auto_scroll
|
344
|
+
Whether to automatically scroll to the bottom of a scrollable container
|
345
|
+
when new content is added. Default is True.
|
346
|
+
width
|
347
|
+
The width of the UI element.
|
348
|
+
height
|
349
|
+
The height of the UI element.
|
350
|
+
"""
|
351
|
+
|
352
|
+
# `content` is most likely a string, so avoid overhead in that case
|
353
|
+
# (it's also important that we *don't escape HTML* here).
|
354
|
+
if isinstance(content, str):
|
355
|
+
ui: RenderedHTML = {"html": content, "dependencies": []}
|
356
|
+
else:
|
357
|
+
ui = TagList(content).render()
|
358
|
+
|
359
|
+
return Tag(
|
360
|
+
"shiny-markdown-stream",
|
361
|
+
markdown_stream_dependency(),
|
362
|
+
ui["dependencies"],
|
363
|
+
{
|
364
|
+
"style": css(
|
365
|
+
width=as_css_unit(width),
|
366
|
+
height=as_css_unit(height),
|
367
|
+
margin="0 auto",
|
368
|
+
),
|
369
|
+
"content-type": content_type,
|
370
|
+
"auto-scroll": "" if auto_scroll else None,
|
371
|
+
},
|
372
|
+
id=resolve_id(id),
|
373
|
+
content=ui["html"],
|
374
|
+
)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# # Within file flags to ignore unused imports
|
2
|
+
# flake8: noqa: F401
|
3
|
+
# pyright: reportUnusedImport=false
|
4
|
+
|
5
|
+
__all__ = (
|
6
|
+
"Annotated",
|
7
|
+
"Concatenate",
|
8
|
+
"ParamSpec",
|
9
|
+
"TypeGuard",
|
10
|
+
"TypeIs",
|
11
|
+
"Never",
|
12
|
+
"Required",
|
13
|
+
"NotRequired",
|
14
|
+
"Self",
|
15
|
+
"TypedDict",
|
16
|
+
"assert_type",
|
17
|
+
)
|
18
|
+
|
19
|
+
|
20
|
+
import sys
|
21
|
+
|
22
|
+
if sys.version_info >= (3, 9):
|
23
|
+
from typing import Annotated
|
24
|
+
else:
|
25
|
+
from typing_extensions import Annotated
|
26
|
+
|
27
|
+
if sys.version_info >= (3, 10):
|
28
|
+
from typing import Concatenate, ParamSpec, TypeGuard
|
29
|
+
else:
|
30
|
+
from typing_extensions import Concatenate, ParamSpec, TypeGuard
|
31
|
+
|
32
|
+
# Even though TypedDict is available in Python 3.8, because it's used with NotRequired,
|
33
|
+
# they should both come from the same typing module.
|
34
|
+
# https://peps.python.org/pep-0655/#usage-in-python-3-11
|
35
|
+
if sys.version_info >= (3, 11):
|
36
|
+
from typing import (
|
37
|
+
Never,
|
38
|
+
NotRequired,
|
39
|
+
Required,
|
40
|
+
Self,
|
41
|
+
TypedDict,
|
42
|
+
assert_type,
|
43
|
+
)
|
44
|
+
else:
|
45
|
+
from typing_extensions import (
|
46
|
+
Never,
|
47
|
+
NotRequired,
|
48
|
+
Required,
|
49
|
+
Self,
|
50
|
+
TypedDict,
|
51
|
+
assert_type,
|
52
|
+
)
|
53
|
+
|
54
|
+
if sys.version_info >= (3, 13):
|
55
|
+
from typing import TypeIs
|
56
|
+
else:
|
57
|
+
from typing_extensions import TypeIs
|
58
|
+
|
59
|
+
# The only purpose of the following line is so that pyright will put all of the
|
60
|
+
# conditional imports into the .pyi file when generating type stubs. Without this line,
|
61
|
+
# pyright will not include the above imports in the generated .pyi file, and it will
|
62
|
+
# result in a lot of red squiggles in user code.
|
63
|
+
_: 'Annotated |Concatenate[str, ParamSpec("P")] | ParamSpec | TypeGuard | TypeIs | NotRequired | Required | TypedDict | assert_type | Self' # type:ignore
|