Hussnainkha commited on
Commit
410bfe2
1 Parent(s): 5ead018

Upload 3 files

Browse files
Files changed (3) hide show
  1. Logo 3.jpg +0 -0
  2. main.py +84 -0
  3. runtime.py +749 -0
Logo 3.jpg ADDED
main.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ import openai
4
+ import streamlit as st
5
+ from openai import OpenAI
6
+
7
+ openai.api_key = os.environ['OPENAI_API_KEY']
8
+
9
+ from openai import OpenAI
10
+ client = OpenAI(
11
+ api_key=os.environ['OPENAI_API_KEY'], # this is also the default, it can be omitted
12
+ )
13
+
14
+ # Function to generate the recipe and detailed instructions
15
+ def create_dish_prompt(ingredients):
16
+ prompt = "Make a dish with the following ingredients: " + ', '.join(
17
+ ingredients) + ".\nWrite down the title, detailed recipe, and a description of the image you want to generate.\n"
18
+ return prompt
19
+
20
+
21
+ # Function to generate the image
22
+ def generate_dalle_image(recipe_title):
23
+ response = client.images.generate(
24
+ model="dall-e-3",
25
+ prompt=recipe_title,
26
+ size="1024x1024",
27
+ quality="standard",
28
+ n=1,
29
+ )
30
+ url = response.data[0].url if response.data else None
31
+ return url
32
+
33
+
34
+ # Initialize Streamlit
35
+
36
+ st.image("logo 3.jpg")
37
+ st.title("AutoChef.AI")
38
+
39
+ # Create a text input box for users to input ingredients
40
+ ingredients = st.text_input("Enter your ingredients (comma-separated):", "")
41
+
42
+ # Check if the user has entered ingredients and clicked on the 'Generate Recipe' button
43
+ if ingredients:
44
+ # Convert the input string into a list of ingredients
45
+ list_of_ingredients = [item.strip() for item in ingredients.split(',')]
46
+
47
+ # Call your existing functions to generate the recipe and image
48
+ recipe = create_dish_prompt(list_of_ingredients)
49
+ completion = client.chat.completions.create(
50
+ messages=[
51
+ {
52
+ "role": "user",
53
+ "content": recipe,
54
+ }
55
+ ],
56
+ model="gpt-3.5-turbo",
57
+ )
58
+ content = completion.choices[0].message.content
59
+ split_content = content.split('\n', 1)
60
+ recipe_title = split_content[0].strip()
61
+ detailed_recipe = split_content[1].strip()
62
+ url = generate_dalle_image(recipe_title)
63
+
64
+ # Display the recipe title and detailed recipe
65
+ st.title(recipe_title)
66
+ st.subheader("A comprehensive culinary guide outlining the step-by-step procedure for preparing a dish.")
67
+ st.write(detailed_recipe)
68
+
69
+ # Display the image if a URL exists
70
+ if url:
71
+ st.image(url)
72
+ else:
73
+ st.write("No image URL generated.")
74
+ else:
75
+ st.write("Please enter the ingredients and click on the 'Generate Recipe' button.")
76
+
77
+ # Code for your app
78
+ # st.title("Streamlit(app).py")
79
+ #
80
+ # # Code to display the app address
81
+ # local_url = f"http://localhost:8501"
82
+ # st.text(f"View this app at {local_url}")
83
+
84
+
runtime.py ADDED
@@ -0,0 +1,749 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ import time
19
+ import traceback
20
+ from dataclasses import dataclass, field
21
+ from enum import Enum
22
+ from typing import TYPE_CHECKING, Awaitable, Dict, NamedTuple, Optional, Tuple, Type
23
+
24
+ from typing_extensions import Final
25
+
26
+ from streamlit import config
27
+ from streamlit.logger import get_logger
28
+ from streamlit.proto.BackMsg_pb2 import BackMsg
29
+ from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
30
+ from streamlit.runtime.app_session import AppSession
31
+ from streamlit.runtime.caching import (
32
+ get_data_cache_stats_provider,
33
+ get_resource_cache_stats_provider,
34
+ )
35
+ from streamlit.runtime.caching.storage.local_disk_cache_storage import (
36
+ LocalDiskCacheStorageManager,
37
+ )
38
+ from streamlit.runtime.forward_msg_cache import (
39
+ ForwardMsgCache,
40
+ create_reference_msg,
41
+ populate_hash_if_needed,
42
+ )
43
+ from streamlit.runtime.legacy_caching.caching import _mem_caches
44
+ from streamlit.runtime.media_file_manager import MediaFileManager
45
+ from streamlit.runtime.media_file_storage import MediaFileStorage
46
+ from streamlit.runtime.memory_session_storage import MemorySessionStorage
47
+ from streamlit.runtime.runtime_util import is_cacheable_msg
48
+ from streamlit.runtime.script_data import ScriptData
49
+ from streamlit.runtime.scriptrunner.script_cache import ScriptCache
50
+ from streamlit.runtime.session_manager import (
51
+ ActiveSessionInfo,
52
+ SessionClient,
53
+ SessionClientDisconnectedError,
54
+ SessionManager,
55
+ SessionStorage,
56
+ )
57
+ from streamlit.runtime.state import (
58
+ SCRIPT_RUN_WITHOUT_ERRORS_KEY,
59
+ SessionStateStatProvider,
60
+ )
61
+ from streamlit.runtime.stats import StatsManager
62
+ from streamlit.runtime.uploaded_file_manager import UploadedFileManager
63
+ from streamlit.runtime.websocket_session_manager import WebsocketSessionManager
64
+ from streamlit.watcher import LocalSourcesWatcher
65
+
66
+ if TYPE_CHECKING:
67
+ from streamlit.runtime.caching.storage import CacheStorageManager
68
+
69
+ # Wait for the script run result for 60s and if no result is available give up
70
+ SCRIPT_RUN_CHECK_TIMEOUT: Final = 60
71
+
72
+ LOGGER: Final = get_logger(__name__)
73
+
74
+
75
+ class RuntimeStoppedError(Exception):
76
+ """Raised by operations on a Runtime instance that is stopped."""
77
+
78
+
79
+ @dataclass(frozen=True)
80
+ class RuntimeConfig:
81
+ """Config options for StreamlitRuntime."""
82
+
83
+ # The filesystem path of the Streamlit script to run.
84
+ script_path: str
85
+
86
+ # The (optional) command line that Streamlit was started with
87
+ # (e.g. "streamlit run app.py")
88
+ command_line: Optional[str]
89
+
90
+ # The storage backend for Streamlit's MediaFileManager.
91
+ media_file_storage: MediaFileStorage
92
+
93
+ # The upload file manager
94
+ uploaded_file_manager: UploadedFileManager
95
+
96
+ # The cache storage backend for Streamlit's st.cache_data.
97
+ cache_storage_manager: CacheStorageManager = field(
98
+ default_factory=LocalDiskCacheStorageManager
99
+ )
100
+
101
+ # The SessionManager class to be used.
102
+ session_manager_class: Type[SessionManager] = WebsocketSessionManager
103
+
104
+ # The SessionStorage instance for the SessionManager to use.
105
+ session_storage: SessionStorage = field(default_factory=MemorySessionStorage)
106
+
107
+
108
+ class RuntimeState(Enum):
109
+ INITIAL = "INITIAL"
110
+ NO_SESSIONS_CONNECTED = "NO_SESSIONS_CONNECTED"
111
+ ONE_OR_MORE_SESSIONS_CONNECTED = "ONE_OR_MORE_SESSIONS_CONNECTED"
112
+ STOPPING = "STOPPING"
113
+ STOPPED = "STOPPED"
114
+
115
+
116
+ class AsyncObjects(NamedTuple):
117
+ """Container for all asyncio objects that Runtime manages.
118
+ These cannot be initialized until the Runtime's eventloop is assigned.
119
+ """
120
+
121
+ # The eventloop that Runtime is running on.
122
+ eventloop: asyncio.AbstractEventLoop
123
+
124
+ # Set after Runtime.stop() is called. Never cleared.
125
+ must_stop: asyncio.Event
126
+
127
+ # Set when a client connects; cleared when we have no connected clients.
128
+ has_connection: asyncio.Event
129
+
130
+ # Set after a ForwardMsg is enqueued; cleared when we flush ForwardMsgs.
131
+ need_send_data: asyncio.Event
132
+
133
+ # Completed when the Runtime has started.
134
+ started: asyncio.Future[None]
135
+
136
+ # Completed when the Runtime has stopped.
137
+ stopped: asyncio.Future[None]
138
+
139
+
140
+ class Runtime:
141
+ _instance: Optional[Runtime] = None
142
+
143
+ @classmethod
144
+ def instance(cls) -> Runtime:
145
+ """Return the singleton Runtime instance. Raise an Error if the
146
+ Runtime hasn't been created yet.
147
+ """
148
+ if cls._instance is None:
149
+ raise RuntimeError("Runtime hasn't been created!")
150
+ return cls._instance
151
+
152
+ @classmethod
153
+ def exists(cls) -> bool:
154
+ """True if the singleton Runtime instance has been created.
155
+
156
+ When a Streamlit app is running in "raw mode" - that is, when the
157
+ app is run via `python app.py` instead of `streamlit run app.py` -
158
+ the Runtime will not exist, and various Streamlit functions need
159
+ to adapt.
160
+ """
161
+ return cls._instance is not None
162
+
163
+ def __init__(self, config: RuntimeConfig):
164
+ """Create a Runtime instance. It won't be started yet.
165
+
166
+ Runtime is *not* thread-safe. Its public methods are generally
167
+ safe to call only on the same thread that its event loop runs on.
168
+
169
+ Parameters
170
+ ----------
171
+ config
172
+ Config options.
173
+ """
174
+ if Runtime._instance is not None:
175
+ raise RuntimeError("Runtime instance already exists!")
176
+ Runtime._instance = self
177
+
178
+ # Will be created when we start.
179
+ self._async_objs: Optional[AsyncObjects] = None
180
+
181
+ # The task that runs our main loop. We need to save a reference
182
+ # to it so that it doesn't get garbage collected while running.
183
+ self._loop_coroutine_task: Optional[asyncio.Task[None]] = None
184
+
185
+ self._main_script_path = config.script_path
186
+ self._command_line = config.command_line or ""
187
+
188
+ self._state = RuntimeState.INITIAL
189
+
190
+ # Initialize managers
191
+ self._message_cache = ForwardMsgCache()
192
+ self._uploaded_file_mgr = config.uploaded_file_manager
193
+ self._media_file_mgr = MediaFileManager(storage=config.media_file_storage)
194
+ self._cache_storage_manager = config.cache_storage_manager
195
+ self._script_cache = ScriptCache()
196
+
197
+ self._session_mgr = config.session_manager_class(
198
+ session_storage=config.session_storage,
199
+ uploaded_file_manager=self._uploaded_file_mgr,
200
+ script_cache=self._script_cache,
201
+ message_enqueued_callback=self._enqueued_some_message,
202
+ )
203
+
204
+ self._stats_mgr = StatsManager()
205
+ self._stats_mgr.register_provider(get_data_cache_stats_provider())
206
+ self._stats_mgr.register_provider(get_resource_cache_stats_provider())
207
+ self._stats_mgr.register_provider(_mem_caches)
208
+ self._stats_mgr.register_provider(self._message_cache)
209
+ self._stats_mgr.register_provider(self._uploaded_file_mgr)
210
+ self._stats_mgr.register_provider(SessionStateStatProvider(self._session_mgr))
211
+
212
+ @property
213
+ def state(self) -> RuntimeState:
214
+ return self._state
215
+
216
+ @property
217
+ def message_cache(self) -> ForwardMsgCache:
218
+ return self._message_cache
219
+
220
+ @property
221
+ def uploaded_file_mgr(self) -> UploadedFileManager:
222
+ return self._uploaded_file_mgr
223
+
224
+ @property
225
+ def cache_storage_manager(self) -> CacheStorageManager:
226
+ return self._cache_storage_manager
227
+
228
+ @property
229
+ def media_file_mgr(self) -> MediaFileManager:
230
+ return self._media_file_mgr
231
+
232
+ @property
233
+ def stats_mgr(self) -> StatsManager:
234
+ return self._stats_mgr
235
+
236
+ @property
237
+ def stopped(self) -> Awaitable[None]:
238
+ """A Future that completes when the Runtime's run loop has exited."""
239
+ return self._get_async_objs().stopped
240
+
241
+ # NOTE: A few Runtime methods listed as threadsafe (get_client and
242
+ # is_active_session) currently rely on the implementation detail that
243
+ # WebsocketSessionManager's get_active_session_info and is_active_session methods
244
+ # happen to be threadsafe. This may change with future SessionManager implementations,
245
+ # at which point we'll need to formalize our thread safety rules for each
246
+ # SessionManager method.
247
+ def get_client(self, session_id: str) -> Optional[SessionClient]:
248
+ """Get the SessionClient for the given session_id, or None
249
+ if no such session exists.
250
+
251
+ Notes
252
+ -----
253
+ Threading: SAFE. May be called on any thread.
254
+ """
255
+ session_info = self._session_mgr.get_active_session_info(session_id)
256
+ if session_info is None:
257
+ return None
258
+ return session_info.client
259
+
260
+ async def start(self) -> None:
261
+ """Start the runtime. This must be called only once, before
262
+ any other functions are called.
263
+
264
+ When this coroutine returns, Streamlit is ready to accept new sessions.
265
+
266
+ Notes
267
+ -----
268
+ Threading: UNSAFE. Must be called on the eventloop thread.
269
+ """
270
+
271
+ # Create our AsyncObjects. We need to have a running eventloop to
272
+ # instantiate our various synchronization primitives.
273
+ async_objs = AsyncObjects(
274
+ eventloop=asyncio.get_running_loop(),
275
+ must_stop=asyncio.Event(),
276
+ has_connection=asyncio.Event(),
277
+ need_send_data=asyncio.Event(),
278
+ started=asyncio.Future(),
279
+ stopped=asyncio.Future(),
280
+ )
281
+ self._async_objs = async_objs
282
+
283
+ self._loop_coroutine_task = asyncio.create_task(
284
+ self._loop_coroutine(), name="Runtime.loop_coroutine"
285
+ )
286
+
287
+ await async_objs.started
288
+
289
+ def stop(self) -> None:
290
+ """Request that Streamlit close all sessions and stop running.
291
+ Note that Streamlit won't stop running immediately.
292
+
293
+ Notes
294
+ -----
295
+ Threading: SAFE. May be called from any thread.
296
+ """
297
+
298
+ async_objs = self._get_async_objs()
299
+
300
+ def stop_on_eventloop():
301
+ if self._state in (RuntimeState.STOPPING, RuntimeState.STOPPED):
302
+ return
303
+
304
+ LOGGER.debug("Runtime stopping...")
305
+ self._set_state(RuntimeState.STOPPING)
306
+ async_objs.must_stop.set()
307
+
308
+ async_objs.eventloop.call_soon_threadsafe(stop_on_eventloop)
309
+
310
+ def is_active_session(self, session_id: str) -> bool:
311
+ """True if the session_id belongs to an active session.
312
+
313
+ Notes
314
+ -----
315
+ Threading: SAFE. May be called on any thread.
316
+ """
317
+ return self._session_mgr.is_active_session(session_id)
318
+
319
+ def connect_session(
320
+ self,
321
+ client: SessionClient,
322
+ user_info: Dict[str, Optional[str]],
323
+ existing_session_id: Optional[str] = None,
324
+ session_id_override: Optional[str] = None,
325
+ ) -> str:
326
+ """Create a new session (or connect to an existing one) and return its unique ID.
327
+
328
+ Parameters
329
+ ----------
330
+ client
331
+ A concrete SessionClient implementation for communicating with
332
+ the session's client.
333
+ user_info
334
+ A dict that contains information about the session's user. For now,
335
+ it only (optionally) contains the user's email address.
336
+
337
+ {
338
+ "email": "example@example.com"
339
+ }
340
+ existing_session_id
341
+ The ID of an existing session to reconnect to. If one is not provided, a new
342
+ session is created. Note that whether the Runtime's SessionManager supports
343
+ reconnecting to an existing session depends on the SessionManager that this
344
+ runtime is configured with.
345
+ session_id_override
346
+ The ID to assign to a new session being created with this method. Setting
347
+ this can be useful when the service that a Streamlit Runtime is running in
348
+ wants to tie the lifecycle of a Streamlit session to some other session-like
349
+ object that it manages. Only one of existing_session_id and
350
+ session_id_override should be set.
351
+
352
+ Returns
353
+ -------
354
+ str
355
+ The session's unique string ID.
356
+
357
+ Notes
358
+ -----
359
+ Threading: UNSAFE. Must be called on the eventloop thread.
360
+ """
361
+ assert not (
362
+ existing_session_id and session_id_override
363
+ ), "Only one of existing_session_id and session_id_override should be set!"
364
+
365
+ if self._state in (RuntimeState.STOPPING, RuntimeState.STOPPED):
366
+ raise RuntimeStoppedError(f"Can't connect_session (state={self._state})")
367
+
368
+ session_id = self._session_mgr.connect_session(
369
+ client=client,
370
+ script_data=ScriptData(self._main_script_path, self._command_line or ""),
371
+ user_info=user_info,
372
+ existing_session_id=existing_session_id,
373
+ session_id_override=session_id_override,
374
+ )
375
+ self._set_state(RuntimeState.ONE_OR_MORE_SESSIONS_CONNECTED)
376
+ self._get_async_objs().has_connection.set()
377
+
378
+ return session_id
379
+
380
+ def create_session(
381
+ self,
382
+ client: SessionClient,
383
+ user_info: Dict[str, Optional[str]],
384
+ existing_session_id: Optional[str] = None,
385
+ session_id_override: Optional[str] = None,
386
+ ) -> str:
387
+ """Create a new session (or connect to an existing one) and return its unique ID.
388
+
389
+ Notes
390
+ -----
391
+ This method is simply an alias for connect_session added for backwards
392
+ compatibility.
393
+ """
394
+ LOGGER.warning("create_session is deprecated! Use connect_session instead.")
395
+ return self.connect_session(
396
+ client=client,
397
+ user_info=user_info,
398
+ existing_session_id=existing_session_id,
399
+ session_id_override=session_id_override,
400
+ )
401
+
402
+ def close_session(self, session_id: str) -> None:
403
+ """Close and completely shut down a session.
404
+
405
+ This differs from disconnect_session in that it always completely shuts down a
406
+ session, permanently losing any associated state (session state, uploaded files,
407
+ etc.).
408
+
409
+ This function may be called multiple times for the same session,
410
+ which is not an error. (Subsequent calls just no-op.)
411
+
412
+ Parameters
413
+ ----------
414
+ session_id
415
+ The session's unique ID.
416
+
417
+ Notes
418
+ -----
419
+ Threading: UNSAFE. Must be called on the eventloop thread.
420
+ """
421
+ session_info = self._session_mgr.get_session_info(session_id)
422
+ if session_info:
423
+ self._message_cache.remove_refs_for_session(session_info.session)
424
+ self._session_mgr.close_session(session_id)
425
+ self._on_session_disconnected()
426
+
427
+ def disconnect_session(self, session_id: str) -> None:
428
+ """Disconnect a session. It will stop producing ForwardMsgs.
429
+
430
+ Differs from close_session because disconnected sessions can be reconnected to
431
+ for a brief window (depending on the SessionManager/SessionStorage
432
+ implementations used by the runtime).
433
+
434
+ This function may be called multiple times for the same session,
435
+ which is not an error. (Subsequent calls just no-op.)
436
+
437
+ Parameters
438
+ ----------
439
+ session_id
440
+ The session's unique ID.
441
+
442
+ Notes
443
+ -----
444
+ Threading: UNSAFE. Must be called on the eventloop thread.
445
+ """
446
+ session_info = self._session_mgr.get_active_session_info(session_id)
447
+ if session_info:
448
+ # NOTE: Ideally, we'd like to keep ForwardMsgCache refs for a session around
449
+ # when a session is disconnected (and defer their cleanup until the session
450
+ # is garbage collected), but this would be difficult to do as the
451
+ # ForwardMsgCache is not thread safe, and we have no guarantee that the
452
+ # garbage collector will only run on the eventloop thread. Because of this,
453
+ # we clean up refs now and accept the risk that we're deleting cache entries
454
+ # that will be useful once the browser tab reconnects.
455
+ self._message_cache.remove_refs_for_session(session_info.session)
456
+ self._session_mgr.disconnect_session(session_id)
457
+ self._on_session_disconnected()
458
+
459
+ def handle_backmsg(self, session_id: str, msg: BackMsg) -> None:
460
+ """Send a BackMsg to an active session.
461
+
462
+ Parameters
463
+ ----------
464
+ session_id
465
+ The session's unique ID.
466
+ msg
467
+ The BackMsg to deliver to the session.
468
+
469
+ Notes
470
+ -----
471
+ Threading: UNSAFE. Must be called on the eventloop thread.
472
+ """
473
+ if self._state in (RuntimeState.STOPPING, RuntimeState.STOPPED):
474
+ raise RuntimeStoppedError(f"Can't handle_backmsg (state={self._state})")
475
+
476
+ session_info = self._session_mgr.get_active_session_info(session_id)
477
+ if session_info is None:
478
+ LOGGER.debug(
479
+ "Discarding BackMsg for disconnected session (id=%s)", session_id
480
+ )
481
+ return
482
+
483
+ session_info.session.handle_backmsg(msg)
484
+
485
+ def handle_backmsg_deserialization_exception(
486
+ self, session_id: str, exc: BaseException
487
+ ) -> None:
488
+ """Handle an Exception raised during deserialization of a BackMsg.
489
+
490
+ Parameters
491
+ ----------
492
+ session_id
493
+ The session's unique ID.
494
+ exc
495
+ The Exception.
496
+
497
+ Notes
498
+ -----
499
+ Threading: UNSAFE. Must be called on the eventloop thread.
500
+ """
501
+ if self._state in (RuntimeState.STOPPING, RuntimeState.STOPPED):
502
+ raise RuntimeStoppedError(
503
+ f"Can't handle_backmsg_deserialization_exception (state={self._state})"
504
+ )
505
+
506
+ session_info = self._session_mgr.get_active_session_info(session_id)
507
+ if session_info is None:
508
+ LOGGER.debug(
509
+ "Discarding BackMsg Exception for disconnected session (id=%s)",
510
+ session_id,
511
+ )
512
+ return
513
+
514
+ session_info.session.handle_backmsg_exception(exc)
515
+
516
+ @property
517
+ async def is_ready_for_browser_connection(self) -> Tuple[bool, str]:
518
+ if self._state not in (
519
+ RuntimeState.INITIAL,
520
+ RuntimeState.STOPPING,
521
+ RuntimeState.STOPPED,
522
+ ):
523
+ return True, "ok"
524
+
525
+ return False, "unavailable"
526
+
527
+ async def does_script_run_without_error(self) -> Tuple[bool, str]:
528
+ """Load and execute the app's script to verify it runs without an error.
529
+
530
+ Returns
531
+ -------
532
+ (True, "ok") if the script completes without error, or (False, err_msg)
533
+ if the script raises an exception.
534
+
535
+ Notes
536
+ -----
537
+ Threading: UNSAFE. Must be called on the eventloop thread.
538
+ """
539
+ # NOTE: We create an AppSession directly here instead of using the
540
+ # SessionManager intentionally. This isn't a "real" session and is only being
541
+ # used to test that the script runs without error.
542
+ session = AppSession(
543
+ script_data=ScriptData(self._main_script_path, self._command_line),
544
+ uploaded_file_manager=self._uploaded_file_mgr,
545
+ script_cache=self._script_cache,
546
+ message_enqueued_callback=self._enqueued_some_message,
547
+ local_sources_watcher=LocalSourcesWatcher(self._main_script_path),
548
+ user_info={"email": "test@test.com"},
549
+ )
550
+
551
+ try:
552
+ session.request_rerun(None)
553
+
554
+ now = time.perf_counter()
555
+ while (
556
+ SCRIPT_RUN_WITHOUT_ERRORS_KEY not in session.session_state
557
+ and (time.perf_counter() - now) < SCRIPT_RUN_CHECK_TIMEOUT
558
+ ):
559
+ await asyncio.sleep(0.1)
560
+
561
+ if SCRIPT_RUN_WITHOUT_ERRORS_KEY not in session.session_state:
562
+ return False, "timeout"
563
+
564
+ ok = session.session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY]
565
+ msg = "ok" if ok else "error"
566
+
567
+ return ok, msg
568
+ finally:
569
+ session.shutdown()
570
+
571
+ def _set_state(self, new_state: RuntimeState) -> None:
572
+ LOGGER.debug("Runtime state: %s -> %s", self._state, new_state)
573
+ self._state = new_state
574
+
575
+ async def _loop_coroutine(self) -> None:
576
+ """The main Runtime loop.
577
+
578
+ This function won't exit until `stop` is called.
579
+
580
+ Notes
581
+ -----
582
+ Threading: UNSAFE. Must be called on the eventloop thread.
583
+ """
584
+
585
+ async_objs = self._get_async_objs()
586
+
587
+ try:
588
+ if self._state == RuntimeState.INITIAL:
589
+ self._set_state(RuntimeState.NO_SESSIONS_CONNECTED)
590
+ elif self._state == RuntimeState.ONE_OR_MORE_SESSIONS_CONNECTED:
591
+ pass
592
+ else:
593
+ raise RuntimeError(f"Bad Runtime state at start: {self._state}")
594
+
595
+ # Signal that we're started and ready to accept sessions
596
+ async_objs.started.set_result(None)
597
+
598
+ while not async_objs.must_stop.is_set():
599
+ if self._state == RuntimeState.NO_SESSIONS_CONNECTED: # type: ignore[comparison-overlap]
600
+ # mypy 1.4 incorrectly thinks this if-clause is unreachable,
601
+ # because it thinks self._state must be INITIAL | ONE_OR_MORE_SESSIONS_CONNECTED.
602
+ await asyncio.wait( # type: ignore[unreachable]
603
+ (
604
+ asyncio.create_task(async_objs.must_stop.wait()),
605
+ asyncio.create_task(async_objs.has_connection.wait()),
606
+ ),
607
+ return_when=asyncio.FIRST_COMPLETED,
608
+ )
609
+
610
+ elif self._state == RuntimeState.ONE_OR_MORE_SESSIONS_CONNECTED:
611
+ async_objs.need_send_data.clear()
612
+
613
+ for active_session_info in self._session_mgr.list_active_sessions():
614
+ msg_list = active_session_info.session.flush_browser_queue()
615
+ for msg in msg_list:
616
+ try:
617
+ self._send_message(active_session_info, msg)
618
+ except SessionClientDisconnectedError:
619
+ self._session_mgr.disconnect_session(
620
+ active_session_info.session.id
621
+ )
622
+
623
+ # Yield for a tick after sending a message.
624
+ await asyncio.sleep(0)
625
+
626
+ # Yield for a few milliseconds between session message
627
+ # flushing.
628
+ await asyncio.sleep(0.01)
629
+
630
+ else:
631
+ # Break out of the thread loop if we encounter any other state.
632
+ break
633
+
634
+ await asyncio.wait(
635
+ (
636
+ asyncio.create_task(async_objs.must_stop.wait()),
637
+ asyncio.create_task(async_objs.need_send_data.wait()),
638
+ ),
639
+ return_when=asyncio.FIRST_COMPLETED,
640
+ )
641
+
642
+ # Shut down all AppSessions.
643
+ for session_info in self._session_mgr.list_sessions():
644
+ # NOTE: We want to fully shut down sessions when the runtime stops for
645
+ # now, but this may change in the future if/when our notion of a session
646
+ # is no longer so tightly coupled to a browser tab.
647
+ self._session_mgr.close_session(session_info.session.id)
648
+
649
+ self._set_state(RuntimeState.STOPPED)
650
+ async_objs.stopped.set_result(None)
651
+
652
+ except Exception as e:
653
+ async_objs.stopped.set_exception(e)
654
+ traceback.print_exc()
655
+ LOGGER.info(
656
+ """
657
+ Please report this bug at https://github.com/streamlit/streamlit/issues.
658
+ """
659
+ )
660
+
661
+ def _send_message(self, session_info: ActiveSessionInfo, msg: ForwardMsg) -> None:
662
+ """Send a message to a client.
663
+
664
+ If the client is likely to have already cached the message, we may
665
+ instead send a "reference" message that contains only the hash of the
666
+ message.
667
+
668
+ Parameters
669
+ ----------
670
+ session_info : ActiveSessionInfo
671
+ The ActiveSessionInfo associated with websocket
672
+ msg : ForwardMsg
673
+ The message to send to the client
674
+
675
+ Notes
676
+ -----
677
+ Threading: UNSAFE. Must be called on the eventloop thread.
678
+ """
679
+ msg.metadata.cacheable = is_cacheable_msg(msg)
680
+ msg_to_send = msg
681
+ if msg.metadata.cacheable:
682
+ populate_hash_if_needed(msg)
683
+
684
+ if self._message_cache.has_message_reference(
685
+ msg, session_info.session, session_info.script_run_count
686
+ ):
687
+ # This session has probably cached this message. Send
688
+ # a reference instead.
689
+ LOGGER.debug("Sending cached message ref (hash=%s)", msg.hash)
690
+ msg_to_send = create_reference_msg(msg)
691
+
692
+ # Cache the message so it can be referenced in the future.
693
+ # If the message is already cached, this will reset its
694
+ # age.
695
+ LOGGER.debug("Caching message (hash=%s)", msg.hash)
696
+ self._message_cache.add_message(
697
+ msg, session_info.session, session_info.script_run_count
698
+ )
699
+
700
+ # If this was a `script_finished` message, we increment the
701
+ # script_run_count for this session, and update the cache
702
+ if (
703
+ msg.WhichOneof("type") == "script_finished"
704
+ and msg.script_finished == ForwardMsg.FINISHED_SUCCESSFULLY
705
+ ):
706
+ LOGGER.debug(
707
+ "Script run finished successfully; "
708
+ "removing expired entries from MessageCache "
709
+ "(max_age=%s)",
710
+ config.get_option("global.maxCachedMessageAge"),
711
+ )
712
+ session_info.script_run_count += 1
713
+ self._message_cache.remove_expired_entries_for_session(
714
+ session_info.session, session_info.script_run_count
715
+ )
716
+
717
+ # Ship it off!
718
+ session_info.client.write_forward_msg(msg_to_send)
719
+
720
+ def _enqueued_some_message(self) -> None:
721
+ """Callback called by AppSession after the AppSession has enqueued a
722
+ message. Sets the "needs_send_data" event, which causes our core
723
+ loop to wake up and flush client message queues.
724
+
725
+ Notes
726
+ -----
727
+ Threading: SAFE. May be called on any thread.
728
+ """
729
+ async_objs = self._get_async_objs()
730
+ async_objs.eventloop.call_soon_threadsafe(async_objs.need_send_data.set)
731
+
732
+ def _get_async_objs(self) -> AsyncObjects:
733
+ """Return our AsyncObjects instance. If the Runtime hasn't been
734
+ started, this will raise an error.
735
+ """
736
+ if self._async_objs is None:
737
+ raise RuntimeError("Runtime hasn't started yet!")
738
+ return self._async_objs
739
+
740
+ def _on_session_disconnected(self) -> None:
741
+ """Set the runtime state to NO_SESSIONS_CONNECTED if the last active
742
+ session was disconnected.
743
+ """
744
+ if (
745
+ self._state == RuntimeState.ONE_OR_MORE_SESSIONS_CONNECTED
746
+ and self._session_mgr.num_active_sessions() == 0
747
+ ):
748
+ self._get_async_objs().has_connection.clear()
749
+ self._set_state(RuntimeState.NO_SESSIONS_CONNECTED)