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.
- streamlit/components/v2/__init__.py +3 -3
- streamlit/components/v2/types.py +4 -4
- streamlit/config.py +14 -0
- streamlit/elements/lib/options_selector_utils.py +98 -0
- streamlit/elements/widgets/multiselect.py +19 -9
- streamlit/elements/widgets/selectbox.py +12 -24
- streamlit/proto/MetricsEvent_pb2.py +4 -4
- streamlit/proto/MetricsEvent_pb2.pyi +4 -1
- streamlit/proto/PageProfile_pb2.py +6 -6
- streamlit/proto/PageProfile_pb2.pyi +11 -1
- streamlit/runtime/metrics_util.py +4 -0
- streamlit/runtime/runtime.py +3 -0
- streamlit/starlette.py +34 -0
- streamlit/static/index.html +1 -1
- streamlit/static/manifest.json +300 -300
- streamlit/static/static/js/{ErrorOutline.esm.j3b3OjAK.js → ErrorOutline.esm.BcqUpfNe.js} +1 -1
- streamlit/static/static/js/{FileDownload.esm.DCizXv6Q.js → FileDownload.esm.CtJWBuub.js} +1 -1
- streamlit/static/static/js/{FileHelper.EpMV5UVe.js → FileHelper.D0dQPhOs.js} +1 -1
- streamlit/static/static/js/{FormClearHelper.lF7Ran5M.js → FormClearHelper.Cm3GDSk6.js} +1 -1
- streamlit/static/static/js/{InputInstructions.CMvqhPhy.js → InputInstructions.D7Hxdzwv.js} +1 -1
- streamlit/static/static/js/{Particles.DsGe8psi.js → Particles.vAUDtAR8.js} +1 -1
- streamlit/static/static/js/{ProgressBar.DzoKn4D-.js → ProgressBar.Dp2CGRba.js} +2 -2
- streamlit/static/static/js/{StreamlitSyntaxHighlighter.CFab0b_b.js → StreamlitSyntaxHighlighter.DC0000nJ.js} +1 -1
- streamlit/static/static/js/{TableChart.esm.nZsTq1Sb.js → TableChart.esm.DDVoSKOT.js} +1 -1
- streamlit/static/static/js/{Toolbar.CFMvwQYl.js → Toolbar.CuMH-Gqe.js} +1 -1
- streamlit/static/static/js/{WidgetLabelHelpIconInline.D2EEUEQX.js → WidgetLabelHelpIconInline.WAReecol.js} +1 -1
- streamlit/static/static/js/{base-input.DKTA2QNz.js → base-input.BX9Jll5o.js} +4 -4
- streamlit/static/static/js/{checkbox.D9H9J-_W.js → checkbox.BzMHUSz1.js} +1 -1
- streamlit/static/static/js/{createDownloadLinkElement.DCk6EhPM.js → createDownloadLinkElement.CviG5BQx.js} +1 -1
- streamlit/static/static/js/{data-grid-overlay-editor.DExrGdqs.js → data-grid-overlay-editor.Dzo9A4l6.js} +1 -1
- streamlit/static/static/js/{downloader.CLJ7BreF.js → downloader.DIDvj0d5.js} +1 -1
- streamlit/static/static/js/{embed.CxOHZWx2.js → embed.C8qeQ38b.js} +6 -6
- streamlit/static/static/js/{es6.C99ebre4.js → es6.Dpcc-U7U.js} +2 -2
- streamlit/static/static/js/{formatNumber.D_w4fBsk.js → formatNumber.CPvuaBa8.js} +1 -1
- streamlit/static/static/js/{iconPosition.Cfhw1RkE.js → iconPosition.y0q-Rqem.js} +1 -1
- streamlit/static/static/js/{iframeResizer.contentWindow.BcWUIYOe.js → iframeResizer.contentWindow.DblExdXF.js} +1 -1
- streamlit/static/static/js/{index.BWK_h3IL.js → index.B-g6lwYa.js} +1 -1
- streamlit/static/static/js/{index.DoLorXMA.js → index.B10MmI2m.js} +1 -1
- streamlit/static/static/js/{index.Dx8TcTHV.js → index.B6vCS66f.js} +6 -6
- streamlit/static/static/js/{index.Bp_LrAiI.js → index.B9TG5Ah8.js} +1 -1
- streamlit/static/static/js/{index.CrJ9KZpt.js → index.BDb0PRvK.js} +1 -1
- streamlit/static/static/js/{index.1AemKTSK.js → index.BGufEmCz.js} +1 -1
- streamlit/static/static/js/{index.7Q3Iaebc.js → index.BItU4jMo.js} +1 -1
- streamlit/static/static/js/{index.D18KqoUa.js → index.BPMqWDef.js} +1 -1
- streamlit/static/static/js/index.BTHi5W25.js +1 -0
- streamlit/static/static/js/{index.BHx4Qw7z.js → index.BVN9cI-k.js} +1 -1
- streamlit/static/static/js/index.BWOP7HFT.js +1 -0
- streamlit/static/static/js/{index.DDx6TP95.js → index.BXYgO5B8.js} +1 -1
- streamlit/static/static/js/{index.DkSjHoXw.js → index.BZBWLU1C.js} +8 -8
- streamlit/static/static/js/{index.f_s01aPm.js → index.BfZdZpv-.js} +2 -2
- streamlit/static/static/js/{index.C7_5JMRC.js → index.Bxe7fKbw.js} +1 -1
- streamlit/static/static/js/{index.BcbR2mbc.js → index.C1ElSZEy.js} +1 -1
- streamlit/static/static/js/index.C26ZOVFL.js +1 -0
- streamlit/static/static/js/{index.aJ3XRx8R.js → index.C7lwRBvF.js} +1 -1
- streamlit/static/static/js/{index.CGX2fllG.js → index.C93QGPyk.js} +1 -1
- streamlit/static/static/js/{index.COjurlZk.js → index.CCDejIvL.js} +2 -2
- streamlit/static/static/js/index.CcMmNHAq.js +1 -0
- streamlit/static/static/js/index.D42y-GeO.js +1 -0
- streamlit/static/static/js/{index.C0VFHmJN.js → index.D69ULFWq.js} +1 -1
- streamlit/static/static/js/{index.V4C1Oi-F.js → index.DB4MbQ40.js} +3 -3
- streamlit/static/static/js/{index.DJ4GBc1k.js → index.DRFJgBf-.js} +1 -1
- streamlit/static/static/js/{index.BXQNt1hj.js → index.DTK-btqV.js} +1 -1
- streamlit/static/static/js/{index.CFE-yHdT.js → index.DZEQ0G7H.js} +1 -1
- streamlit/static/static/js/{index.Bu3Lto_G.js → index.DaU1ayM7.js} +1 -1
- streamlit/static/static/js/{index.DSSQzzPk.js → index.Dla9XiNe.js} +2 -2
- streamlit/static/static/js/{index.DiZfOR0A.js → index.DmQqT9OM.js} +1 -1
- streamlit/static/static/js/{index.DuFqxjbN.js → index.DppScppA.js} +1 -1
- streamlit/static/static/js/{index.Dqphk1ee.js → index.Dr8b3Vn6.js} +1 -1
- streamlit/static/static/js/{index.CPo5dtx7.js → index.DvQ7-afx.js} +1 -1
- streamlit/static/static/js/{index.ATP5607r.js → index.F-NmmwfK.js} +1 -1
- streamlit/static/static/js/{index.gnFSTAhI.js → index.MKI8t7l2.js} +1 -1
- streamlit/static/static/js/{index.BXnQdCa5.js → index.RRAKXgBZ.js} +1 -1
- streamlit/static/static/js/{index.CyVBY8PG.js → index.VSsf6tsu.js} +2 -2
- streamlit/static/static/js/{index.DcudoGfL.js → index.aEeM0ekc.js} +1 -1
- streamlit/static/static/js/{index.Ds-w0zIo.js → index.eweE9HKU.js} +1 -1
- streamlit/static/static/js/{index.CBZQ_6AF.js → index.gr6yGiCL.js} +1 -1
- streamlit/static/static/js/{index.mZ1qbnKs.js → index.kKkHk9Mc.js} +1 -1
- streamlit/static/static/js/index.pMvzHC7z.js +1 -0
- streamlit/static/static/js/{index.DIIdzDwK.js → index.pgifwCIr.js} +1 -1
- streamlit/static/static/js/index.q83b8_8b.js +1 -0
- streamlit/static/static/js/{index.BzQChe4y.js → index.u2AvcQQT.js} +1 -1
- streamlit/static/static/js/{index.CrzXL2V8.js → index.w2_VrtVw.js} +1 -1
- streamlit/static/static/js/{index.DbmtfcDm.js → index.wv1DYVsn.js} +1 -1
- streamlit/static/static/js/{index.COZICZL6.js → index.x4j6nnNb.js} +1 -1
- streamlit/static/static/js/{input.CZPD7mCu.js → input.BoJqOS7a.js} +1 -1
- streamlit/static/static/js/{main.CX1jAiMw.js → main.B5XmUwmQ.js} +1 -1
- streamlit/static/static/js/{memory.m0jC5ULx.js → memory.9izgq54V.js} +1 -1
- streamlit/static/static/js/{number-overlay-editor.Dy0iTeCo.js → number-overlay-editor.PqOGQcLn.js} +1 -1
- streamlit/static/static/js/{pandasStylerUtils.D9jj-wHU.js → pandasStylerUtils.DUtF2t3R.js} +1 -1
- streamlit/static/static/js/{sandbox.CbvG1iAz.js → sandbox.DgKTHPLO.js} +1 -1
- streamlit/static/static/js/{styled-components.Cw3ioniY.js → styled-components.D4x8jmOI.js} +1 -1
- streamlit/static/static/js/{throttle.CAwhGpn0.js → throttle.DxEHIIp7.js} +1 -1
- streamlit/static/static/js/{timepicker.Bh3m6Pjp.js → timepicker.CDks5DWI.js} +1 -1
- streamlit/static/static/js/{toConsumableArray.DM0o32JS.js → toConsumableArray.CvTQRsV-.js} +1 -1
- streamlit/static/static/js/uniqueId.RScLN3St.js +1 -0
- streamlit/static/static/js/{useBasicWidgetState.C9zOVP8a.js → useBasicWidgetState.D6_b0IqA.js} +1 -1
- streamlit/static/static/js/{useIntlLocale.Bo42aN1U.js → useIntlLocale.Ctz17A7g.js} +1 -1
- streamlit/static/static/js/{useTextInputAutoExpand.DmyHLDxl.js → useTextInputAutoExpand.PAFB5o1T.js} +1 -1
- streamlit/static/static/js/{useUpdateUiValue.aWXWpqmw.js → useUpdateUiValue.CcTvmGlb.js} +1 -1
- streamlit/static/static/js/{useWaveformController.DlE14M1X.js → useWaveformController.vLi36Ir4.js} +1 -1
- streamlit/static/static/js/{withCalculatedWidth.B5JFJSmG.js → withCalculatedWidth.BtA2jL5-.js} +1 -1
- streamlit/static/static/js/{withFullScreenWrapper.dgVioHk1.js → withFullScreenWrapper.CeeoYBpc.js} +1 -1
- streamlit/web/bootstrap.py +105 -10
- streamlit/web/cli.py +21 -4
- streamlit/web/server/app_discovery.py +421 -0
- streamlit/web/server/server.py +0 -13
- streamlit/web/server/starlette/__init__.py +2 -1
- streamlit/web/server/starlette/starlette_app.py +326 -3
- streamlit/web/server/starlette/starlette_routes.py +27 -8
- {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/RECORD +115 -113
- streamlit/static/static/js/index.BOGNGR9a.js +0 -1
- streamlit/static/static/js/index.C-lnh8pI.js +0 -1
- streamlit/static/static/js/index.C98anBCM.js +0 -1
- streamlit/static/static/js/index.CFtGP8pH.js +0 -1
- streamlit/static/static/js/index.Cg59Loqx.js +0 -1
- streamlit/static/static/js/index.CiU2Tdcl.js +0 -1
- streamlit/static/static/js/index.DpnqUQVD.js +0 -1
- streamlit/static/static/js/uniqueId.DtV_RZzG.js +0 -1
- {streamlit_nightly-1.52.3.dev20260108.data → streamlit_nightly-1.52.3.dev20260110.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.52.3.dev20260108.dist-info → streamlit_nightly-1.52.3.dev20260110.dist-info}/entry_points.txt +0 -0
- {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"]
|
streamlit/web/server/server.py
CHANGED
|
@@ -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",
|