ã¯ããã«
ãã³ãã³ããã¼ããã¯ããã°ãã¨ã ã¹ãªã¼ AIã»æ©æ¢°å¦ç¿ãã¼ã ã§ã¨ã³ã¸ãã¢å ¼YouTuberããã£ã¦ãã¾ãæ²³åã¨ç¬¹å·ã§ã*1ãæ¬è¨äºã¯ãAIãã¼ã ã社å åãã«æä¾ãåãããã¸ã¥ã¢ã©ã¤ãºã¢ããªã±ã¼ã·ã§ã³ã«é¢ãã解説ã®è¨äºã§ãã
GKEä¸ã®Streamlitãµã¼ãã®ãã¹ãã£ã³ã°è¨å®ã¨ãæ©æ¢°å¦ç¿ã¨ã³ã¸ãã¢ã社å åãã®å¯è¦åãè¡ãéã®ä¸ä¾ã¨ãã¦ãåèã¨ãªãã°å¹¸ãã§ãã
ã
- ã¯ããã«
- Background
- Streamlitã¨ã¯
- Streamlitã®ç¹å¾´
- ã¢ããªã±ã¼ã·ã§ã³ãã¤ã³ãã©æ§æ
- ãããã«
- We're hiring
ã
Background
ä¸è¬çã«ãæ©æ¢°å¦ç¿ã¨ã³ã¸ãã¢ã社å åãã®å¯è¦åã¢ããªã±ã¼ã·ã§ã³ãä½ããã¨ãã£ãã±ã¼ã¹ã§ã¯ã以ä¸ã®ãããªã·ã¹ãã å©ç¨ãèãããããã¨æãã¾ãã
- HTMLãxlsxãGoogleã¹ãã¬ããã·ã¼ããªã©ãä½æãé å¸ãã
- S3ãGCSã®ãããªã¹ãã¬ã¼ã¸ã®éçãµã¤ããã¹ãã£ã³ã°ãµã¼ãã¹ãå©ç¨ãã
- LookerãDataloreãMetabaseãRedashãSupersetãªã©ã®BIãµã¼ãã¹ãå©ç¨ãã
- LambdaãECSãGAEãFirebaseããããã¯å¤é¨ã®ãã¹ãã£ã³ã°ãµã¼ãã¹ãå©ç¨ãã
- HTTPãµã¼ãç¨ã¤ã³ã¹ã¿ã³ã¹ã社å ãµã¼ãã¹åãã®ãªã³ãã¬ãµã¼ããä½æãã
- æ¢åã®ã¤ã³ãã©ã®ä¸ã«éåãããå½¢ã§ãã¹ãã£ã³ã°ãã
åæçµæãå®å¸¸çãªãã¼ã¿ã®ã°ã©ãã使ã£ãå¯è¦åãªã©ãJupyter NotebookãHTMLã§åºåãã¦ãå ±æããå½¢ã§çµããäºãå¤ã ããã¾ãã ã¾ããå®éã«éçãµã¤ãã®ãã¹ãã£ã³ã°ãµã¼ãã¹ã§åé¡ãªãå ´é¢ãå¤ãåå¨ãã¾ãã
å¯è¦åã¢ããªã±ã¼ã·ã§ã³ã¨è¨ãã©ãã¤ã³ãã©ã³ã¹ãã管çã³ã¹ããæå°éã§æ¸ããããã¡ã¤ã«é å¸ã§æ¸ãã®ã§ããã°ãããããæã§ãã
ã
ä¸æ¹ã§ãå¯è¦åã®ç®çã«ä¾ã£ã¦ã¯ã
ãæ¢åã®ãã¼ã¿åå¾ãå å·¥ãæ©æ¢°å¦ç¿ã¢ãã«ã®æ¨è«ãªã©ãå®è¡ããPythonã¹ã¯ãªããã使ãã¾ããã¦ä½æãããã
ã社å
å¤ã®QAãæ¬çªç°å¢ã®ãã¼ã¿ã½ã¼ã¹ã«ã»ãã¥ãªãã£ãã¢ã¯ã»ã¹æ¨©éãã¦ã¼ã¶å¶éå¨ããèæ
®ããä¸ã§ã¢ã¯ã»ã¹ãããã
ã¨ãã£ãç¶æ³ãèãããã¾ãã
å¯è¦åã¢ããªã±ã¼ã·ã§ã³ã®ã¦ã¼ã¶å´ãã½ããã¦ã§ã¢ã¨ã³ã¸ãã¢ãªã³ã°ã®æé¤ãããå ´åã¯ãã³ã¹ããåæ¸ã§ããPythonã¹ã¯ãªãããåä½ãããæ¹æ³è«ã¨ãã¦ãGoogle Colabãªã©ãåè£ã«æããã§ãããã ã½ããã¦ã§ã¢ã¨ã³ã¸ãã¢ä»¥å¤ã«ãè¦ãããå ´åãªã©ãå¯è¦åããªããã«ããããã°BIãµã¼ãã¹ãå©ç¨ããæ¹åã«ããªããã¨æãã¾ãã å®éã«LookerãDataloreãRe:dashçã§ã¯ãJupyter NotebookãPythonã¹ã¯ãªãããå©ç¨ããäºãå¯è½ã§ãã
ããã«ãªãããªå®è£ ãèããå ´åã¨ãã¦ãHTTPãµã¼ãããã¹ãã£ã³ã°ããæ¹æ³ãèãããã¾ãã ãã®å ´åãã¯ã©ã¦ãã®ä½ããã®ãµã¼ãã¹ãå©ç¨ãããã社å ãµã¼ããæ¢åã®ã¤ã³ãã©ãå©ç¨ããäºã«ãªãã¾ãã
ã
ã¤ã¾ããã©ã®å½¢å¼ã§å¯è¦åçµæãå ±æãããã¯ãç¶æ³ãç®ç次第ã¨ããäºã§ãã ã
ä»åGKEã§Streamlitããã¹ãã£ã³ã°ããã«è³ã£ãçµç·¯ã¨ãã¦ã¯ã以ä¸ã®ãããªãã¤ã³ããããã¾ãã
- å¯è¦åããããã¼ã¿ãä¸å®æéãã¨ã«å¤åããããããã®å ´ã§BigQueryãBigTableã社å ã®APIãªã©ã«ã¢ã¯ã»ã¹ããã
- æ¢ã«ã»ãã¥ãªãã£ã¨ãã¼ã¿ã½ã¼ã¹ã¢ã¯ã»ã¹ã«é¢ãã権éãã¯ãªã¢ããGKEã¯ã©ã¹ã¿ãDockerã³ã³ãããåå¨ãã
- APIã¢ã¯ã»ã¹ãªã©ã«å®è£ æ¸ã¿ã®æ¢åã®Pythonã¹ã¯ãªããã使ããã (æ°è¦ã«ã³ã¼ããæ¸ãã®ãé²ãmoduleã¨ãã¦ä½¿ãåããã)
- ãStreamlitã使ãããï¼ãã¨ããç§ã®é¡æ
ä¸ã§ãã»ãã¥ãªãã£é¢ã®è¦³ç¹ã§ãæ¬çªãã¼ã¿ã¸ã®ã¢ã¯ã»ã¹ã社å ã§ã®å¶éä»ãAPIã®éç¨ãk8sã¯ã©ã¹ã¿ä¸ã«ã¦å®ç¸¾ãæã£ã¦ããæã大ããªãã¤ã³ãã§ãã ã¾ããterraformãæ´åããå½¢ã§ãCI/CDã«ããå¯è¦åçµæã確èªã§ããHTTPãµã¼ãããããã¤ãããã¦ãã¦ãæ¢ã«æã£ã¦ããäºããStreamlitæ¡ç¨ã®ä¸å ã¨ãã¦ããã¾ãã ï¼ç§ãStreamlitã使ãããã£ãã¨ããæ°æã¡ã®åé¡ãããã¾ãï¼
ã
Streamlitã¨ã¯
Streamlitã¯ãStreamlit社ã2018å¹´ããå ¬éãã¦ããå¯è¦åã®ããã®Pythonã¢ã¸ã¥ã¼ã«ã§ãã github.com
2019å¹´ã«Streamlitã®co-founderã§ããAdrien Treuilleæ°ã®ä»¥ä¸ããã°è¨äºãRedditãªã©ã§è©±é¡ã«ãªã£ãäºããã£ããã«ããã¼ã¿åæãçæ¥ã¨ããæ¥çã§ä½¿ããå§ããããã«è¨æ¶ãã¦ãã¾ãã towardsdatascience.com
è¿å¹´ã§ã¯æ©æ¢°å¦ç¿ã¢ã¸ã¥ã¼ã«ã¨ãã¦ä»£è¡¨çãªAllenNLPãspacyãuniverseããã¸ã§ã¯ãã¨ãã¦Streamlitããµãã¼ãããä»ãæ¥æ¬ã§ãã¦ã¼ã¶ãå¢ããå¤ãã®æè¡ããã°ãå ¬éããã¦ãã¾ãã æåå³åã«ãªãã¾ãããç§ãå®æçã«ä½¿ãã ãã§ãªããå ¬å¼ãã©ã¼ã©ã ã«åå ãããvimã¨ã®é£æºè¨å®ãå ¬éããçãã¦ãã¾ãã
Spacy Universe Project: Overview · spaCy Universe
ã
Streamlitã¯ããã°ãã°2018年以åããå ¬éããã¦ããPlotly DashãBokehã¨ãã£ãã¢ã¸ã¥ã¼ã«ã¨æ¯è¼ããã¾ãã DashãBokehãã¨ã³ã¿ã¼ãã©ã¤ãºã«åããå´é¢ãããã®ã«å¯¾ãã¦ãStreamlitã¯ã©ããããããã¿ã¤ãã³ã°ã«ç¦ç¹ãå½ã¦ããç¹ã大ããªéãã¨ãªã£ã¦ãã¾ãã APIã¯ç°¡ç´ ã§æ±ãããããJupyter Notebookã®ãããªå¯è¦åãé¢æ°ã®çµæã®cacheãå°ãªãPythonã³ã¼ãã§ä½æã§ãã¾ããåé¢ãã³ã³ãã¼ãã³ãã®æ¡å¼µãã¨ã³ããã¤ã³ãã®è¿½å ãã°ãªãããã¶ã¤ã³ãã¦ã¼ã¶èªè¨¼ã®ä»çµã¿ã追å ããäºãããã©ã«ãã§ã¯ãµãã¼ããã¦ãã¾ããã
é©æé©æã¨ã¯ãªãã¾ãããä»åã®ç§ã®äºä¾ã§ã¯ãå¯è¦åçµæã®å ±æã«ã¤ãã¦ãç¶ç¶çã§ãªãçæéã ãè¡ãããã¨ããç®çããStreamlitã®ã©ããããããã¿ã¤ãã³ã°ã®ææ³ã¨åã£ã¦ããã¨ãè¨ãã¾ãã ï¼ãã¡ããç§èªèº«ã使ãããæ°æã¡ãå¼·ãã£ãã§ãï¼
ã
Streamlitã®ç¹å¾´
ç°¡åãªExampleã¨ã¯ãªã£ã¦ãã¾ãã¾ãããStreamlitãç¨ãã¦pandas DataFrameãããã¤ãã®æ¹æ³ã§å¯è¦åããä¾ã以ä¸ã«ç¤ºãã¾ãã
import streamlit as st import pandas as pd # DataFrameãçæããcacheæ©è½ä»ãã®é¢æ°ãå®ç¾© @st.cache def get_df(s): return pd.DataFrame({'id': ['1', '2', '3'], 'name': ['X', 'Y', s]}) # text formãçæ s = st.text_input('input s') df = get_df(s) # DataFrameããã¼ãã«ã§è¡¨ç¤º st.markdown('# Table') st.table(df) # histgramã表示 st.markdown('# Histgram') df['name'].hist() st.pyplot()
Streamlitã®ç¹å¾´çãªæ©è½ã¨ãã¦ãst.cache
ã pyplotã®hook
ãããã¾ãã
st.cacheã¯å é¨çã«ã¯ãmethodã®å ¥åå¤ããã©ã¡ã¼ã¿ã¨ãã¦pickleã§dumpããå®è£ ã¨ãªã£ã¦ãã¾ããpickleã®I/Oã®æéããå°ããå¦çãä¾ãã°æ¤ç´¢APIã«queryãæããããDBãããã¼ã¿ãåå¾ãããã§ããã°ã表示ã¾ã§ã®æéãç縮ããäºãã§ãã¾ããã¾ããst.cacheã使ãäºã§è² è·ãã³ã¹ãé¢ã«åªããå½¢ã§å¯è¦åãè¡ãäºãåºæ¥ãå ´åãããã§ãããã
pyplotã®hockã¯ãJupyter Notebookã®hockã¨ã»ã¼åçã®æ©è½ã§ããmatplotlibãçµç±ãã¦ããã°ãpandasã§çµ±è¨éãç®åºãã¦è¡¨ç¤ºããããæ©æ¢°å¦ç¿ã¢ãã«ã®æ¨è«çµæãWordCloudãªã©æ§ã ãªçµæã表示ããäºãã§ãã¾ãã
ã
ä¸è¨ã¹ã¯ãªããã streamlit run
ã³ãã³ãã®å¼æ°ã«æå®ããæãlocalhostã«ä»¥ä¸ã®ãããªç»é¢ãè¿ããµã¼ãããã¹ãã£ã³ã°ããã¾ãã
ããã¹ããã©ã¼ã å ¥åã«å¿ããå¯è¦åçµæã表示ããã¦ãããã¨ãåããã¾ããã¾ããå度ãã©ã¼ã ãå ¥åããªããã¦å®è¡ãã¦ã¿ãã¨ãcacheãå¹ãã¦ããäºã確èªã§ãã¾ãã ãã¡ããããã®ä»ã«ããã¿ã³ããµã¤ããã¼ãè¨ç½®ããactionãhockããäºãã§ãã¾ãã
ã
ä¸æ¹ãStreamlitã¯ãã©ããããããã¿ã¤ãã³ã°ãç®çã¨ããææ³ãããåçãªãµã¼ãã¨ã®ããåããè¤éãªUIè¨è¨ã¯è¦æã§ãã Streamlitã®ããã³ãã¨ãµã¼ãã¨ã®ããåãã«ã¯webpackãèµ°ã£ã¦ãã¾ãããããã©ã«ãã§ã¯ãµã¼ãã¨ããã³ãã®ç¶æ å¤åãæ¤ç¥ãããã®ã§ãåæçãªå¯è¦åå¦çã«å¯¾å¿ãã¦ãã¾ããã åæçãªå¯è¦åãè¡ãå ´åã¯ãReactå´ã®Componentãç¬èªã«å®è£ ããå¿ è¦ããã£ãããGrid Layoutã§2ã«ã©ã ã§è¦ããçã®ãã¶ã¤ã³å¤æ´ã«ã¯ãplotlyãçµç±ãããªã©ãã¦å°ã è¤éãªæ¡å¼µå®è£ ãå¿ è¦ã«ãªãã¾ãã
å®ä¾ãåºãã¨ãStreamlitã³ãã¥ããã£ã®ã¢ãã¬ã¼ã¿ã§ããFanilo Andrianasoloæ°ãplotly.expressãåä½ãããããã®React Componentä½æä¾ãå ¬éãã¦ãã¾ãã dev.to
ã¾ããStreamlitã³ãã¥ããã£ã§ãå¤ãè²¢ç®ãã¦ããMarc Skov Madsenæ°ãawesome-streamlitã¨ããRepositoryãä½æãã¦ãããç§ãGrid LayoutãCookieã使ã£ãcacheä¿åã®å®è£ ãªã©å¤ããåèã«ãã¦ãã¾ãã github.com
ãããã£ã課é¡ã«é¢ãã¦ã¯ãStreamlitã³ãã¥ããã£ã§æ´»çºã«è°è«ãæããã¦ããã®ã§ã以ä¸ã§æ¤ç´¢ãã¦ã¿ãã®ããªã¹ã¹ã¡ã§ãã discuss.streamlit.io
ã
ã¢ããªã±ã¼ã·ã§ã³ãã¤ã³ãã©æ§æ
æ¬é¡ã¨ãã¦ãä»åä½æãã社å ç¨ã¢ããªã±ã¼ã·ã§ã³ã«ã¤ãã¦è§¦ãã¦ããã¾ãã
ä»åã¯ãBigQueryãBigTableãå é¨åãAPIãããã¼ã¿ãåå¾ãpandas DataFrameã«è½ã¨ãè¾¼ã¿ãçµ±è¨æ å ±ãè¨ç®ãmatplotlibã§å¯è¦åããå½¢ã«ãã¦ãã¾ãã
ä¸è¨ã¢ããªã±ã¼ã·ã§ã³ã社å åãã«ãã¹ãã£ã³ã°ããéã®ã¤ã³ãã©ã®æ§æã¨ãã¦ã¯ãä¸è¿°ã®éãããã§ã«éç¨ããã¦ããGKEã®ã¯ã©ã¹ã¿ä¸ã«ãããã¤ãããã¨ã¨ãã¾ããã ãã§ã«ãã¢ããªã±ã¼ã·ã§ã³ãDockerä¸ã§åãããã«éçºããã¦ãããã¨ããããç¹ã«é¸æè¢ã¨ãã¦ã¯éåæããªããã¨æãã¾ãã ã¾ããæ¢åã®ã¯ã©ã¹ã¿ã¯ç¤¾å åãã®ãµã¼ãã¹å ¬éç¨ã«ã»ãã¥ãªãã£çãªè¨å®ããªããã¦ãã¾ãããä»åã®ç¨éã«ã´ã£ããã§ããã
åè¿°ã®éããä»åä½æããå¯è¦åã¢ããªã§ã¯ã社å ã§å©ç¨ããã¦ããBigQueryããæ¢åã®BigTableãå¥ã¢ããªã±ã¼ã·ã§ã³ã®å é¨åãAPIã¨ã³ããã¤ã³ããåç §ãããã®ã¨ãªã£ã¦ãã¾ãã ãã®è¾ºãã®ãªã½ã¼ã¹ã¸ã®ã¢ã¯ã»ã¹å¶å¾¡ã¯ãå¿ è¦ãªãªã½ã¼ã¹ã«å¯¾ãã¦ã権éãä»ä¸ããService AccountããWorkload Identityãå©ç¨ãã¦ã ã¢ããªã¨ç´ä»ãããã¨ã§è¡ã£ã¦ãããã»ãã¥ãªãã£ãªã¹ã¯ã®ããã¢ã«ã¦ã³ããã¼ã®çºè¡ã管çãGCPãµã¤ãã«ä»»ãã¦ãã¾ãã
Kubernetesãªã½ã¼ã¹ã®è¨å®ã¯ä»¥ä¸ã®ãããªæãã«ãªãã¾ãã (説æã«å¿ è¦ãªç®æã®ã¿æ½åºãé©å®çç¥ãå¤æ´ãã¦ãã¾ã)ã
apiVersion: v1 kind: Service metadata: blahblah blahblah blahblah --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: visualizer name: visualizer namespace: sample spec: selector: matchLabels: app: visualizer template: metadata: labels: app: visualizer spec: containers: - command: - streamlit - run - /app/feed_checker_app.py - --server.port=8000 - --server.enableCORS=false env: - name: BIGQUERY_RELATED_CONFIG value: bigquery_related_config - name: BIGTABLE_RELATED_CONFIG value: bigtable_related_config - name: INTERNAL_API_RELATED_CONFIG value: internal_api_related_config image: app_image livenessProbe: failureThreshold: 6 httpGet: path: /healthz port: 8000 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 1 name: feed-visualizer ports: - containerPort: 8000 readinessProbe: failureThreshold: 3 httpGet: path: /healthz port: 8000 initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 1 resources: limits: memory: 2Gi requests: cpu: 100m memory: 1Gi --- apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: blahblah blahblah blahblah --- apiVersion: cloud.google.com/v1beta1 kind: BackendConfig metadata: name: visualizer namespace: sample spec: connectionDraining: drainingTimeoutSec: 40 securityPolicy: name: policy-name timeoutSec: 36000
ä¸è¨ã¯ãç¹ã«å¤ãã£ããã¨ã¯ãã¦ããããã¢ããªã±ã¼ã·ã§ã³ãããã¤ã®éã®æ¨æºçãªè¨å®ã§ãã
Streamlitç¹æã®è¨å®ã¨ãã¦ã¯ã/healthz
ã¨ãããã¹ã«å¹³å¸¸æã«200ãè¿ãã¨ã³ããã¤ã³ãããã *2 ã®ã§ããããliveness, readinessã«å©ç¨ãã¦ãã¾ãã
ãã ããæä¸é¨ã«å®ç¾©ãããBackendConfig
ã«éè¦ãªãã¤ã³ããããã¾ãã
BackendConfigã¯GKEã§å©ç¨å¯è½ãªCustom Resouce Definition (CRD) ã§ãããCloud ArmorãCloud CDNãLBã®timeoutãªã©ã®è¨å®ãè¨è¿°ããã¨ãè¨å®å
容ãå種GCPãªã½ã¼ã¹ã¨é£æºããåä½ãã¾ãã
ããã§ãä»åã®ã¢ããªã±ã¼ã·ã§ã³ã§ã¯ãLBã®timeoutã 36000
ã«è¨å®ãã¦ããã¾ã (ã¡ãªã¿ã«defaultã¯30)ã
ãã®ããã«è¨å®ããçç±ã¯ãStreamlitãã 以ä¸ãªã©ã«æãããã¦ãããããªåé¡ã«ãããWebSocketã®éä¿¡ã®é¢ä¿ã§ãLBã®ã¿ã¤ã ã¢ã¦ãæã«ãç»é¢ããªã»ããããã¦ãã¾ããããã¾ã§è§¦ã£ã¦ããå¯è¦åã®è¨å®ãå
ã«æ»ã£ã¦ãã¾ãã¨ããåé¡ãããããã§ã (ãã¼ã«ã«éçºæã®Dockerã§ã¯åç¾ããªãã£ãã®ã§èª¿æ»ã«å°ãæéåãã¾ãã)ã
Publicãªå ´æã«å
¬éããã¢ããªã§ãã®ãããªè¨å®ã¯åé¡ãããã¾ããã社å
åããã¤éå®çãªã¡ã³ãã¼ããé²è¦§ããªããã¨ãããä»åã¯ããã®ãããªè¨å®ã§ä¸æçã«éãããã¨ã«ãã¾ããã
LBã«é¢ããè°è«ãããã¥ã¡ã³ãã¯ä»¥ä¸ãåèã«ãã¦ãã¾ãã discuss.streamlit.io cloud.google.com
ãã®åé¡ã«ã¤ãã¦ã¯ãæ©ãè¦ã¦ãæ¬ä½ã«contributeããã®ãããããããã¾ããã ãã®è¾ºãã¯ãStreamlitã®ã¾ã æ¯ããã£ã¦ããªãé¨åã§ããã ã»ã¼ããã³ãã¨ã³ãã³ã¼ããè¨è¿°ããå¿ è¦ããªããä½ãã³ã¹ãã§é«å質ãªå¯è¦åãå®ç¾ã§ããç´ æ©ã社å ã«å ¬éã§ããã¡ãªããã¯ãæ¬ ç¹ãè£ã£ã¦ä½ãããã¨æãã¾ãã
ã
ãããã«
ä»åã¯ããã¼ã¿å¯è¦åã¢ããªã±ã¼ã·ã§ã³ã®é¸æã¨Streamlitã«ããã¢ããªã±ã¼ã·ã§ã³ä½æãã¯ã¤ãã¯ã«ç¤¾å åãã«å ¬éãããã¦ãã¦ã«ã¤ãã¦ç´¹ä»ãã¾ããã
ã
We're hiring
ã¨ã ã¹ãªã¼ã§ã¯ãæ©æ¢°å¦ç¿ã®ã¿ã¹ã¯ãå®è£ ããçµæãå¯è¦åãã¦é«éã«æ¹åãããã¨ã§ãå»çãåé²ããããããã¯ããéçºããã¨ã³ã¸ãã¢ãåéãã¦ãã¾ãï¼ æããã¯ï¼ ã¨ããæ¹ã¯ãã²ä»¥ä¸ãããå¿åãã ããï¼
*1:ã¾ã 1æ¬ããåºã¦ãã¾ããã2æ¬ç®ãä¼ç»ä¸ã§ãã®ã§æ¯éãã£ã³ãã«ç»é²ãé¡ããã¾ã https://youtu.be/eTZkl7EdT4A
*2:å®è£ ç®æã¯ããã®è¾ºã https://github.com/streamlit/streamlit/blob/6754e97e52e783f64f6ed49c1cdc75b881ade892/lib/streamlit/server/server.py#L330-L332