streamlit-nightly 1.52.3.dev20260108__py3-none-any.whl → 1.52.3.dev20260110__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 (123) hide show
  1. streamlit/components/v2/__init__.py +3 -3
  2. streamlit/components/v2/types.py +4 -4
  3. streamlit/config.py +14 -0
  4. streamlit/elements/lib/options_selector_utils.py +98 -0
  5. streamlit/elements/widgets/multiselect.py +19 -9
  6. streamlit/elements/widgets/selectbox.py +12 -24
  7. streamlit/proto/MetricsEvent_pb2.py +4 -4
  8. streamlit/proto/MetricsEvent_pb2.pyi +4 -1
  9. streamlit/proto/PageProfile_pb2.py +6 -6
  10. streamlit/proto/PageProfile_pb2.pyi +11 -1
  11. streamlit/runtime/metrics_util.py +4 -0
  12. streamlit/runtime/runtime.py +3 -0
  13. streamlit/starlette.py +34 -0
  14. streamlit/static/index.html +1 -1
  15. streamlit/static/manifest.json +300 -300
  16. streamlit/static/static/js/{ErrorOutline.esm.j3b3OjAK.js → ErrorOutline.esm.BcqUpfNe.js} +1 -1
  17. streamlit/static/static/js/{FileDownload.esm.DCizXv6Q.js → FileDownload.esm.CtJWBuub.js} +1 -1
  18. streamlit/static/static/js/{FileHelper.EpMV5UVe.js → FileHelper.D0dQPhOs.js} +1 -1
  19. streamlit/static/static/js/{FormClearHelper.lF7Ran5M.js → FormClearHelper.Cm3GDSk6.js} +1 -1
  20. streamlit/static/static/js/{InputInstructions.CMvqhPhy.js → InputInstructions.D7Hxdzwv.js} +1 -1
  21. streamlit/static/static/js/{Particles.DsGe8psi.js → Particles.vAUDtAR8.js} +1 -1
  22. streamlit/static/static/js/{ProgressBar.DzoKn4D-.js → ProgressBar.Dp2CGRba.js} +2 -2
  23. streamlit/static/static/js/{StreamlitSyntaxHighlighter.CFab0b_b.js → StreamlitSyntaxHighlighter.DC0000nJ.js} +1 -1
  24. streamlit/static/static/js/{TableChart.esm.nZsTq1Sb.js → TableChart.esm.DDVoSKOT.js} +1 -1
  25. streamlit/static/static/js/{Toolbar.CFMvwQYl.js → Toolbar.CuMH-Gqe.js} +1 -1
  26. streamlit/static/static/js/{WidgetLabelHelpIconInline.D2EEUEQX.js → WidgetLabelHelpIconInline.WAReecol.js} +1 -1
  27. streamlit/static/static/js/{base-input.DKTA2QNz.js → base-input.BX9Jll5o.js} +4 -4
  28. streamlit/static/static/js/{checkbox.D9H9J-_W.js → checkbox.BzMHUSz1.js} +1 -1
  29. streamlit/static/static/js/{createDownloadLinkElement.DCk6EhPM.js → createDownloadLinkElement.CviG5BQx.js} +1 -1
  30. streamlit/static/static/js/{data-grid-overlay-editor.DExrGdqs.js → data-grid-overlay-editor.Dzo9A4l6.js} +1 -1
  31. streamlit/static/static/js/{downloader.CLJ7BreF.js → downloader.DIDvj0d5.js} +1 -1
  32. streamlit/static/static/js/{embed.CxOHZWx2.js → embed.C8qeQ38b.js} +6 -6
  33. streamlit/static/static/js/{es6.C99ebre4.js → es6.Dpcc-U7U.js} +2 -2
  34. streamlit/static/static/js/{formatNumber.D_w4fBsk.js → formatNumber.CPvuaBa8.js} +1 -1
  35. streamlit/static/static/js/{iconPosition.Cfhw1RkE.js → iconPosition.y0q-Rqem.js} +1 -1
  36. streamlit/static/static/js/{iframeResizer.contentWindow.BcWUIYOe.js → iframeResizer.contentWindow.DblExdXF.js} +1 -1
  37. streamlit/static/static/js/{index.BWK_h3IL.js → index.B-g6lwYa.js} +1 -1
  38. streamlit/static/static/js/{index.DoLorXMA.js → index.B10MmI2m.js} +1 -1
  39. streamlit/static/static/js/{index.Dx8TcTHV.js → index.B6vCS66f.js} +6 -6
  40. streamlit/static/static/js/{index.Bp_LrAiI.js → index.B9TG5Ah8.js} +1 -1
  41. streamlit/static/static/js/{index.CrJ9KZpt.js → index.BDb0PRvK.js} +1 -1
  42. streamlit/static/static/js/{index.1AemKTSK.js → index.BGufEmCz.js} +1 -1
  43. streamlit/static/static/js/{index.7Q3Iaebc.js → index.BItU4jMo.js} +1 -1
  44. streamlit/static/static/js/{index.D18KqoUa.js → index.BPMqWDef.js} +1 -1
  45. streamlit/static/static/js/index.BTHi5W25.js +1 -0
  46. streamlit/static/static/js/{index.BHx4Qw7z.js → index.BVN9cI-k.js} +1 -1
  47. streamlit/static/static/js/index.BWOP7HFT.js +1 -0
  48. streamlit/static/static/js/{index.DDx6TP95.js → index.BXYgO5B8.js} +1 -1
  49. streamlit/static/static/js/{index.DkSjHoXw.js → index.BZBWLU1C.js} +8 -8
  50. streamlit/static/static/js/{index.f_s01aPm.js → index.BfZdZpv-.js} +2 -2
  51. streamlit/static/static/js/{index.C7_5JMRC.js → index.Bxe7fKbw.js} +1 -1
  52. streamlit/static/static/js/{index.BcbR2mbc.js → index.C1ElSZEy.js} +1 -1
  53. streamlit/static/static/js/index.C26ZOVFL.js +1 -0
  54. streamlit/static/static/js/{index.aJ3XRx8R.js → index.C7lwRBvF.js} +1 -1
  55. streamlit/static/static/js/{index.CGX2fllG.js → index.C93QGPyk.js} +1 -1
  56. streamlit/static/static/js/{index.COjurlZk.js → index.CCDejIvL.js} +2 -2
  57. streamlit/static/static/js/index.CcMmNHAq.js +1 -0
  58. streamlit/static/static/js/index.D42y-GeO.js +1 -0
  59. streamlit/static/static/js/{index.C0VFHmJN.js → index.D69ULFWq.js} +1 -1
  60. streamlit/static/static/js/{index.V4C1Oi-F.js → index.DB4MbQ40.js} +3 -3
  61. streamlit/static/static/js/{index.DJ4GBc1k.js → index.DRFJgBf-.js} +1 -1
  62. streamlit/static/static/js/{index.BXQNt1hj.js → index.DTK-btqV.js} +1 -1
  63. streamlit/static/static/js/{index.CFE-yHdT.js → index.DZEQ0G7H.js} +1 -1
  64. streamlit/static/static/js/{index.Bu3Lto_G.js → index.DaU1ayM7.js} +1 -1
  65. streamlit/static/static/js/{index.DSSQzzPk.js → index.Dla9XiNe.js} +2 -2
  66. streamlit/static/static/js/{index.DiZfOR0A.js → index.DmQqT9OM.js} +1 -1
  67. streamlit/static/static/js/{index.DuFqxjbN.js → index.DppScppA.js} +1 -1
  68. streamlit/static/static/js/{index.Dqphk1ee.js → index.Dr8b3Vn6.js} +1 -1
  69. streamlit/static/static/js/{index.CPo5dtx7.js → index.DvQ7-afx.js} +1 -1
  70. streamlit/static/static/js/{index.ATP5607r.js → index.F-NmmwfK.js} +1 -1
  71. streamlit/static/static/js/{index.gnFSTAhI.js → index.MKI8t7l2.js} +1 -1
  72. streamlit/static/static/js/{index.BXnQdCa5.js → index.RRAKXgBZ.js} +1 -1
  73. streamlit/static/static/js/{index.CyVBY8PG.js → index.VSsf6tsu.js} +2 -2
  74. streamlit/static/static/js/{index.DcudoGfL.js → index.aEeM0ekc.js} +1 -1
  75. streamlit/static/static/js/{index.Ds-w0zIo.js → index.eweE9HKU.js} +1 -1
  76. streamlit/static/static/js/{index.CBZQ_6AF.js → index.gr6yGiCL.js} +1 -1
  77. streamlit/static/static/js/{index.mZ1qbnKs.js → index.kKkHk9Mc.js} +1 -1
  78. streamlit/static/static/js/index.pMvzHC7z.js +1 -0
  79. streamlit/static/static/js/{index.DIIdzDwK.js → index.pgifwCIr.js} +1 -1
  80. streamlit/static/static/js/index.q83b8_8b.js +1 -0
  81. streamlit/static/static/js/{index.BzQChe4y.js → index.u2AvcQQT.js} +1 -1
  82. streamlit/static/static/js/{index.CrzXL2V8.js → index.w2_VrtVw.js} +1 -1
  83. streamlit/static/static/js/{index.DbmtfcDm.js → index.wv1DYVsn.js} +1 -1
  84. streamlit/static/static/js/{index.COZICZL6.js → index.x4j6nnNb.js} +1 -1
  85. streamlit/static/static/js/{input.CZPD7mCu.js → input.BoJqOS7a.js} +1 -1
  86. streamlit/static/static/js/{main.CX1jAiMw.js → main.B5XmUwmQ.js} +1 -1
  87. streamlit/static/static/js/{memory.m0jC5ULx.js → memory.9izgq54V.js} +1 -1
  88. streamlit/static/static/js/{number-overlay-editor.Dy0iTeCo.js → number-overlay-editor.PqOGQcLn.js} +1 -1
  89. streamlit/static/static/js/{pandasStylerUtils.D9jj-wHU.js → pandasStylerUtils.DUtF2t3R.js} +1 -1
  90. streamlit/static/static/js/{sandbox.CbvG1iAz.js → sandbox.DgKTHPLO.js} +1 -1
  91. streamlit/static/static/js/{styled-components.Cw3ioniY.js → styled-components.D4x8jmOI.js} +1 -1
  92. streamlit/static/static/js/{throttle.CAwhGpn0.js → throttle.DxEHIIp7.js} +1 -1
  93. streamlit/static/static/js/{timepicker.Bh3m6Pjp.js → timepicker.CDks5DWI.js} +1 -1
  94. streamlit/static/static/js/{toConsumableArray.DM0o32JS.js → toConsumableArray.CvTQRsV-.js} +1 -1
  95. streamlit/static/static/js/uniqueId.RScLN3St.js +1 -0
  96. streamlit/static/static/js/{useBasicWidgetState.C9zOVP8a.js → useBasicWidgetState.D6_b0IqA.js} +1 -1
  97. streamlit/static/static/js/{useIntlLocale.Bo42aN1U.js → useIntlLocale.Ctz17A7g.js} +1 -1
  98. streamlit/static/static/js/{useTextInputAutoExpand.DmyHLDxl.js → useTextInputAutoExpand.PAFB5o1T.js} +1 -1
  99. streamlit/static/static/js/{useUpdateUiValue.aWXWpqmw.js → useUpdateUiValue.CcTvmGlb.js} +1 -1
  100. streamlit/static/static/js/{useWaveformController.DlE14M1X.js → useWaveformController.vLi36Ir4.js} +1 -1
  101. streamlit/static/static/js/{withCalculatedWidth.B5JFJSmG.js → withCalculatedWidth.BtA2jL5-.js} +1 -1
  102. streamlit/static/static/js/{withFullScreenWrapper.dgVioHk1.js → withFullScreenWrapper.CeeoYBpc.js} +1 -1
  103. streamlit/web/bootstrap.py +105 -10
  104. streamlit/web/cli.py +21 -4
  105. streamlit/web/server/app_discovery.py +421 -0
  106. streamlit/web/server/server.py +0 -13
  107. streamlit/web/server/starlette/__init__.py +2 -1
  108. streamlit/web/server/starlette/starlette_app.py +326 -3
  109. streamlit/web/server/starlette/starlette_routes.py +27 -8
  110. {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/METADATA +1 -1
  111. {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/RECORD +115 -113
  112. streamlit/static/static/js/index.BOGNGR9a.js +0 -1
  113. streamlit/static/static/js/index.C-lnh8pI.js +0 -1
  114. streamlit/static/static/js/index.C98anBCM.js +0 -1
  115. streamlit/static/static/js/index.CFtGP8pH.js +0 -1
  116. streamlit/static/static/js/index.Cg59Loqx.js +0 -1
  117. streamlit/static/static/js/index.CiU2Tdcl.js +0 -1
  118. streamlit/static/static/js/index.DpnqUQVD.js +0 -1
  119. streamlit/static/static/js/uniqueId.DtV_RZzG.js +0 -1
  120. {streamlit_nightly-1.52.3.dev20260108.data → streamlit_nightly-1.52.3.dev20260110.data}/scripts/streamlit.cmd +0 -0
  121. {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/WHEEL +0 -0
  122. {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/entry_points.txt +0 -0
  123. {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,421 @@
1
+ # Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
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
+ """App discovery utilities for detecting ASGI app instances in scripts.
16
+
17
+ This module provides functions to discover if a Python script contains an
18
+ ASGI application instance (like st.App, FastAPI, or Starlette), enabling
19
+ the CLI to auto-detect whether to run in traditional mode or ASGI mode.
20
+
21
+ By design, this supports not only Streamlit's st.App but also other ASGI
22
+ frameworks like FastAPI and Starlette. This allows `streamlit run` to serve
23
+ as a unified entry point for any ASGI app, providing a consistent developer
24
+ experience for projects that combine Streamlit with other frameworks or use
25
+ ASGI apps directly.
26
+
27
+ The detection uses AST (Abstract Syntax Tree) parsing to safely analyze
28
+ the source code without executing it.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import ast
34
+ from dataclasses import dataclass
35
+ from typing import TYPE_CHECKING, Final
36
+
37
+ from streamlit.logger import get_logger
38
+
39
+ if TYPE_CHECKING:
40
+ from pathlib import Path
41
+
42
+ _LOGGER: Final = get_logger(__name__)
43
+
44
+ # Preferred variable names to look for when discovering ASGI app instances.
45
+ # These are checked in order of priority.
46
+ _PREFERRED_APP_NAMES: Final[tuple[str, ...]] = ("app", "streamlit_app")
47
+
48
+ # Known ASGI app classes with their fully qualified module paths.
49
+ # Each entry is a dotted path like "module.submodule.ClassName".
50
+ # Only classes matching these paths will be detected as ASGI apps.
51
+ #
52
+ # Note: FastAPI and Starlette are intentionally included here. This enables
53
+ # `streamlit run` to serve as a unified entry point for ASGI apps, which is
54
+ # useful for projects that mount Streamlit within other frameworks or want
55
+ # to use `streamlit run` for any ASGI application.
56
+ _KNOWN_ASGI_APP_CLASSES: Final[tuple[str, ...]] = (
57
+ # Streamlit App
58
+ "streamlit.starlette.App",
59
+ "streamlit.web.server.starlette.App",
60
+ "streamlit.web.server.starlette.starlette_app.App",
61
+ # FastAPI
62
+ "fastapi.FastAPI",
63
+ "fastapi.applications.FastAPI",
64
+ # Starlette
65
+ "starlette.applications.Starlette",
66
+ )
67
+
68
+
69
+ @dataclass
70
+ class AppDiscoveryResult:
71
+ """Result of ASGI app discovery.
72
+
73
+ Attributes
74
+ ----------
75
+ is_asgi_app
76
+ True if the script contains an ASGI app instance.
77
+ app_name
78
+ The name of the app instance variable (e.g., "app").
79
+ import_string
80
+ The import string for uvicorn (e.g., "module:app").
81
+ """
82
+
83
+ is_asgi_app: bool
84
+ app_name: str | None
85
+ import_string: str | None
86
+
87
+
88
+ def _get_call_name_parts(node: ast.Call) -> tuple[str, ...] | None:
89
+ """Extract the name parts from a Call node's func attribute.
90
+
91
+ For example:
92
+ - `App(...)` returns ("App",)
93
+ - `st.App(...)` returns ("st", "App")
94
+ - `streamlit.starlette.App(...)` returns ("streamlit", "starlette", "App")
95
+
96
+ Parameters
97
+ ----------
98
+ node
99
+ An AST Call node.
100
+
101
+ Returns
102
+ -------
103
+ tuple[str, ...] | None
104
+ A tuple of name parts, or None if the call target is not a simple
105
+ name or attribute chain.
106
+ """
107
+ func = node.func
108
+ parts: list[str] = []
109
+
110
+ while isinstance(func, ast.Attribute):
111
+ parts.append(func.attr)
112
+ func = func.value
113
+
114
+ if isinstance(func, ast.Name):
115
+ parts.append(func.id)
116
+ return tuple(reversed(parts))
117
+
118
+ return None
119
+
120
+
121
+ def _extract_imports(tree: ast.AST) -> dict[str, str]:
122
+ """Extract import mappings from an AST.
123
+
124
+ Builds a mapping from local names to their fully qualified module paths.
125
+
126
+ For example:
127
+ - `from streamlit.starlette import App` → {"App": "streamlit.starlette.App"}
128
+ - `from streamlit import starlette` → {"starlette": "streamlit.starlette"}
129
+ - `import streamlit as st` → {"st": "streamlit"}
130
+ - `import fastapi` → {"fastapi": "fastapi"}
131
+
132
+ Parameters
133
+ ----------
134
+ tree
135
+ The parsed AST of a Python module.
136
+
137
+ Returns
138
+ -------
139
+ dict[str, str]
140
+ A mapping from local names to their fully qualified module paths.
141
+ """
142
+ imports: dict[str, str] = {}
143
+
144
+ for node in ast.walk(tree):
145
+ if isinstance(node, ast.Import):
146
+ # Handle: import x, import x as y
147
+ for alias in node.names:
148
+ local_name = alias.asname or alias.name
149
+ imports[local_name] = alias.name
150
+
151
+ elif isinstance(node, ast.ImportFrom) and node.module:
152
+ # Handle: from x.y import z, from x.y import z as w
153
+ for alias in node.names:
154
+ local_name = alias.asname or alias.name
155
+ imports[local_name] = f"{node.module}.{alias.name}"
156
+
157
+ return imports
158
+
159
+
160
+ def _resolve_call_to_module_path(
161
+ parts: tuple[str, ...], imports: dict[str, str]
162
+ ) -> str | None:
163
+ """Resolve a call's name parts to a fully qualified module path.
164
+
165
+ Uses the import mapping to resolve the first part of the call chain,
166
+ then appends any remaining parts.
167
+
168
+ For example, with imports {"App": "streamlit.starlette.App"}:
169
+ - ("App",) → "streamlit.starlette.App"
170
+
171
+ With imports {"st": "streamlit"}:
172
+ - ("st", "starlette", "App") → "streamlit.starlette.App"
173
+
174
+ Parameters
175
+ ----------
176
+ parts
177
+ The name parts from a Call node (e.g., ("st", "App")).
178
+ imports
179
+ The import mapping from _extract_imports.
180
+
181
+ Returns
182
+ -------
183
+ str | None
184
+ The fully qualified module path, or None if resolution fails.
185
+ """
186
+ if not parts:
187
+ return None
188
+
189
+ first_part = parts[0]
190
+ remaining_parts = parts[1:]
191
+
192
+ if first_part in imports:
193
+ # The first part was imported, resolve it
194
+ base_path = imports[first_part]
195
+ if remaining_parts:
196
+ return f"{base_path}.{'.'.join(remaining_parts)}"
197
+ return base_path
198
+
199
+ # Not imported - could be a fully qualified name or unknown
200
+ # For fully qualified names like streamlit.starlette.App(),
201
+ # just join all parts
202
+ return ".".join(parts)
203
+
204
+
205
+ def _is_asgi_app_call(node: ast.Call, imports: dict[str, str]) -> bool:
206
+ """Check if a Call node represents a known ASGI app constructor.
207
+
208
+ This function resolves the call to its fully qualified module path
209
+ using the import mapping, then checks if it matches any known
210
+ ASGI app class.
211
+
212
+ Parameters
213
+ ----------
214
+ node
215
+ An AST Call node.
216
+ imports
217
+ The import mapping from _extract_imports.
218
+
219
+ Returns
220
+ -------
221
+ bool
222
+ True if the call is a known ASGI app constructor.
223
+ """
224
+ parts = _get_call_name_parts(node)
225
+ if parts is None:
226
+ return False
227
+
228
+ resolved_path = _resolve_call_to_module_path(parts, imports)
229
+ if resolved_path is None:
230
+ return False
231
+
232
+ return resolved_path in _KNOWN_ASGI_APP_CLASSES
233
+
234
+
235
+ def _get_module_string_from_path(path: Path) -> str:
236
+ """Convert a file path to a module import string.
237
+
238
+ Since `streamlit run` adds the script's directory to sys.path via
239
+ _fix_sys_path, the module string should just be the script's stem,
240
+ not a fully qualified package path.
241
+
242
+ Parameters
243
+ ----------
244
+ path
245
+ Path to the Python file.
246
+
247
+ Returns
248
+ -------
249
+ str
250
+ The module string suitable for uvicorn (e.g., "myapp").
251
+ """
252
+ resolved = path.resolve()
253
+
254
+ # Handle __init__.py files - use the directory name
255
+ if resolved.is_file() and resolved.stem == "__init__":
256
+ return resolved.parent.stem
257
+
258
+ return resolved.stem
259
+
260
+
261
+ def _find_asgi_app_assignments(source: str) -> dict[str, int]:
262
+ """Find all variable assignments to ASGI app constructors in source code.
263
+
264
+ This function parses the source code, extracts import statements to
265
+ understand the module context, then finds assignments to known ASGI
266
+ app constructors.
267
+
268
+ Parameters
269
+ ----------
270
+ source
271
+ Python source code to analyze.
272
+
273
+ Returns
274
+ -------
275
+ dict[str, int]
276
+ A mapping of variable names to their line numbers where ASGI app
277
+ instances are assigned.
278
+ """
279
+ try:
280
+ tree = ast.parse(source)
281
+ except SyntaxError as e:
282
+ _LOGGER.debug("Failed to parse source: %s", e)
283
+ return {}
284
+
285
+ # Extract imports to resolve call names to their source modules
286
+ imports = _extract_imports(tree)
287
+
288
+ app_assignments: dict[str, int] = {}
289
+
290
+ for node in ast.walk(tree):
291
+ # Check for simple assignment: app = App(...)
292
+ if (
293
+ isinstance(node, ast.Assign)
294
+ and isinstance(node.value, ast.Call)
295
+ and _is_asgi_app_call(node.value, imports)
296
+ ):
297
+ for target in node.targets:
298
+ if isinstance(target, ast.Name):
299
+ app_assignments[target.id] = node.lineno
300
+
301
+ # Check for annotated assignment: app: App = App(...)
302
+ elif (
303
+ isinstance(node, ast.AnnAssign)
304
+ and node.value
305
+ and isinstance(node.value, ast.Call)
306
+ and _is_asgi_app_call(node.value, imports)
307
+ and isinstance(node.target, ast.Name)
308
+ ):
309
+ app_assignments[node.target.id] = node.lineno
310
+
311
+ return app_assignments
312
+
313
+
314
+ def discover_asgi_app(
315
+ path: Path,
316
+ app_name: str | None = None,
317
+ ) -> AppDiscoveryResult:
318
+ """Discover if a Python file contains an ASGI app instance using AST parsing.
319
+
320
+ This function safely analyzes the source code without executing it.
321
+ It tracks import statements to verify that detected App classes actually
322
+ come from known ASGI frameworks (streamlit, fastapi, starlette), preventing
323
+ false positives from custom classes with the same name.
324
+
325
+ Supported import patterns:
326
+ - `from streamlit.starlette import App`
327
+ - `import streamlit` (for `streamlit.starlette.App`)
328
+ - `from fastapi import FastAPI`
329
+ - `from starlette.applications import Starlette`
330
+
331
+ The app variable can have any name (e.g., `app`, `my_dashboard`, `server`).
332
+ Preferred names checked first: "app", "streamlit_app".
333
+
334
+ Parameters
335
+ ----------
336
+ path
337
+ Path to the Python script to check.
338
+ app_name
339
+ Optional specific variable name to look for. If provided, only that
340
+ name is checked. If not provided, checks preferred names first
341
+ ("app", "streamlit_app"), then falls back to any
342
+ discovered ASGI app.
343
+
344
+ Returns
345
+ -------
346
+ AppDiscoveryResult
347
+ Discovery result indicating whether an ASGI app was found and how
348
+ to import it.
349
+
350
+ Examples
351
+ --------
352
+ >>> result = discover_asgi_app(Path("streamlit_app.py"))
353
+ >>> if result.is_asgi_app:
354
+ ... print(f"Found ASGI app: {result.import_string}")
355
+ """
356
+ if not path.exists():
357
+ _LOGGER.debug("Path does not exist: %s", path)
358
+ return AppDiscoveryResult(is_asgi_app=False, app_name=None, import_string=None)
359
+
360
+ try:
361
+ source = path.read_text(encoding="utf-8")
362
+ except (OSError, UnicodeDecodeError) as e:
363
+ _LOGGER.debug("Failed to read file %s: %s", path, e)
364
+ return AppDiscoveryResult(is_asgi_app=False, app_name=None, import_string=None)
365
+
366
+ app_assignments = _find_asgi_app_assignments(source)
367
+
368
+ if not app_assignments:
369
+ _LOGGER.debug("No ASGI app assignments found in %s", path)
370
+ return AppDiscoveryResult(is_asgi_app=False, app_name=None, import_string=None)
371
+
372
+ module_str = _get_module_string_from_path(path)
373
+
374
+ # If app_name is provided, check for that specific name
375
+ if app_name:
376
+ if app_name in app_assignments:
377
+ _LOGGER.debug(
378
+ "Found ASGI app at %s:%s (line %d)",
379
+ module_str,
380
+ app_name,
381
+ app_assignments[app_name],
382
+ )
383
+ return AppDiscoveryResult(
384
+ is_asgi_app=True,
385
+ app_name=app_name,
386
+ import_string=f"{module_str}:{app_name}",
387
+ )
388
+ _LOGGER.debug("No ASGI app found with name '%s'", app_name)
389
+ return AppDiscoveryResult(is_asgi_app=False, app_name=None, import_string=None)
390
+
391
+ # Check preferred names first
392
+ for preferred_name in _PREFERRED_APP_NAMES:
393
+ if preferred_name in app_assignments:
394
+ _LOGGER.debug(
395
+ "Found ASGI app at %s:%s (preferred name, line %d)",
396
+ module_str,
397
+ preferred_name,
398
+ app_assignments[preferred_name],
399
+ )
400
+ return AppDiscoveryResult(
401
+ is_asgi_app=True,
402
+ app_name=preferred_name,
403
+ import_string=f"{module_str}:{preferred_name}",
404
+ )
405
+
406
+ # Fall back to the first discovered app (by line number)
407
+ first_app = min(app_assignments.items(), key=lambda x: x[1])
408
+ _LOGGER.debug(
409
+ "Found ASGI app at %s:%s (fallback, line %d)",
410
+ module_str,
411
+ first_app[0],
412
+ first_app[1],
413
+ )
414
+ return AppDiscoveryResult(
415
+ is_asgi_app=True,
416
+ app_name=first_app[0],
417
+ import_string=f"{module_str}:{first_app[0]}",
418
+ )
419
+
420
+
421
+ __all__ = ["AppDiscoveryResult", "discover_asgi_app"]
@@ -16,7 +16,6 @@ from __future__ import annotations
16
16
 
17
17
  import errno
18
18
  import logging
19
- import mimetypes
20
19
  import os
21
20
  import sys
22
21
  from pathlib import Path
@@ -292,7 +291,6 @@ class Server:
292
291
  def __init__(self, main_script_path: str, is_hello: bool) -> None:
293
292
  """Create the server. It won't be started yet."""
294
293
  _set_tornado_log_levels()
295
- self.initialize_mimetypes()
296
294
 
297
295
  self._main_script_path = main_script_path
298
296
  self._use_starlette = bool(config.get_option("server.useStarlette"))
@@ -323,17 +321,6 @@ class Server:
323
321
  ),
324
322
  )
325
323
 
326
- self._runtime.stats_mgr.register_provider(media_file_storage)
327
-
328
- @classmethod
329
- def initialize_mimetypes(cls) -> None:
330
- """Ensures that common mime-types are robust against system misconfiguration."""
331
- mimetypes.add_type("text/html", ".html")
332
- mimetypes.add_type("application/javascript", ".js")
333
- mimetypes.add_type("application/javascript", ".mjs")
334
- mimetypes.add_type("text/css", ".css")
335
- mimetypes.add_type("image/webp", ".webp")
336
-
337
324
  def __repr__(self) -> str:
338
325
  return util.repr_(self)
339
326
 
@@ -12,10 +12,11 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from streamlit.web.server.starlette.starlette_app import create_starlette_app
15
+ from streamlit.web.server.starlette.starlette_app import App, create_starlette_app
16
16
  from streamlit.web.server.starlette.starlette_server import UvicornRunner, UvicornServer
17
17
 
18
18
  __all__ = [
19
+ "App",
19
20
  "UvicornRunner",
20
21
  "UvicornServer",
21
22
  "create_starlette_app",